5 Simulation Control

5.4 Probes

In addition to controllers and monitors, applications can also attach streams of data, known as probes, to input and output values associated with the simulation. Probes derive from the same base class ModelAgentBase as controllers and monitors, but differ in that

  1. 1.

    They are associated with an explicit time interval during which they are applied;

  2. 2.

    They can have an attached file for supplying input data or recording output data;

  3. 3.

    They are displayable in the ArtiSynth timeline panel.

A probe is applied (by calling its apply() method) only for time steps that fall within its time interval. This interval can be set and queried using the following methods:

   setStartTime (double t0);
   setStopTime (double t1);
   setInterval (double t0, double t1);
   double getStartTime();
   double getStopTime();

The probe’s attached file can be set and queried using:

   setAttachedFileName (String filePath);
   String getAttachedFileName ();

where filePath is a string giving the file’s path name. If filePath is relative (i.e., it does not start at the file system root), then it is assumed to be relative to the ArtiSynth working folder, which can be queried and set using the methods

   File ArtisynthPath.getWorkingFolder();
   ArtisynthPath.setWorkingFolder (File folder);

of ArtisynthPath. The working folder can also be set from the ArtiSynth GUI by choosing File > Set working folder ....

If not explicitly set within the application, the working folder will default to a system dependent setting, which may be the user’s home folder, or the working folder of the process used to launch Artisynth.

Details about the timeline display can be found in the section “The Timeline” in the ArtiSynth User Interface Guide.

There are two types of probe: input probes, which are applied at the beginning of each simulation step before the controllers, and output probes, which are applied at the end of the step after the monitors.

While applications are free to construct any type of probe by subclassing either InputProbe or OutputProbe, most applications utilize either NumericInputProbe or NumericOutputProbe, which explicitly implement streams of numeric data which are connected to properties of various model components. The remainder of this section will focus on numeric probes.

As with controllers and monitors, probes also implement a isActive() method that indicates whether or not the probe is active. Probes that are not active are not invoked. Probes also provide a setActive() method to control this setting, and export it as the property active. This allows probe activity to be controlled at run time.

To enable or disable a probe at run time, locate it in the navigation panel (under the RootModel’s inputProbes or outputProbes list), chose Edit properties ... from the right-click context menu, and set the active property as desired.
Probes can also be enabled or disabled in the timeline, by either selecting the probe and invoking activate or deactivate from the right-click context menu, or by clicking the track mute button (which activates or deactivates all probes on that track).

5.4.1 Numeric probe structure

Numeric probes are associated with:

  • A vector of temporally-interpolated numeric data;

  • One or more properties to which the probe is bound and which are either set by the numeric data (input probes), or used to set the numeric data (output probes).

The numeric data is implemented internally by a NumericList, which stores the data as a series of vector-valued knot points at prescribed times t_{k} and then interpolates the data for an arbitrary time t using an interpolation scheme provided by Interpolation.

Some of the numeric probe methods associated with the interpolated data include:

   int getVsize();                      // returns the size of the data vector
   setInterpolationOrder (Order order); // sets the interpolation scheme
   Order getInterpolationOrder();       // returns the interpolation scheme
   VectorNd getData (double t);         // interpolates data for time t
   NumericList getNumericList();        // returns the underlying NumericList

Interpolation schemes are described by the enumerated type Interpolation.Order and presently include:

Step

Values at time t are set to the values of the closest knot point k such that t_{k}\leq t.

Linear

Values at time t are set by linear interpolation of the knot points (k,k+1) such that t_{k}\leq t\leq t_{k+1}.

Parabolic

Values at time t are set by quadratic interpolation of the knots (k-1,k,k+1) such that t_{k}\leq t\leq t_{k+1}.

Cubic

Values at time t are set by cubic Catmull interpolation of the knots (k-1,\ldots,k+2) such that t_{k}\leq t\leq t_{k+1}.

Each property bound to a numeric probe must have a value that can be mapped onto a scalar or vector value. Such properties are know as numeric properties, and whether or not a value is numeric can be tested usingNumericConverter.isNumeric(value).

By default, the total number of scalar and vector values associated with all the properties should equal the size of the interpolated vector (as returned by getVsize()). However, it is possible to establish more complex mappings between the property values and the interpolated vector. These mappings are beyond the scope of this document, but are discussed in the sections “General input probes” and “General output probes” of the ArtiSynth User Interface Guide.

5.4.2 Creating probes in code

This section discusses how to create numeric probes in code. They can also be created and added to a model graphically, as described in the section “Adding and Editing Numeric Probes” in the ArtiSynth User Interface Guide.

Numeric probes have a number of constructors and methods that make it relatively easy to create instances of them in code. For NumericInputProbe, there is the constructor

   NumericInputProbe (ModelComponent c, String propPath, String filePath);

which creates a NumericInputProbe, binds it to a property located relative to the component c by propPath, and then attaches it to the file indicated by filePath and loads data from this file (see Section 5.4.4). The probe’s start and stop times are specified in the file, and its vector size is set to match the size of the scalar or vector value associated with the property.

To create a probe attached to multiple properties, one may use the constructor

   NumericInputProbe (ModelComponent c, String propPaths[], String filePath);

which binds the probe to multiple properties specified relative to c by propPaths. The probe’s vector size is set to the sum of the sizes of the scalar or vector values associated with these properties.

For NumericOutputProbe, one may use the constructor

   NumericOutputProbe (ModelComponent c, String propPath, String filePath, double interval);

which creates a NumericOutputProbe, binds it to the property propPath located relative to c, and then attaches it to the file indicated by filePath. The argument interval indicates the update interval associated with the probe, in seconds; a value of 0.01 means that data will be added to the probe every 0.01 seconds. If interval is specified as -1, then the update interval will default to the simulation step size. This interval can also be accessed after the probe is created using

   int getUpdateInterval()              // returns the update interval
   void setUpdateInterval(int sec)      // sets the update interval (seconds)

