The methods associated with time advancement are:
initialize() is called to initialize the model for a particular time. It is called at the beginning of a simulation (with time t = 0), when the model is moved to a state and time defined by a WayPoint, and when a step is repeated during adaptive stepping (Section 2.5).
preadvance() is called to prepare the model for advancement from time t0 to t1. Often this method does nothing; it is supplied for situtations where the model needs to perform computation before the application of controllers or input probes (Section 2.2), such as evolving internal state in some way. The method can optionally return a StepAdjustment object to request a change in step size (Section 2.5).
advance() is called to advance the model from time t0 to t1. This is the main driver method for simulation, and typically involves solving an ordinary differential equation (ODE) associated with an underlying mechanical system, for which the model employs an internal physics solver. The method can optionally return a StepAdjustment object to request a change in step size (Section 2.5).
A very basic simulation might proceed as follows:
The rate of advancement (h in the above example) is limited by the model’s effective step size, which is nominally the maximum step size of the root model (Section 2.3). The model can override this by providing its own maximum step size (via getMaxStepSize()) that is less than that of the root model. Advance intervals can be smaller than the effective step size, if required by other time events imposed by WayPoints, rendering, or output probes. The effective step size may also be reduced when adaptive stepping is employed (Section 2.5).
A model can contain state, which is defined to be all information needed to deterministically advance it forward in time.
Models can contain state, as supported by the following methods:
If a model actually maintains state, then its hasState() method (inherited from ModelComponent) should return true, and createState() should create an appropriate object for saving and restoring the state using using getState() and setState().
The state of a model should contain all the internal information required to advance it forward in time. In particular, in the code fragment,
the model should have the exact same state and appearance after both the first and seconds calls to advance() (the call to initialize() is used to reset time-dependent quantities, such as time-dependent forces). For mechanical systems, the most prominent state quantities are the positions and velocities of the dynamic components, but there can be other quantities as well, such as contact state and viscoelastic state for FEM models.
As models are advanced, auxiliary agents can be employed to control the inputs and observe the outputs of the model. These include probes, controllers, and monitors.
A Probe is an agent that sets model input data, or records model output data, over a specific window of time. Probes that set input data are input probes (InputProbe), while those that record output data are output probes (OutputProbe). Examples of input data include muscle excitation signals or external forces. Output data often includes items such as velocities, postions, or internal forces.
Input probes can be used to perform a function analagous to the “loading curves” used in FEM analysis.
A probe contains several principal methods:
The task of applying input data or recording output data is performed in the apply() method, which is called periodically during the time window delimited by getStartTime() and getStopTime().
The methods isActive() and setActive() control whether or not the probe is active. Inactive probes will not have their apply() method called by the system. Probe activity is exported as the property active, and allows probes to be enabled or disabled at run time.
For input probes, the apply() method is called between the the preadvance() and advance() methods of the model it is associated with (Section 2.2.3). For output probes, apply() is called after the model’s advance() method, whenever the time advanced to equals an update time for the probe. Update times for an output probe are given by the start and stop times, plus any time that is an integer multiple of its effective update interval. The effective update interval is given by either the value returned by getUpdateInterval(), if it is not undefined (i.e., equal to -1), or the effective step size for the probe’s associated model (Section 2.1).
Note that the start time, stop time, and update interval can also be observed and controlled via the properties startTime, stopTime, and updateInterval.
Controllers and monitors are other agents that can be used to control or observe a simulation. Controllers are called immediately before a model’s advance() method (and after the preadvance() method and the application of any input probes) and are intended to compute control signals, while monitors are called immediately after the advance() method and are intended to record and process output data.
The primary method for both is
which performs the work of the agent. The times t0 and t1 are the same times passed to the model’s advance() method.
As with probes, controllers and monitors can be active or inactive, as determined the method isActive(). Controllers or monitors which are based on the default implementation classes ControllerBase or MonitorBase export also provide a setActive() method to control this setting, and export it as the property active. This allows controller and monitor activity to be controlled at run time.
As indicated above, model agents are typically associated with a specific model within the ArtiSynth structure, and are then applied either before or after the advance() method of that model. Agents which are not explicitly associated with a model are implicitly associated with the root model Section 2.3.
Methods to obtain and set the associated model include:
setModel() sets the model directly, while setModelFromComponent() takes a subcomponet of a model and searches up the hierarchy to find the model itself. For example, when connecting a component property to a numeric probe, the system automatically determines the probe’s model by calling setModelFromComponent() on the component hosting the property.
Like models, agents can also have state, and therefore implement the same methods hasState(), createState(), getState(), and setState() described for models in Section 2.1.
The base classes for probes, controllers, and monitors define stateless version of these methods (i.e., hasState() returns false, and getState() and setState() do nothing), so that agents which actually do contain state must override these methods.
In the context of agents, state can be thought of as the internal information that is required so that the agent’s actions and effect on its associated model are always identical for a specific time and state. A common example of state in the context of a controller or monitor might be the time history used to filter a signal.
Note that the requirement “effect on its associated model” means that state is also needed for input probes to handle situations when the simulation is moved to a time and state defined by a WayPoint. That’s because probes are applied to a model only over a specific time window, and so when the simulation is reset to a time outside that window, it is usually necessary to reset the model attributes controlled by the probe to their original values at that time. As a simple example, assume that the apply() method of an input probe sets a value x in a model to 10 over the time window [2,4], and that before that time, x has a value of 0. Now if time is advanced to t = 3, x will be set to 10, and if time is then reset to t = 1 (before the probe’s window), x will need to be restored to 0. This must be done by restoring the probe’s state, since the apply() method will not be called at t = 1.
If a controller has state, then it is important to implement getState() and setState() to ensure proper behavior with respect to both adaptive stepping Section 2.5 and WayPoints. If a probe or monitor has state, implementation of getState() and setState() is necessary to ensure its proper behavior with respect to WayPoints, but not adaptive stepping, since probe and monitor state is not changed during adaptive stepping).
All the models simulated by ArtiSynth at any given time are collected together within a root model (RootModel), which is the top-level model component in the hierarchy. Every system that is simulated by ArtiSynth is associated with a specific instance of a RootModel, and is typically created in code by subclassing RootModel and then creating and assembling the necessary components in the subclass’s constructor. Alternately, a RootModel can be loaded from a file, in which case a generic RootModel is created and then populated with structures determined from the file.
A RootModel contains a list of all the system’s models, probes, controllers, and monitors. Methods to add or remove these include:
The root model is the top-level object seen by the ArtiSynth scheduler when running a simulation. As a simulation proceeds, the scheduler determines the next time to advance to and then calls the root model’s advance() method:
In turn, the advance() method then individually advances all the models contained in the root model, as described below. The next advance time t1, computed by getNextAdvanceTime(), is determined mainly by the root model’s maximum step size (returned by RootModel.getMaxStepSize()), along with other events such as WayPoint locations and the render update rate.
The root model’s maximum step size is therefore the primary simulation step size. It can be set using the method RootModel.setMaxStepSize(), and is exposed as the RootModel property maxStepSize. It is also coupled to the "step" display in the ArtiSynth GUI, and can also be obtained or set using Main.getMaxStep() or Main.setMaxStep(). When a RootModel is created, if its maximum step size is not set explicitly (either in the constructor or in a file specification), then it is set to a default value which is either 0.01, or the value specified by the -maxStep command line argument.
A RootModel advances each of its models in sequence, using a procedure called advanceModel(). Because a model may have a maximum step size (as returned by getMaxStepSize()) that is less than that of the root model, or some of its output probes may have events that preceed t1, each model is advanced using a series of sub-advances, with advanceModel() taking the form of a loop:
Each time through the loop, getNextAdvanceTime() determines the next appropriate sub-advance time tb, and then calls the model’s advance() method, surrounded by the application of any probes, controllers or monitors that are associated with it. The preadvance() method is called first, followed by input probes and controllers. Then advance() is called, monitors are applied, and output probes are applied if their next update time is equal to tb.
The apply time for input probes is not the time ta at the beginning of the time step, but rather the time tb corresponding to its end. This might seem counterintuitive, but makes sense when one considers that input probes are generally used to provide targets for the advance process, and we typically want targets specified for the end of the time step. If the target is a force, then this is also consistent with implicit integration methods (used most commonly by ArtiSynth) which solve for the system forces at the end of the time step.
The RootModel advance() method in turn calls advanceModel() for all models, surrounded by the application of any probes, controllers and monitors which do not have specific models of their own and are therefore considered to be “owned” by the root model. Because some of these output probes may have event times that preceed the desired advance time of t1, this process is also done in a loop:
Note that at present, the preadvance() method for a root model does nothing and is not called.
It is possible for models to request adaptive time stepping, which may be necessary if the model determines that a requested time step is too large for stable simulation. The model can indicate this by having either its preadvance() or advance() methods return a StepAdjustment object, which contains a recommended scaling for the step size via its scaling attribute. Then, if adaptive stepping is enabled in the root model, it will reduce the effective time step for the model and redo the advance. If preadvance() or advance() return null, then it is assumed that the step size should remain unchanged (which is equivalent to returning a StepAdjustment with a scaling of 1).
Adaptive time stepping can be enabled or disabled using the adaptiveStepping property of RootModel, or by using the RootModel methods
When adaptive stepping is enabled, the inner loop of the advanceModel() procedure described above is modified to the following:
where GET_SCALING() returns 1 if preadvance() or advance() returns null, or the value of StepAdjustment.getScaling() otherwise.
At the beginning of the loop, the model’s state is saved in case a retry is necessary. Then if preadvance or advance() recommend scaling the step by s < 1, the advance time is reduced (by reducing the model’s effective step size), the state is restored to what is was at the beginning of the step, and the step is retried. After a step succeeds, the root model will incrementally try to increase the effective step size, up to its nominal value.
The exact interpretation of the scaling value s is as follows:
Advance unsuccessful; no recommendation as to how much to reduce the next step.
Advance unsuccessful; recommend trying to reduce the step by s*(tb-ta).
Advance successful, no recommendation as to how much to increase the step by.
Advance successful; recommend trying to increase the step by s*(tb-ta).
When requesting a step size reduction, models may provide a string message indicating the reason via the message attribute of StepAdjustment. The system will abort if the effective step size falls below the minimum value specified by the RootModel property getMinStepSize. At present, recommended increases in step size are ignored and treating simply as s = 1.
Models are of course free to implement adaptive stepping internally, in a way that is invisible to the root model. However, the saving and restoring of state, along with the algorithms for step size adjustment, are sufficiently intricate that it is generally convenient to use the adaptive stepping provided by RootModel.