September, 2021
Artisynth is a mechanical modeling system that allows users to combine finite element method (FEM) components with multibody systems, constraints, and collision handling. It is implemented in Java, and provides a graphical interface for interactive model editing and simulation control.
At present, most Artisynth models are created with Java code, using the Artisynth API. The programmatic aspects of Artisynth are the focus of this manual.
Artsynth models are created from a hierarchy of components. Each component is an instance of ModelComponent, which contains a number of methods used to maintain the component hierarchy. These include methods for naming and numbering components:
Each component can be assigned a name, which can be any sequence of characters that does not begin with a digit, does not contain the characters ’.’, ’/’, ’:’, ’*’, or ’?’, and does not equal the string "null". For components which are not assigned a name, getName() will return null.
Artisynth may also be configured so that components names must be unique for all components which are children of the same parent.
Even if a component does not have a name, it has a number, which identifies it with respect to its parent. Numbers are assigned automatically when a component is added to its parent, and persist unchanged until the component is removed from its parent. This persistence of numbers is important to ensure that components keep the same path name as long as they are connected to the hierarchy.
Names and/or numbers can be used to form a path name for each component that identifies its place in the hierarchy. If a component does not have a name, its number is used in the path instead. Some example path names look like:
ModelComponent contains a number of other methods for navigating and maintaining hierarchy structure:
getParent() returns the component’s parent, which is a CompositeComponent (Section 1.3). Conversely, if getParent() returns null, the component is not attached to any parent and is not connected to the hierarchy unless it is the top-level RootModel component (Section 2.3).
When a model component (or one of its ancestors) is added or removed from the component hierarchy, its methods connectToHierarchy(hcomp) or disconnectFromHierarchy(hcomp) are called, with hcomp indicating the hierarchy component to which the component (or ancestor) was attached or detached. When either of these methods are called, the component will still be connected to the hierarchy, and so hierarchy-dependent initialization or deinitialization can be performed, like setting (or removing) back pointers to references, etc.:
The methods getHardReferences() and getSoftReferences() are described in Section 1.2.
It is also necessary to notify components in the hierarchy when there are changes in structure or component properties, so that the necessary adjustments can be made, including the clearing of cached data. Notification is done using the method notifyParentOfChange(), which propagates an appropriate change event up the hierarchy. It will typically do this by calling the componentChanged() method of the parent (see Section 1.3).
The method hasState() should return true if the component contains state information. This is always true if the component contains dynamic state information such as positions or velocities, but components may sometimes contain additional state information (such as contact state). Structural changes involving the addtion or removal of state-bearing components should be announced to the system by calling notifyParentOfChange() with a ComponentChangeEvent for which stateChanged() returns true.
A ModelComponent also maintains a number of flags:
All of these flags are false by default.
indicates whether or not a component is selected. Components can be selected using various selection mechanisms in the Artisynth interface, such as the navigation panel or the viewer. When selected, its isSelected() method will return true.
indicates, if true, that a component should not be removed from its parent. It is used to fix required child components of composite components that contain otherwise removable children (Section 1.4).
is available for use by graph-processing algorithms involving the component hierarchy, to indicate when a component has been visited or otherwise processed. This flag should be used with care to avoid side effects.
Important:
setSelected() should only be used by the SelectionManager, and should not be called by applications. Programmatic component selection should be performed by calling the addSelected() or removeSelected() methods of the SelectionManager.
Finally, all ModelComponents implement the interface Scannable, which provides methods for writing and scanning to and from persistent storage. Details are given in Section 3.
For convenience, ModelComponentBase provides a base implementations of all the ModelComponent methods. Most ArtiSynth components inherit from ModelComponentBase.
Model components can reference additional components outside of the parent-child relationships of the hierarchy. For example, a point-to-point spring contains two references to its end-points, which are themselves model components. As another example, components which implement the ExcitationComponent interface can maintain references to other ExcitationComponents to use as excitation sources. References can be considered to be either hard or soft. A hard reference is one which the component requires in order for its continued existence to be meaningful. The end-point references for a point-to-point spring are usually hard. A soft reference is one that the component can do without, such as the excitation source inputs mentioned above. The methods getHardReferences() and getSoftReferences() are used to report all hard and soft references held by a component.
Note:
getHardReferences() and getSoftReferences() should report only references held by the component itself, and not those held by any of its descendents.
The distinction between hard and soft references is used by the system when components in the hierarchy are deleted. A component that holds a hard reference to a deleted component is generally deleted as well. However, when only soft references are deleted, then the updateReferences() method of the referring component is called to update the component’s internal structures. updateReferences() should also store information about the update, to allow the update to be undone in case the method is called later with its undo argument set to true. A typical implementation pattern for updateReferences() is shown by the following example, in which maspack.util.ListRemove is used to remove selected items from a list of soft references, and store information needed to undo this later:
When updating, the method uses ComponentUtils.areConnected() to determine which soft references have been deleted from the hierarchy. A ListRemove object is used to assemble the remove requests and then perform the remove all at once and store information about what was removed for possible later undoing. The remove object is appended to the end of undoInfo. If no undo was needed, then NULL_OBJ is stored instead because Deque objects don’t accept null arguments. Undo information is stored at the end of the deque and removed from the front. This allows multiple updates, including that for the super class, to be performed in sequence.
CompositeComponent is a subinterface of ModelComponent which can contain children. Its main methods include:
Most of the above methods are self-explanatory. It is important to note the difference between indices and numbers when identifying child components. An index is the location of the child within the list of children, starting from 0, and can change as children are added or removed. A number, on the other hand, as described above, is assigned automatically to a child when it is added to the parent and persists as long as it remains.
The componentChanged() method is called to indicate structure or property changes. Appropriate actions may include clearing cached data, and propogating the event further up the hierarchy (using notifyParentOfChange()).
MutableCompositeComponent is a subinterface of CompositeComponent which allows child components to be added and removed by an ArtiSynth application. It is a generic class parameterized by a class type C which must be an extension of ModelComponent. Its definition is:
A default implementation of CompositeComponent is provided by CompositeComponentBase. It is a non-generic class that provides a base for composite components whose composition is created at construction time and is not intended to change during the running of an ArtiSynth application.
ComponentList is a much more flexible class which implements MutableCompositeComponent and provides for collections of components whose composition may be built and changed by an application. ComponentList is used widely to store the many lists of components that comprise a working ArtiSynth model.
[]
In particular, MechModel and FemModel3d, the primary ArtiSynth classes for implementing mechanical and finite element models, are themselves subclasses of ComponentList which contain lists of mechanical components (such as particles, rigid bodies, and force effectors for MechModel, and nodes, elements, and geometry for FemModel3d).
In the case of MechModel, applications can create and add their own component lists to the model itself:
By default, child components that belong to a MutableCompositeComponent (which includes ComponentList) may be selected by the ArtiSynth application for deletion. This may be undesirable, particularly if internal structures depend on certain child components. Components that should not be removed from their parents should have their fixed flag set to true in the composite component constructor, either by calling setFixed(), or by adding the component using the addFixed() method of ComponentList.
The class ComponentListImpl is available as an internal implementation class for constructing instances of either CompositeComponent or MutableCompositeComponent. It provides most of the implementation methods needed for a mutable component list, which can be exposed in the client class using delegate methods. Components implementing only CompositeComponent may choose to expose only some of these methods. For details, one should consult the source code for CompositeComponentBase or ComponentList.
A Model is a specific ModelComponent that can contain state and be advanced forward in time.
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.
The most common types of probes used in ArtiSynth are NumericInputProbe and NumericOutputProbe, which are used to connect model properties to streams of numeric data which can be edited and observed on the system’s timeline. Numeric probe data can also be saved to (or loaded from) external files.
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.
At present, NumericInputProbe defines getState() and setState() to save and restore any model property values that it controls. No state is defined for NumericOutputProbe.
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.
ArtiSynth model components have the ability to save and restore themselves from persistent storage. They do this by implementing the write() and scan() methods of maspack.util.Scannable, and the postscan() method of ModelComponent:
The operation and implementation of these methods will now be described in detail. A summary of the key points is given in Section 3.5.
The write() method writes information about the component to a PrintWriter, using NumberFormat to format floating point numbers where appropriate. The ref argument is used to provide additional context information for generating the output, and is specifically used to generate path names for other components that are referenced by the component being written (Section 3.1.1).
In general, each component writes out its attributes as a list of name/value pairs, each of the form
with the list itself enclosed between square brackets ’[ ]’ which serve as begin and end delimiters. The value associated with each attribute name may itself be a quantity (such as a vector, matrix, or another component) delimited by square brackets. For example, the output for a Particle component may look like this:
The output begins with an openning square bracket, followed by four attribute/value pairs and a closing square bracket. The position attribute is a 3-vector also enclosed between square brackets.
Within the write() method, the above output could be produced with code like this:
Output indentation can be controlled by using an IndentingPrintWriter, and IndentingPrintWriter.addIndentation() will increase (or decrease) output indentation if pw is an instance of IndentingPrintWriter.
In practice, components do not generally need to provide explicit code to write out all their attribute values. In particular, any information that is associated with a Property (see the Property section of the Maspack Reference Manual) can be written out automatically using a code fragment of the form:
In addition, any attribute information contained in a component’s superclass will usually be written by that superclass. The default write() definition for a model component is usually looks something like this:
The write() and dowrite() methods take care of writting the square brackets and setting up the initial indentation. Then a call to writeItems() prints out all necessary property values. The ancestor argument obtained from ref will be discussed in Section 3.1.1.
If all a component’s attribute information is associated with property values, then it is usually not necessary to provide any component-specific code for writing the component: the default implementations of write() and writeItems() will handle it. If a component does have attribute information that is not associated with a property, then it is usually sufficient to handle this by overriding writeItems(). For example, if a component has a non-property “centroid” attribute, it can be written by an override of writeItems() constructed like this:
ArtiSynth components often contain references to other components that are not part of their ancestor hierarchy. For example, a two-point spring will contain references to its two end-point particles, and a FemElement3d will contain references to its nodes. The set of all references refered to by a component is returned by the combination of the component’s getHardReferences() getSoftReferences() methods.
Because they generally reside outside a component’s immediate ancestor hierarchy, information about each reference’s location needs to be explicitly written and scanned as part of writing and scanning a component. The location information is stored using the component’s path with respect to some known ancestor. This ancestor is passed to the component’s write() method through the ref argument, and is cast explicitly to CompositeComponent and passed to writeItems() as the ancestor argument. A component can then use ComponentUtils.getWritePathName() to obtain the path name of each reference with respect to the ancestor, and write this to the output.
As an example, here is a possible implementation of writeItems() for a two-point spring:
This will produce an output like this,
where models/points/n gives the path name of the n-th point with respect to the ancestor. Alternatively, if a component has a variable number of references, they can be written out as a list between square brackets:
The above code will produce output like this:
The ancestor used for reading and writing references will always be a common ancestor of both the references and the refering component. This may sometimes be the root model (i.e., the top of the hierarchy), but more typically it will be the first common ancestor for which hierarchyContainsReferences() returns true (implying that all references are contained within the ancestors’s descendants). This allows paths to be written more compactly. Most Model components presently enforce the hierarchyContainsReferences() condition.
If a component is a CompositeComponent, then it also needs to write out its child components along with its attribute information. This can be done by recursively calling the children’s write() methods.
If the child component configuration is fixed (i.e., the component does not implement MutableCompositeComponent) and the children are created in the composite’s constructor, then attribute names can be used to delimit each child. For example, suppose a component contains two children: a list of particles and a list of springs. This component could then be written using a code construction such as:
If the composite component has been implemented internally using an instance of ComponentListImpl (Section 1.4), then the above can be written using the latter’s writeComponentsByName() method, which writes each child, using its component name as an attribute name:
One the other hand, composite components which are instances of MutableCompositeComponent may not have predetermined component arrangements and so these components cannot be identified by an attribute name. Instead, the components must simply be printed out in sequence. For example, the writeItems() method for a list of particles could be programmed like this:
This would produce output such as:
However, when scanning a MutableCompositeComponent, the scan() method (discussed below) needs to create and scan new child components as it encounters them in the input. It is therefore necessary for scan() to know what class of component to create. Typically, the composite component has a default component type; for example, the default type for ComponentList<Particle> is Particle. However, in some cases the components may be subclasses of the default type, or the default type may be an interface or abstract class and hence not instantiable. In such instances, the output needs to be augmented with class type information, which is placed before the openning ’[’ of the component output. The details of how to do this are beyond the scope of this document. However, if the composite component has been implemented internally using an instance of ComponentListImpl, then one can use the latter’s writeComponents() method to automatically write out all the components, along with the necessary class information:
The result may look something like this:
In this example, the second and fourth particles have class types that differ from the default and so class information for each is prepended to the output.
The scan() method reads the component in from a token stream provided by a ReaderTokenizer. This translates the input into a stream of tokens, including words, numbers, and special token characters (such as ’[’, ’]’, and ’=’), which are then used to parse the input. Authors implementing scanning code should have some familiarity with ReaderTokenizer. A description is beyond the scope of this document but good documentation is available in the ReaderTokenizer class header.
The main code inside the default scan() method for a model component looks roughly like this:
The method looks for and scans the initial ’[’ character (and will throw an IOException if this is not found). It then reads other tokens (using nextToken()) until the terminating ’]’ character is found. After each token is inspected, it is pushed back into the token stream using pushBack() and scanItem() is called to try and read an individual attribute or subcomponent from the input. If scanItem() cannot match the input to any attributes or child components it returns false.
Note: ReaderTokenizer allows one token of look-ahead, so that any read token can be pushed back once. In particular, in the following sequence, t1 and t2 should be the same:
t1 = rtok.nextToken(); rtok.pushBack(); t2 = rtok.nextToken();
The default implementation of scanItem() provides code for reading property values and looks something like this:
The method begins by getting the next token and then calling ScanWriteUtils.scanProperty() to see if the token is a word matching the name of one of the component’s properties. If so, then scanProperty() scans and sets the property value and returns true, and scanItem() itself returns true. Otherwise, scanItem() returns false indicating that it was unable to find a match for the input. The tokens argument is used to store information whose processing must be deferred until the post-scan step, as discussed in Section 3.2.1.
Typically, component implementations will not need to override scan() unless the scanning procedure calls for pre- or post-processing, as in:
Note that if a class makes use of the post-scan step (Section 3.2.1), then it may be necessary to do the post-processing in an override of postscan() instead.
Component implementations often will need to override scanItem() to scan additional attribute information. For example:
First, the method gets the next token. Then it checks if its type (ttype) corresponds to a WORD token and if the word’s string value (sval) equals the attribute name attributeXXX. If so, then it scans the ’=’ character following attribute name, scans whatever information is associated with the attribute, and returns true. Otherwise, if no expected attribute name is matched, the current token is pushed back, and the superclass method is called to see if it can match the current input.
Most implementations of ModelComponent provide the convenience method scanAttributeName(rtok, name) which allows the code fragment
to be replaced with
Employing this in a larger example, we have
Here, if any of the attribute names name, position, mass, or dynamic are matched, then the corresponding string, vector, numeric, or boolean attribute values are scanned using scanQuotedString(), the vector’s own scan() method, scanNumber(), or scanBoolean(). Each of these will throw an IOException if the input token sequence does not match what is expected.
When scanning a component that contains references, the path for each reference is used to locate the referenced component within the component hierarchy. However, this poses a problem: because components are created only as the component hierarchy is recursively scanned, it is possible that some references may not yet exist at the time when the component is scanned. For example, if the points referenced by a two-point spring belong to part of the hierarchy further "to the right" of the spring components, then when the spring is scanned the points won’t yet exist and the scanning method will be unable to find them.
The solution to this problem is to employ a two-step scanning process in which the initial scan is followed by a secondary "post-scan" which can be used to resolve references. Each reference path found during the initial scan is saved for later use in the post-scan step, by which time all components are guaranteed to have been created. Reference information, along with any other information needed for the post-scan step, is saved in a queue of ScanTokens supplied to the scan() method through the ref argument. Several different types of ScanTokens allow different types of information to be stored: StringTokens are use to store attribute names and reference paths; ObjectTokens are use to store object pointers; and special marker tokens, ScanToken.BEGIN and ScanToken.END, can be used as delimiters.
At a minimum, scanning each component causes BEGIN and END tokens to be added to the token queue, with additional tokens added in between as necessary. Revisiting the basic scan() method code shown at the top of Section 3.2, we show the additional code that is needed to handle this:
The token queue itself, called tokens, is obtained from the ref argument via an explicit cast. BEGIN and END tokens are added at the beginning and end of the scan. In between, the token queue is passed to scanItem(), which adds addtional tokens when necessary.
Within scanItem(), tokens are added to provide whatever information is needed for the post-scan step. This information is often provided in the form of two or more tokens comprising an attribute name/value pair, so that the post-scan step is not sensitive to input ordering. In this sense, the information stored in the token queue will reflect the same structure as the tokens in the original input.
Consider the first example in 3.1.1 where the reference information for a two-point spring was output as:
To process this inside scanItem(), we check for the attribute names point0 and point1 and if either is found, we store both the attribute name and the reference path in the token queue using StringTokens. For point0, the corresponding code looks like
Most implementations of ModelComponent provide the convenience method scanAndStoreReference (rtok, name, tokens) which allows this to be compressed into
One may also use ScanWriteUtils.scanAndStoreReference() for the same purpose. The scanItem() method for a two-point spring can then be written as:
Alternatively, if we have an attribute followed by a list of references enclosed in square brackets, such as
then we want to store a sequence of tokens consisting of the attribute name, a BEGIN token, the reference paths, and an END token:
That can be done by a code sequence that looks like
and which is available in most ModelComponent implementations via the convenience method scanAndStoreReferences():
One may also use ScanWriteUtils.scanAndStoreReferences() for the same purpose.
In addition to their attributes, composite components need to scan in their child components. This can be done by recursively calling the children’s scan() methods.
If the child component configuration is fixed (i.e., the component does not implement MutableCompositeComponent), and the children are created in the composite’s constructor and written out using attribute names as delimiters, then these attributes names can be used to drive the scanning. The example composite from Section 3.1.2, comprising a list of particles and a list of springs, could be scanned in using code such as:
Here, an ObjectToken() identifying each scanned componet is stored on the token queue for later use in the post-scan step. If the composite component has been implemented internally using an instance of ComponentListImpl (Section 1.4), then the above can be written more succinctly using the latter’s scanAndStoreComponentByName() method:
On the other hand, composite components which are instances of MutableCompositeComponent are written out in sequence, without using attribute names but with possible prefixed information giving information about the component’s class. When scanning in these children, scanItem() must determine the class for the child, create an instance of the child, and then scan it in. The code required for these steps is beyond the scope of this document. However, if the mutable composite has been implemented internally using an instance of ComponentListImpl, then one can use the latter’s methods scanBegin(), scanAndStoreComponent(), and scanEnd() to handle the scanning.
First, scanBegin() is called in an override of scan():
scanAndStoreComponent() can then be called in scanItem() to handle scanning of individual components:
The method checks to see if the input contains child component information, and if so, scans the information, creates an instance of the component if necessary, stores a copy of the component in the token queue for use in post-scanning, and returns true. Note that in this case one should call scanAndStoreComponent() after the super method to avoid confusing attribute names with class information.
scanAndStoreComponent() will not create a new component if a fixed component (Section 1.4) of the appropriate class already exists at the current list position. Instead, the existing component will be scanned “in place”.
Finally, scanEnd() is called in an override of postscan() method (discussed below):
Once the token queue has been built by the scan() methods, it is processed in the post-scan step. This is done by each component using a postscan() method that takes as arguments the token queue and the ancestor with respect to which reference paths should be evaluated. The default postscan() method for most components looks something like this:
postscanBeginToken() gets the next token on the queue, checks that it is a BEGIN token, and throws an exception if this is not the case. Then the method simply calls postScanItem(), which does the actual token handling work, until a terminating END token is found.
As is the case with scan(), subclasses typically do not need to override postscan(). The exception to this is when post-processing is required after the scan process:
However, any component which adds tokens in its scanItem() method will need to process those tokens in an override of postscanItem(). Tokens can be removed from the queue using the queue’s poll() method, and can be examined (without removing them) using the queue’s peek() method. More usefully, the utility class ScanWriteUtils provides a number of methods for token processing, including:
ModelComponentBase also makes convenience wrappers for these directly available within the class. postscanAttributeName() checks if the next token in the queue is a StringToken matching name, and if it is, consumes that token and returns true. postscanReference() checks that the next token is a StringToken containing a path reference, finds the component referenced by that path relative to ancestor, checks that it is an instance of clazz, and returns it. postscanReferences() obtains a set of component references described by a sequence of StringTokens located between BEGIN and END tokens, and returns the referenced components in an array. These methods will throw an IOException if they encounter unexpected tokens or if referenced components cannot be found.
Employing these methods to handle the point0, point1 reference example above, we obtain:
Similarly, the points reference example can be handled as:
Finally, for composite components, it is necessary to call postscan() for each of their children. For composites implemented using ComponentListImpl, this can be done by calling the latter’s postscanComponent() method. For both CompositeComponent and MutableCompositeComponent implementations, the corresponding code looks like this:
postscanComponent() checks to see if the next token is an ObjectToken containing a ModelComponent, and if it is, it removes that token, calls the component’s postscan() method, and returns true.
As described in 3.2, base implementations of scanItem() automatically read in and set property values for the component. However, in a few cases it may be necessary to defer setting the property value until the post-scan step, either because it depends on component references, or requires the component structure to be fully realized. A current example of this is the surfaceRendering property for FemModel.
Deferring the settting of property values until the post-scan step can be done by saving the scanned property values in the token queue, and then actually setting the properties during the post-scan. Two convenience methods, ScanWriteUtils.scanAndStorePropertyValue() and ScanWriteUtils.postscanPropertyValue() allow this to be done fairly easily:
Note that it is often not necessary to provide a component-specific implementation of postscanItem() since the defaullt implementation for most components already contains a call to ScanWriteUtils.postscanPropertyValues().
If a property value depends on references, it is also important to ensure that reference information is written out before the property information, so that in the post-scan step, it will be set before the property values. In such cases, that means that writeItems() should be structured as follows:
The two-step scanning process means that for the top-level invocation of scan, the application needs to create a token queue, call scan() with this queue as the ref argument, and then call postscan():
For convenience, the above code fragment is encapsulated into the method ScanWriteUtils.scanfull().
If we are scanning an entire hierarchy from scratch, then comp will be the root component of the hierarchy and ancestor will equal comp. Otherwise, if we are scanning a new sub-hierarchy, then comp will be the root of the sub-hierarchy and ancestor may be some component higher in the existing hierarchy.
Because Artisynth components are responsible for writing and scanning themselves, there is no mandatory file structure imposed per-se. However, there is a structure that should be adhered to whenever possible. Expressed loosely as a production grammar, with ’*’ expressing repetition of zero or more times, this is:
Here, NAME and WORD are identifiers that consist of alphanumerics, ’$’, or ’_’, and do not begin with a digit. CLASSINFO is the classname for a component, or an alias that can be mapped to a classname using ClassAliases.resolveClass().
Within an ArtiSynth file, ’#’ is a comment character, causing all remaining characters on the line to be discarded.
When tokens are saved for the post-scan step, they should arranged in a structure similiar to that used for the file itself:
Here, BEGIN and END are ScanToken.BEGIN and ScanToken.END, NAME is a StringToken with an attribute name as a value, COMPONENT is a ObjectToken with a reference to the object as a value, and STRING and OBJECT are StringToken and ObjectToken, respectively.
Debugging write and scan methods is generally not too difficult because of the ascii nature of the data files. A good first test is to write components out, read them back in, and then write them out a second time and make sure that the second output equals the first. Scan methods will generally throw IOExceptions when unexpected input is encountered, and these usually provide the offending line number.
Problems that occur in post-scan can be slightly harder to solve because the token queue is not normally written out in any place where it can be inspected. To help with this, once can use ScanWriteUtils.setTokenPrinting() to enable the token queue produced by ScanWriteUtils.scanfull() to be printed to the standard output. It is also possible to print a token queue directly using ScanWriteUtils.printTokens().
The main points concerning component writing and scanning are as follows:
Writing and scanning are done using the component’s write(), scan(), postscan() methods. These methods usually employ writeItems(), scanItem(), and postscanItem() to handle the writing and scanning of individual attributes and child components.
Where possible, the structure described in Section 3.3 should be adhered to.
Scanning is a two-step process, involving a scan step and a post-scan step. This is to accomodate the fact that some aspects of scanning (most importantly the evaluation of references) cannot be done until the entire component hierarchy has been constructed. Information needed for the post-scan step (such as reference path names) should be stored in the token queue passed to scan() and scanItems().
It is usually only necessary for a component implementation to override writeItems(), scanItem(), and postscanItem(). Property values are usually written and scanned automatically by the base implementations of writeItems() and scanItem(). If a component does not contain references or non-property attributes, it may not be necessary for the implementation to override any methods at all.
Composite components need to call write(), scan(), and postscan() for their child components. Composites implemented using ComponentListImpl can do this using methods supplied by that class, such as writeComponents(), scanAndStoreComponent(), and postscanComponent().
A complete scan operation involves creating a token queue and then calling both scan() and postscan() for the top-level component. This can be done using the convenience method ScanWriteUtils.scanfull().
The utility class ScanWriteUtils contains a large number of methods that facilitate writing and scanning.