[] To create an output probe attached to multiple properties, one may use the constructor

   NumericOutputProbe (
      ModelComponent c, String propPaths[], String filePath, double interval);

As the simulation proceeds, an output probe will accumulate data, but this data will not be saved to any attached file until the probe’s save() method is called. This can be requested in the GUI for all probes by clicking on the Save button in the timeline toolbar, or for specific probes by selecting them in the navigation panel (or the timeline) and then choosing Save data in the right-click context menu.

Output probes created with the above constructors have a default interval of [0, 1]. A different interval may be set using setInterval(), setStartTime(), or setStopTime().

5.4.3 Example: probes connected to SimpleMuscle

A model showing a simple application of probes is defined in

  artisynth.demos.tutorial.SimpleMuscleWithProbes

This extends SimpleMuscle (Section 4.5.2) to add an input probe to move particle p1 along a defined path, along with an output probe to record the velocity of the frame marker. The complete class definition is shown below:

1 package artisynth.demos.tutorial;
2
3 import java.io.IOException;
4 import maspack.matrix.*;
5 import maspack.util.PathFinder;
6
7 import artisynth.core.modelbase.*;
8 import artisynth.core.mechmodels.*;
9 import artisynth.core.probes.*;
10
11 public class SimpleMuscleWithProbes extends SimpleMuscleWithPanel
12 {
13    public void createInputProbe() throws IOException {
14       NumericInputProbe p1probe =
15          new NumericInputProbe (
16             mech, "particles/p1:targetPosition",
17             PathFinder.getSourceRelativePath (this, "simpleMuscleP1Pos.txt"));
18       p1probe.setName("Particle Position");
19       addInputProbe (p1probe);
20    }
21
22    public void createOutputProbe() throws IOException {
23       NumericOutputProbe mkrProbe =
24          new NumericOutputProbe (
25             mech, "frameMarkers/0:velocity",
26             PathFinder.getSourceRelativePath (this, "simpleMuscleMkrVel.txt"),
27             0.01);
28       mkrProbe.setName("FrameMarker Velocity");
29       mkrProbe.setDefaultDisplayRange (-4, 4);
30       mkrProbe.setStopTime (10);
31       addOutputProbe (mkrProbe);
32    }
33
34    public void build (String[] args) throws IOException {
35       super.build (args);
36
37       createInputProbe ();
38       createOutputProbe ();
39       mech.setBounds (-1, 0, -1, 1, 0, 1);
40    }
41
42 }

The input and output probes are added using the custom methods createInputProbe() and createOutputProbe(). At line 14, createInputProbe() creates a new input probe bound to the targetPosition property for the component particles/p1 located relative to the MechModel mech. The same constructor attaches the probe to the filesimpleMuscleP1Pos.txt, which is read to load the probe data. The format of this and other probe data files is described in Section 5.4.4. The method PathFinder.getSourceRelativePath() is used to locate the file relative to the source directory for the application model (see Section 2.6). The probe is then given the name "Particle Position" (line 18) and added to the root model (line 19).

Similarly, createOutputProbe() creates a new output probe which is bound to the velocity property for the component particles/0 located relative to mech, is attached to the file simpleMuscleMkrVel.txt located in the application model source directory, and is assigned an update interval of 0.01 seconds. This probe is then named "FrameMarker Velocity" and added to the root model.

The build() method calls super.build() to create everything required for SimpleMuscle, calls createInputProbe() and createOutputProbe() to add the probes, and adjusts the MechModel viewer bounds to make the resulting probe motion more visible.

To run this example in ArtiSynth, select All demos > tutorial > SimpleMuscleWithProbes from the Models menu. After the model is loaded, the input and output probes should appear on the timeline (Figure 5.4). Expanding the probes should display their numeric contents, with the knot points for the input probe clearly visible. Running the model will cause particle p1 to trace the trajectory specified by the input probe, while the velocity of the marker is recorded in the output probe. Figure 5.5 shows an expanded view of both probes after the simulation has run for about six seconds.

Figure 5.4: Timeline view of the probes created by SimpleMuscleWithProbes.
Figure 5.5: Expanded view of the probes after SimpleMuscleWithProbes has run for about 6 seconds, showing the data accumulated in the output probe "FrameMarker Velocity".

5.4.4 Data file format

The data files associated with numeric probes are ASCII files containing two lines of header information followed by a set of knot points, one per line, defining the numeric data. The time value (relative to the probe’s start time) for each knot point can be specified explicitly at the start of the each line, in which case the file takes the following format:

startTime stopTime scale
interpolation vsize explicit
t0 val00 val01 val02 ...
t1 val10 val11 val12 ...
t0 val20 val21 val22 ...
...

Knot point information begins on line 3, with each line being a sequence of numbers giving the knot’s time followed by n values, where n is the vector size of the probe (i.e., the value returned by getVsize()).

Alternatively, time values can be implicitly specified starting at 0 (relative to the probe’s start time) and incrementing by a uniform timeStep, in which case the file assumes a second format:

startTime stopTime scale
interpolation vsize timeStep
val00 val01 val02 ...
val10 val11 val12 ...
val20 val21 val22 ...
...

For both formats, startTime, startTime, and scale are numbers giving the probe’s start and stop time in seconds and scale gives the scale factor (which is typically 1.0). interpolation is a word describing how the data should be interpolated between knot points and is the string value of Interpolation.Order as described in Section 5.4.1 (and which is typically Linear, Parabolic, or Cubic). vsize is an integer giving the probe’s vector size.

The last entry on the second line is either a number specifying a (uniform) time step for the knot points, in which case the file assumes the second format, or the keyword explicit, in which case the file assumes the first format.

As an example, the file used to specify data for the input probe in the example of Section 5.4.3 looks like the following:

0 4.0 1.0
Linear 3 explicit
0.0  0.0 0.0 0.0
1.0  0.5 0.0 0.5
2.0  0.0 0.0 1.0
3.0 -0.5 0.0 0.5
4.0  0.0 0.0 0.

Since the data is uniformly spaced beginning at 0, it would also be possible to specify this using the second file format:

0 4.0 1.0
Linear 3 1.0
 0.0 0.0 0.0
 0.5 0.0 0.5
 0.0 0.0 1.0
-0.5 0.0 0.5
 0.0 0.0 0.

5.4.5 Adding input probe data in code

It is also possible to specify input probe data directly in code, instead of reading it from a file. For this, one would use the constructor

   NumericInputProbe (ModelComponent c, String propPath, double t0, double t1)

which creates a NumericInputProbe with the specified property and with start and stop times indicated by t0 and t1. Data can then be added to this probe using the method

   addData (double[] data, double timeStep)

where data is an array of knot point data. This contains the same knot point information as provided by a file (Section 5.4.4), arranged in row-major order. Times values for the knots are either implicitly specified, starting at 0 (relative to the probe’s start time) and increasing uniformly by the amount specified by timeStep, or are explicitly specified at the beginning of each knot if timeStep is set to the built-in constant NumericInputProbe.EXPLICIT_TIME. The size of the data array should then be either n*m (implicit time values) or (n+1)*m (explicit time values), where n is the probe’s vector size and m is the number of knots.

As an example, the data for the input probe in Section 5.4.3 could have been specified using the following code:

      NumericInputProbe p1probe =
         new NumericInputProbe (
            mech, "particles/p1:targetPosition", 0, 5);
      p1probe.addData (
         new double[] {
            0.0,  0.0, 0.0, 0.0,
            1.0,  0.5, 0.0, 0.5,
            2.0,  0.0, 0.0, 1.0,
            3.0, -0.5, 0.0, 0.5,
            4.0,  0.0, 0.0, 0.0 },
            NumericInputProbe.EXPLICIT_TIME);

When specifying data in code, the interpolation defaults to Linear unless explicitly specified usingsetInterpolationOrder(), as in, for example:

   probe.setInterpolationOrder (Order.Cubic);

Data can also be added incrementally, using the method

   probe.addData (double t, VectorNd values)

which adds a data knot the specified values at time t; values should have a size equal to the probe’s vector size. For example, the array-based example above could instead by implemented as

      NumericInputProbe p1probe =
         new NumericInputProbe (
            mech, "particles/p1:targetPosition", 0, 5);
      p1probe.addData (0.0, new VectorNd (0.0, 0.0, 0.0));
      p1probe.addData (1.0, new VectorNd (0.5, 0.0, 0.5));
      p1probe.addData (2.0, new VectorNd (0.0, 0.0, 1.0));
      p1probe.addData (3.0, new VectorNd (-0.5, 0.0, 0.5));
      p1probe.addData (4.0, new VectorNd (0.0, 0.0, 0.0));

When an input probe is created without data specified in its constructor (e.g., by means of a probe data file), then it automatically adds knot points at its start and end times, with values set from the current values of its bound properties. Applying such a probe, without adding additional data, will thus leave the values of its properties unchanged.

The addData() methods described above do not remove existing knot points (although they will overwrite knots that exist at the same times). In order to replace all data, one can first call the method

   probe.clearData()

which removes all knot points. Probes with no knots assign 0 to all their property values. Data can subsequently be added using the addData() methods. Alternatively, one can call

   setData (double[] data, double timeStep)

which behaves identically to the addData(double[],timeStep) method except that it first removes all existing data.

Another convenient way to add data to a probe is to simply copy it from another probe, using the method

   setValues (NumericProbeBase src, boolean useAbsoluteTime)

All knot points and values are copied from src, which can be any numeric probe (input or output), with the only restriction being that its vector size must equal or exceed that of the calling probe (extra values are ignored). If useAbsoluteTime is true, time values are mapped between the probes using absolute time; otherwise, probe relative time is used. When using this method, care should be taken to ensure that the there will be knot points to cover the start and stop times of the calling probe.

5.4.5.1 Extending data to the start and end

By explicitly using the clearData() or setData() methods, it is possible to create an input probe for which data is missing at its start and/or stop times. The behavior in that case is determined by the probe’s extendData property: if the property is set to true, then data values between the start time and the first knot, or the last knot and the stop time, are set to the values of the first and last knot, respectively. Otherwise, these data values are set to zero.

As with all properties, extendData can be set and queried in code using the accessors

   boolean getExtendData()
   void setExtendData (boolean enable)

The default value of extendData is true. As mentioned above, probes with no data set their data values uniformly to 0.

5.4.6 Tracing probes

Figure 5.6: Tracing probes used to show the desired target (cyan) and actual (gold) trajectories of some marker points attached near the elbow of an ArtiSynth shoulder model undergoing an inverse simulation.

It is often useful to follow the path or evolution of a position or vector quantity as it evolves in time during a simulation. For example, one may wish to trace out the path of some points, as per Figure 5.6, which traces the actual and desired paths of some marker points during an inverse simulation of the type described in Chapter 10.

ArtiSynth supplies TracingProbes that can be used for this purpose. A tracing probe can be attached to any component that implements the interface Traceable, for any of the properties that are exported by its getTraceables() method. At present, Traceable is implemented for the following components:

Point

Traceable properties are point and force.

Frame

Traceable properties are transforce and moment.

Tracing probes are easily created using the RootModel method

TracingProbe addTracingProbe (Traceable comp, String propName, double startTime, double stopTime)

which produces a tracing probe of specified duration for the indicated component and property name pair. During simulation, the probe will record the desired property at the rate defined by its updateInterval, and also render it in the viewer. In cases where the probe renders all its stored values (as with PointTracingProbes, described below), the interval at which this occurs can be controlled by its renderInterval property, accessed in code via

double getRenderInterval()

Return the render interval

void setRenderInterval(double interval)

Set the render interval (seconds)

Specifying a render interval of -1 (the default) will cause the render interval to equal the update interval.

Two types of tracing probes are currently implemented:

PointTracingProbe

Created for point properties, this renders the path of the point position over the duration of the probe, either as a continuous curve (if the probe’s pointTracing property is false, the default setting), or as discrete points. The appearance of the rendering is controlled by subproperties of the probe’s render properties, using lineStyle, lineRadius, lineWidth and lineColor (for continuous curves) or pointStyle, pointRadius, pointSize and pointColor (for points).

VectorTracingProbe

Created for vector properties, this renders the vector quantity at the current simulation time as a line segment anchored at the current position of the component. The length of the line segment is controlled by the probe’s lengthScaling property, and other aspects of the appearance are controlled by the subproperties lineStyle, lineRadius, lineWidth and lineColor of the probe’s render properties. Vector tracing is mostly used to visual instantaneous forces.

The following code fragment attaches a tracing probe to the last frame marker of the MultiJointedArm example of Section 3.5.16:

import artisynth.core.probes.TracingProbe;
   ...
   FrameMarker mkr = myMech.frameMarkers().get(/*mkrIndex*/4);
   TracingProbe tprobe = addTracingProbe (mkr, "position", 0.0, 2.0);
   tprobe.setName ("marker trace");
   RenderProps.setLineColor (tprobe, Color.ORANGE);

with the results shown in Figure 5.7 (left). To instead render the output as points, and with a coarser render interval of 0.02, one could modify the probe properties using

import artisynth.core.probes.PointTracingProbe;
   ...
   ((PointTracingProbe)tprobe).setPointTracing (true);
   tprobe.setRenderInterval (0.02); // display points 0.02 sec apart
   RenderProps.setSphericalPoints(tprobe, 0.01, Color.GREEN);

with the results shown in Figure 5.7 (right).

Figure 5.7: Left: tracing applied to the distal marker of MultiJointedArm. Right: same trace, with pointTracing enabled.

Tracing probes can also be created and edited interactively, as described in the section “Point Tracing” of the ArtiSynth User Interface Guide.

5.4.7 Position probes

Applications often find it useful to parametrically control the position of point or frame-based components, which include Point, Frame, FixedMeshBody. and their subclasses (e.g., Particle, RigidBody, and FemNode3d). This can be done by setting either the components’ position or targetPosition properties, and (for frame-based components) their orientation or targetOrientation properties. As described in Section 5.3.1.1, setting targetPosition and targetOrientation is often preferable as this will automatically update the component’s velocity, and so provide more information to the integrator, whereas setting position and orientation will leave the velocity unchanged.

When controlling the position and/or velocity of dynamic components, which include Point and Frame, it is important to ensure that their dynamic property is set to false and that they are unattached. Otherwise, the dynamic simulation will override any attempt at setting their position or velocity.

For frame-based components, the orientation and targetOrientation properties describe their spatial orientation using an instance of AxisAngle, which represents a single rotation \theta about a specific axis {\bf u} in world coordinates (any 3D rotation can be expressed this way). However, when using this in a probe to control or observe a frame-based component’s pose over time, a couple of issues arise:

  • There are other rotation representations that may be more useful or easier to specify, such as z-y-x or x-y-z rotations, in either radians or degrees.

  • Whatever rotation representation is used, care must be taken when interpolating it between knot points. Rotations cannot be treated as vectors and interpolation must instead take the form of curves on the 3D rotation group SO(3). The details are beyond the scope of this document but were initially described by Shoemake [shoemake1985animating].

To address these issues, ArtiSynth supplies PositionInputProbe and PositionOutputProbe, which can be used to control or observe the position of one or more points or frame-based components, while offering a variety of rotation representations and handling rotation interpolation correctly. In particular, PositionInputProbes allow an application to supply key-frame animation control poses for frame-based components.

PositionInputProbes can be created with the constructors

PositionInputProbe (String name, ModelComponent comp, RotationRep rotRep, Mboolean useTargetProps, double startTime, double stopTime)
PositionInputProbe (String name, Collection<? extends ModelComponent> comps, MRotationRep rotRep, boolean useTargetProps, double startTime, double stopTime)
PositionInputProbe (String name, ModelComponent comp, RotationRep rotRep, Mboolean useTargetProps, String filePath)
PositionInputProbe (String name, Collection<? extends ModelComponent> comps, MRotationRep rotRep, boolean useTargetProps, String filePath)

In the above, name is the probe name (optional, can be null), and comp and comps refer to the component(s) to be assigned to the probe; as indicated above, these must be instances of Point, Frame, or FixedMeshBody. rotRep is an instance of RotationRep, which indicates how rotations should be represented, as described further below. The argument useTargetProps, if true, specifies that the probe should be attached to the targetPosition and (for frames) targetOrientation properties, vs. the position and orientation properties (if false). startTime and stopTime give the probe’s start and stop times, while filePath specifies a probe file from which to read in the probe’s basic parameters and data, per Section 5.4.4. Probes constructed without a file need to have data added, as per Section 5.4.5.

As indicated by the constructors, NumericInputProbes can control multiple point and frame-based components, and this arrangement affects the size and composition of the data vector. Each point component requires 3 numbers specifying position, while each frame components requires 3+r numbers specifying position and orientation, where r is the size of the of the rotation representation. Although the orientation and targetOrientation properties describe rotation using an AxisAngle, this will be converted into knot point data based on the assigned RotationRep, with r equal to either 3 or 4. The total m of the data vector is then

m=3n_{p}+(3+r)n_{f}

where n_{p} is the number of points and n_{f} is the number of frames.

RotationRep provides a variety of ways to specify the rotation and applications can choose the most suitable:

ZYX

Successive rotations, in radians, about the z-y-x axes; r=3.

ZYX_DEG

Successive rotations, in degrees, about the z-y-x axes; r=3.

XYZ

Successive rotations, in radians, about the x-y-z axes; r=3.

XYZ_DEG

Successive rotations, in degrees, about the x-y-z axes; r=3.

AXIS_ANGLE

Axis-angle representation (u, \theta), with the \theta given in radians; r=4. The axis u does not need to be normalized.

AXIS_ANGLE_DEG

Axis-angle representation (u, \theta), with the \theta given in degrees; r=4. The axis u does not need to be normalized.

QUATERNION

Unit quaternion; r=4.

As a possibly more convenient alternative to the methods of Section 5.4.5, and to avoid having to explicitly pack the position and orientation information into the probe’s knot data, PositionInputProbes also supply the following methods to set the knot data for individual components being controlled by the probe:

setPointData (Point point, double time, Vector3d pos)

Set point position at a given time.

setPointData (ModelComponent frame, double time, MRigidTransform3d TFW)

Set frame position/orientation at a given time.

Each of these methods writes only the portion of the knot point at the indicated time that corresponds to the specified component; other portions are left unchanged or initialized to 0. For frames, TFW gives the pose (position/orientation) in the form of the transform from frame to world coordinates, and the orientation is transformed into knot point data according the the probe’s rotation representation.

PositionOutputProbes can be created with constructors analogous to PositionInputProbe:

PositionOutputProbe (String name, ModelComponent comp, RotationRep rotRep, Mdouble startTime, double stopTime)
PositionOutputProbe (String name, Collection<? extends ModelComponent> comps, MRotationRep rotRep, double startTime, double stopTime)
PositionOutputProbe (String name, ModelComponent comp, RotationRep rotRep, MString filePath, double startTime, double stopTime, double interval)
PositionOutputProbe (String name, Collection<? extends ModelComponent> comps, MRotationRep rotRep, String filePath, double startTime, double stopTime, double interval)

The knot data format is the same as for PositionInputProbes, with rotations represented according to the setting of rotRep. There is no useTargetProps argument because PositionOutputProbes bind only to the position and orientation properties. If present, the filePath and interval arguments specify an attached file name for saving probe data and the update interval in seconds. Setting interval to -1 causes updating to occur at the simulation update rate, and this is the default for constructors without an interval argument.

5.4.8 Velocity probes

As with positions, applications may sometimes need to parametrically control or observe the velocity of point or frame-based components, which include Point, Frame, and their subclasses (but not FixedMeshBody, as this is not a dynamic component and so does not maintain a velocity). This can be done by setting either the components’ velocity or targetVelocity properties, with the latter sometimes preferable as it will automatically update the component’s position, whereas setting velocity will not. For frame-based components, velocity and targetVelocity are described by a 6-DOF Twist component that contains both translational and angular velocities. Unlike with finite rotations, there is only one representation for angular velocity (as a vector with units of radians/sec) and there are no interpolation issues.

When the position of a component is being controlled it is typically not necessary to also supply velocity information. This is especially true for position probes bound to targetPosition and targetOrientation properties, since these update velocity information automatically. However, for position probes bound to position and orientation, it can be sometimes be useful to use a velocity probe to also supply velocities. One example of this is when components are being tracked by inverse simulation (Chapter [InverseSimulation:sec]), as this extra information this can reduce tracking error and lag.

If a position probe (either input or output) exists for a given collection of components, it is possible to automatically create a VelocityInputProbe for the same set of components, using one of the following static methods:

VelocityInputProbe VelocityInputProbe.createNumeric (String name, MNumericProbeBase source, double interval)

Create by numeric differentiation.

VelocityInputProbe VelocityInputProbe.createInterpolated (String name, MNumericProbeBase source, double interval)

Create by interpolation.

Both take an optional probe name and source probe. The update interval gives the time interval between the created knot points, with -1 causing this to be taken from the source. The first method creates the probe by numeric differentiation of the source’s position data and should be used only when the source’s data is dense, while the second method finds the derivative from the interpolation method of the source probe (as described by getInterpolationOrder()) and is better suited when the source data is sparse (although the resulting velocity will not be continuous unless the interpolation order is greater than linear).

The following code fragments creates a position probe and then uses this to generate a velocity probe:

   PositionInputProbe posProbe;
   VelocityInputProbe velProve;
   String bodyFilePath;
   RigidBody body;
   ...
   boolean useTargetProps = true;
   posProbe = new PositionInputProbe (
      "body position", body, RotationRep.XYZ, useTargetProps, bodyFilePath);
   velProbe = VelocityInputProbe.createNumeric (
      "body velocity", posProbe, /*interval*/-1);

5.4.9 Example: controlling a point and frame

Figure 5.8: Left: PositionProbes when first loaded. Right: model running, showing the point and monkey orbiting around each other and the particle trace in cyan.

A model demonstrating position and velocity probes is defined in

  artisynth.demos.tutorial.PositionProbes

It creates a point and a rigid body (using the Blender monkey mesh), sets both to be non-dynamic, uses position and velocity probes to control their positions, and uses PositionOutputProbe and VelocityOutputProbe to monitor the resulting positions and velocities. The model class definition, excluding the include directives, is shown here:

1 public class PositionProbes extends RootModel {
2    private static final double PI = Math.PI;  // simplify the code
3    private double startTime = 0;           // probe start times
4    private double stopTime = 2;            // probe stop times
5    private boolean useTargetProps = true;  // bind input probes to target props
6
7    public void build (String[] args) throws IOException {
8       MechModel mech = new MechModel ("mech");
9       addModel (mech);
10
11       // Create a rigid body and a point to control the positions of.
12       PolygonalMesh mesh = new PolygonalMesh (
13          getSourceRelativePath ("data/BlenderMonkey.obj"));
14       RigidBody monkey = RigidBody.createFromMesh (
15          "monkey", mesh, /*density*/1000.0, /*scale*/1);
16       monkey.setDynamic (false);
17       mech.addRigidBody (monkey);
18       Point point = new Point ("point", new Point3d(0, 0, 3));
19       mech.addPoint (point);
20
21       // Make a list of these components for creating the probes.
22       ArrayList<ModelComponent> comps = new ArrayList<>();
23       comps.add (point);
24       comps.add (monkey);
25
26       // Create a PositionInputProbe to control the component positions. Since
27       // the RotationRep will be ZYX, body poses are specified using 3 position
28       // values and 3 ZYX angles in radians
29       PositionInputProbe pip = new PositionInputProbe (
30          "target positions", comps, RotationRep.ZYX,
31          useTargetProps, startTime, stopTime);
32
33       pip.setData (new double[] {
34       /*  time  point pos       monkey pos & rotation */
35           0.0,    0, 0, 3.0,    0.0, 0.0, 0.0,  0, 0, 0,
36           0.5, -1.5, 0, 1.5,    1.5, 0.0, 1.5,  0, PI/2, 0,
37           1.0,    0, 0, 0,      0.0, 0.0, 3.0,  0, PI, 0,
38           1.5,  1.5, 0, 1.5,   -1.5, 0.0, 1.5,  0, 3*PI/2, 0,
39           2.0,    0, 0, 3.0,    0.0, 0.0, 0.0,  0, 0, 0,
40          }, NumericProbeBase.EXPLICIT_TIME);
41       // since the knot points are sparse, use cubic interpolation to get a
42       // smoother motion
43       pip.setInterpolationOrder (Interpolation.Order.Cubic);
44       addInputProbe (pip);
45
46       // Create a VelocityInputProbe by differentiating the position probe.
47       VelocityInputProbe vip = VelocityInputProbe.createInterpolated (
48          "target velocities", pip, useTargetProps, 0.02);
49       addInputProbe (vip);
50
51       // Create a PositionOutputProbe to record the component positions
52       PositionOutputProbe pop = new PositionOutputProbe (
53          "tracked positions", comps, RotationRep.ZYX, startTime, stopTime);
54       addOutputProbe (pop);
55
56       // Create a VelocityOutputProbe to record the component velocities
57       VelocityOutputProbe vop = new VelocityOutputProbe (
58          "tracked velocities", comps, startTime, stopTime);
59       addOutputProbe (vop);
60
61       // add a tracing probe to view the path of the point in CYAN
62       TracingProbe tprobe =
63          addTracingProbe (point, "position", startTime, stopTime);
64       tprobe.setName ("point tracing");
65       RenderProps.setLineColor (tprobe, Color.CYAN);
66
67       // render properties:
68       // draw point as a large red sphere; set color for the monkey
69       RenderProps.setSphericalPoints (mech, 0.2, Color.RED);
70       // set color for monkey
71       RenderProps.setFaceColor (mech, new Color(1f, 1f, 0.6f));
72    }
73
74 }

The build() creates a MechModel and then adds to it a rigid body (created from the Blender monkey mesh) and a particle (lines 8-19). Both components are made non-dynamic to allow their positions and velocities to be controlled, and references to them are placed a list used to create the probes (lines 21-24).

A PositionInputProbe for controlling the positions is created at lines 26-44, using a rotation representation of RotationRep.ZYX (successive rotations about the z-y-x axes, in radians). For this example, the useTargetProps is set to false (so that the probe binds to position and orientation), because we are going to be specifying velocities explicitly using another probe. Data for the probe is set using setData(), with the point and monkey positions/poses specified, in order, for times 0, 0.5, 1.0, 1.5 and 2. As per the rotation specification, poses for the monkey are given by 3 positions and 3 z-y-x rotations. Because the data is sparse, the probe’s interpolation order is set to Cubic to generate smooth motions.

A VelocityInputProbe is created by differentiated the position probe (lines 47-49); createInterpolated() is used because the position data is sparse and so numeric differentiation would yield bad results.

Finally, output probes to track both the resulting positions and velocities, together with a tracing probe to render the point’s path in cyan, are created at lines 51-65. When creating the PositionOutputProbe, we use the same rotation representation RotationRep.ZYX as for the input probe, but this is not necessary. For example, if one were to instead specify RotationRep.QUATERNION, then the frame orientations would be specified as quaternions.

To run this example in ArtiSynth, select All demos > tutorial > PositionProbes from the Models menu. When run, both the point and monkey will orbit around each other, with the point path being displayed by the tracing probe (Figure 5.8, right).

Figure 5.9: Left: plot of the PositionInputProbe data, showing a discontinuity at t=1.75 for monkey’s y axis rotation data. Right: plot of the PositionOutputProbe data shows the continuity of the actual trajectory.

Several things about this demo should be noted:

  • The use of setData() in creating the position probe could be replaced by setPointData() and setFrameData(), as per

      pip.setPointData (point, 0.5, new Point3d(-1.5,0,1.5));
      pip.setPointData (point, 1.0, new Point3d(0,0,0));
      pip.setPointData (point, 1.5, new Point3d(1.5,0,1.5));
      pip.setFrameData (monkey, 0.5, new RigidTransform3d(1.5,0,1.5, 0,PI/2,0));
      pip.setFrameData (monkey, 1.0, new RigidTransform3d(0.0,0,3.0, 0,PI,0));
      pip.setFrameData (monkey, 1.5, new RigidTransform3d(-1.5,0,1.5, 0,3*PI/2,0));

    Here, we can omit data for times 0 and 2 (at the probe’s start and end) because the motion begins and ends at the components’ initial positions, for which data was automatically added when the probe was created. (Likewise, data for times 0 and 2 could be omitted in the original code if addData() was used instead of setData()).

  • Position probes handle angle wrapping correctly. In the position input probe, the y-axis rotation for the monkey goes from 3\pi/2 to 0 between t=1.5 and t=2. However, by generating “minimal distance” motions, interpolation proceeds as though the final angle was specified as 2\pi, allowing the circular motion to complete instead of backtracking (although in the display plot, there is an apparent discontinuity at t=1.75; see Figure 5.9, left). Likewise, in position output probes, as data is added, the rotation representation is chosen to be as near as possible to the previous, so that in the example the recorded y rotation does indeed arrive at 2\pi instead of 0 (Figure 5.9, right).

  • Since useTargetProps is set to false in the example, the velocity input probe is needed to set the component velocities; if this probe is made inactive (either in code or interactively in the timeline), then while the bodies will still move, no velocities will be set and the data in the velocity output probe will be uniformly 0.

    Alternatively, if useTargetProps is set to true, then the input probes will be bound to targetPosition, targetOrientation, and targetVelocity, and both positions and velocities will be set for the components if either input probe is active. Reflecting the description in Section 5.3.1.1:

    • If the position input probe is active and the velocity input probe is inactive, then the velocities will be determined by differentiating the target position inputs (albeit with a one-step time lag).

    • If the position input probe is inactive and the velocity input probe is active, then the positions will be determined by integrating the target velocity inputs. Note that in this case, when the probes terminate, the components will continue moving with their last specified velocity.

    • If both the position and velocity input probes are active, then both the target position and velocity information will be used to update the component positions and velocities.

    This provides a useful example of how target properties work.

5.4.10 Numeric monitor probes

In some cases, it may be useful for an application to deploy an output probe in which the data, instead of being collected from various component properties, is generated by a function within the probe itself. This ability is provided by a NumericMonitorProbe, which generates data using its generateData(vec,t,trel) method. This evaluates a vector-valued function of time at either the absolute time t or the probe-relative time trel and stores the result in the vector vec, whose size equals the vector size of the probe (as returned by getVsize()). The probe-relative time trel is determined by

\text{trel}=(\text{t}-\text{tstart})/\text{scale} (5.0)

where tstart and scale are the probe’s start time and scale factors as returned by getStartTime() and getScale().

As described further below, applications have several ways to control how a NumericMonitorProbe creates data:

The application is free to generate data in any desired way, and so in this sense a NumericMonitorProbe can be used similarly to a Monitor, with one of the main differences being that the data generated by a NumericMonitorProbe can be automatically displayed in the ArtiSynth GUI or written to a file.

The DataFunction interface declares an eval() method,

   void eval (VectorNd vec, double t, double trel)

that for NumericMonitorProbes evaluates a vector-valued function of time, where the arguments take the same role as for the monitor’s generateData() method. Applications can declare an appropriate DataFunction and set or query it within the probe using the methods

   void setDataFunction (DataFunction func);
   DataFunction getDataFunction();

The default implementation generateData() checks to see if a data function has been specified, and if so, uses that to generate the probe data. Otherwise, if the probe’s data function is null, the data is simply set to zero.

To create a NumericMonitorProbe using a supplied DataFunction, an application will create a generic probe instance, using one of its constructors such as

   NumericMonitorProbe (vsize, filePath, startTime, stopTime, interval);

and then define and instantiate a DataFunction and pass it to the probe using setDataFunction(). It is not necessary to supply a file name (i.e., filePath can be null), but if one is provided, then the probe’s data can be saved to that file.

A complete example of this is defined in

  artisynth.demos.tutorial.SinCosMonitorProbe

the listing for which is:

1 package artisynth.demos.tutorial;
2
3 import maspack.matrix.*;
4 import maspack.util.Clonable;
5
6 import artisynth.core.workspace.RootModel;
7 import artisynth.core.probes.NumericMonitorProbe;
8 import artisynth.core.probes.DataFunction;
9
10 /**
11  * Simple demo using a NumericMonitorProbe to generate sine and cosine waves.
12  */
13 public class SinCosMonitorProbe extends RootModel {
14
15    // Define the DataFunction that generates a sine and a cosine wave
16    class SinCosFunction implements DataFunction, Clonable {
17
18       public void eval (VectorNd vec, double t, double trel) {
19          // vec should have size == 2, one for each wave
20          vec.set (0, Math.sin (t));
21          vec.set (1, Math.cos (t));
22       }
23
24       public Object clone() throws CloneNotSupportedException {
25          return (SinCosFunction)super.clone();
26       }
27    }
28
29    public void build (String[] args) {
30
31       // Create a NumericMonitorProbe with size 2, file name "sinCos.dat", start
32       // time 0, stop time 10, and a sample interval of 0.01 seconds:
33       NumericMonitorProbe sinCosProbe =
34          new NumericMonitorProbe (/*vsize=*/2, "sinCos.dat", 0, 10, 0.01);
35
36       // then set the data function:
37       sinCosProbe.setDataFunction (new SinCosFunction());
38       addOutputProbe (sinCosProbe);
39    }
40 }

In this example, the DataFunction is implemented using the class SinCosFunction, which also implements Clonable and the associated clone() method. This means that the resulting probe will also be duplicatable within the GUI. Alternatively, one could implement SinCosFunction by extending DataFunctionBase, which implements Clonable by default. Probes containing DataFunctions which are not Clonable will not be duplicatable.

When the example is run, the resulting probe output is shown in the timeline image of Figure 5.10.

Figure 5.10: Output from a NumericMonitorProbe which generates sine and cosine waves.

As an alternative to supplying a DataFunction to a generic NumericMonitorProbe, an application can instead subclass NumericMonitorProbe and override either its generateData(vec,t,trel) or apply(t) methods. As an example of the former, one could create a subclass as follows:

  class SinCosProbe extends NumericMonitorProbe {
     public SinCosProbe (
        String filePath, double startTime, double stopTime, double interval) {
        super (2, filePath, startTime, stopTime, interval);
     }
     public void generateData (VectorNd vec, double t, double trel) {
        vec.set (0, Math.sin (t));
        vec.set (1, Math.cos (t));
     }
  }

Note that when subclassing, one must also create constructor(s) for that subclass. Also, NumericMonitorProbes which don’t have a DataFunction set are considered to be clonable by default, which means that the clone() method may also need to be overridden if cloning requires any special handling.

5.4.11 Numeric control probes

In other cases, it may be useful for an application to deploy an input probe which takes numeric data, and instead of using it to modify various component properties, instead calls an internal method to directly modify the simulation in any way desired. This ability is provided by a NumericControlProbe, which applies its numeric data using its applyData(vec,t,trel) method. This receives the numeric input data via the vector vec and uses it to modify the simulation for either the absolute time t or probe-relative time trel. The size of vec equals the vector size of the probe (as returned by getVsize()), and the probe-relative time trel is determined as described in Section 5.4.10.

A NumericControlProbe is the Controller equivalent of a NumericMonitorProbe, as described in Section 5.4.10. Applications have several ways to control how they apply their data:

The application is free to apply data in any desired way, and so in this sense a NumericControlProbe can be used similarly to a Controller, with one of the main differences being that the numeric data used can be automatically displayed in the ArtiSynth GUI or read from a file.

The DataFunction interface declares an eval() method,

   void eval (VectorNd vec, double t, double trel)

that for NumericControlProbes applies the numeric data, where the arguments take the same role as for the monitor’s applyData() method. Applications can declare an appropriate DataFunction and set or query it within the probe using the methods

   void setDataFunction (DataFunction func);
   DataFunction getDataFunction();

The default implementation applyData() checks to see if a data function has been specified, and if so, uses that to apply the probe data. Otherwise, if the probe’s data function is null, the data is simply ignored and the probe does nothing.

To create a NumericControlProbe using a supplied DataFunction, an application will create a generic probe instance, using one of its constructors such as

   NumericControlProbe (vsize, data, startTime, stopTime, timeStep);
   NumericControlProbe (filePath);

and then define and instantiate a DataFunction and pass it to the probe using setDataFunction(). The latter constructor creates the probe and reads in both the data and timing information from the specified file.

Figure 5.11: Screen shot of the SpinControlDemo, showing the numeric data in the timeline.

A complete example of this is defined in

  artisynth.demos.tutorial.SpinControlProbe

the listing for which is:

1 package artisynth.demos.tutorial;
2
3 import maspack.matrix.RigidTransform3d;
4 import maspack.matrix.VectorNd;
5 import maspack.util.Clonable;
6 import maspack.interpolation.Interpolation;
7
8 import artisynth.core.mechmodels.MechModel;
9 import artisynth.core.mechmodels.RigidBody;
10 import artisynth.core.mechmodels.Frame;
11 import artisynth.core.workspace.RootModel;
12 import artisynth.core.probes.NumericControlProbe;
13 import artisynth.core.probes.DataFunction;
14
15 /**
16  * Simple demo using a NumericControlProbe to spin a Frame about the z
17  * axis.
18  */
19 public class SpinControlProbe extends RootModel {
20
21    // Define the DataFunction that spins the body
22    class SpinFunction implements DataFunction, Clonable {
23
24       Frame myFrame;
25       RigidTransform3d myTFW0; // initial frame to world transform
26
27       SpinFunction (Frame frame) {
28          myFrame = frame;
29          myTFW0 = new RigidTransform3d (frame.getPose());
30       }
31
32       public void eval (VectorNd vec, double t, double trel) {
33          // vec should have size == 1, giving the current spin angle
34          double ang = Math.toRadians(vec.get(0));
35          RigidTransform3d TFW = new RigidTransform3d();
36          TFW.R.mulRpy (ang, 0, 0);
37          myFrame.setPose (TFW);
38       }
39
40       public Object clone() throws CloneNotSupportedException {
41          return super.clone();
42       }
43    }
44
45    public void build (String[] args) {
46
47       MechModel mech = new MechModel ("mech");
48       addModel (mech);
49
50       // Create a parametrically controlled rigid body to spin:
51       RigidBody body = RigidBody.createBox ("box", 1.0, 1.0, 0.5, 1000.0);
52       mech.addRigidBody (body);
53       body.setDynamic (false);
54
55       // Create a NumericControlProbe with size 1, initial spin data
56       // with time step 2.0, start time 0, and stop time 8.
57       NumericControlProbe spinProbe =
58          new NumericControlProbe (
59             /*vsize=*/1,
60             new double[] { 0.0, 90.0, 0.0, -90.0, 0.0 },
61             2.0, 0.0, 8.0);
62       // set cubic interpolation for a smoother result
63       spinProbe.setInterpolationOrder (Interpolation.Order.Cubic);
64       // then set the data function:
65       spinProbe.setDataFunction (new SpinFunction(body));
66       addInputProbe (spinProbe);
67    }
68 }

This example creates a simple box and then uses a NumericControlProbe to spin it about the z axis, using a DataFunction implementation called SpinFunction. A clone method is also implemented to ensure that the probe will be duplicatable in the GUI, as described in Section 5.4.10. A single channel of data is used to control the orientation angle of the box about z, as shown in Figure 5.11.

Alternatively, an application can subclass NumericControlProbe and override either its applyData(vec,t,trel) or apply(t) methods, as described for NumericMonitorProbes (Section 5.4.10).