A simulation in SOFA is described as a scene with an intrinsic generalized hierarchy. This scene is composed of nodes organized as a tree or as a Directed Acyclic Graph (DAG). The different simulated objects are described in separate nodes, and different representations of a same object can be done in different sub-nodes.
Let’s take some examples!
Fig. 1 – A graph with one single child node
Structure of a scene
The scene starts from a parent node, called the "Root" node. All other nodes (called child nodes) inherit from this main node. In the figure 1, a first child node "Liver" is defined and represents a first object. Usually, one node gathers the components associated with the same object (same degrees of freedom).
This design is highly modular, since components in the scene are independent of each other. One physical model (springs with SpringForceField) could be simply replaced by another one (triangular FEM with TriangleFEMForceField) by changing one component in the graph. In the same way, an explicit integration scheme (EulerSolver) could be replaced by an implicit one (EulerImplicitSolver) by modifying one XML line in the scene file. This high modularity of the framework is induced by the scenegraph-visitor approach described below.
Fig. 2 – A graph with one object and its two representations (mechanics and visual)
As illustrated in figure 2, nodes can be structured serially in the graph. Such a hierarchical graph allows for having several representation of a same object. In the example, the first child node "Liver" implements the mechanical behavior of the liver (hexahedral mesh), whereas the sub-node "Visual" describes a surface model (triangular mesh) of the liver.
Fig. 3 – A graph including two different objects computed in the same simulation
The figure 3 shows a simulation involving two different objects. One node can compute the mechanical behavior of a liver whereas the second simulates the electrical behavior of a heart. These two systems rely on two different degrees of freedom, i.e. different physical phenomenon. They must therefore be described in two distinct nodes. This feature shows the ability of SOFA to easily develop advanced and coupled models.
To build a simulation in SOFA, the scene graph can be written both using:
- XML files. Read the associated page about how to write a scene in XML.
- Python scripts. Read the associated page about how to write in Python.
This "Liver" node of Fig. 1 includes components (solvers, forcefield, mass) used to build the mechanical simulation of the liver. Each of these components contains attributes. For instance, a component of mass features an attribute for mass density; an iterative linear solver needs an attribute defining a maximum of iterations. These attributes are also called Data. These Data are containers providing a reflective API used for serialization in XML files and automatic creation of input/output widgets in the user interface.
Two Data instances can be connected one with another to keep their value synchronized. This is only possible if they both have the same type (
vector<double>). A mechanism of lazy evaluation is used to recursively flag Data that are not up-to-date. Then, the Data is recomputed (only if necessary). The network of interconnected Data objects defines a data dependency graph. In an XML file, one Data is connected to another when "@" is used:
All the scenes in SOFA must include an AnimationLoop. This class rules all the steps of the simulation and the system resolution, which succeed each other in a specific order. At each time step, the animation loop triggers each event (solving the matrix system, managing the constraints, detecting the collision, etc.) through a Visitor mechanism (see below). In a scene, if no animation loop is defined, a "DefaultAnimationLoop" is automatically created. Otherwise, in an XLM format, it can be written:
In an XML format, this would be written as follows:
<Node name="root" dt="0.01" gravity="0 -9.81 0"><DefaultAnimationLoop/></Node>
Several AnimationLoops are already available in SOFA:
- DefaultAnimationLoop: default one, created if no animation loop in the scene
- FreeAnimationLoop: for advanced constraints and collisions
- MultiStepAnimationLoop: uncouples the simulation and the visualization (N steps of simulation for one step of visualization)
- MultiTagAnimationLoop: animate the graph one tag after another, given a list of tags
During the different steps of the simulation (initialization, system assembly, solving, visualization), information needs to be recovered from all the graph nodes. An implicit mechanism based on Visitor enables this. You can find the abstract Visitor class in the SofaSimulation package.
For each of these steps, the operation implemented with the visitor can be described as:
- a graph traversal,
- abstract methods depending on the triggered action (ex: clearing an global vector, or accumulating forces),
- and vector identificators.
Visitors traverse the scene top-down and bottom-up, and call the corresponding virtual functions at each graph node traversal. Visitors are therefore used to trigger actions by calling the associated virtual functions (e.g. animating the simulation, accumulating forces). Algorithmic operations on the simulated objects are implemented by deriving the Visitor class and overloading its virtual functions topDown( ) and bottomUp( ). This approach hides the scene structure (parent, children) from the components, for more implementation flexibility and a better control of the execution model. Moreover, various parallelism strategies can be applied independently of the mechanical computations performed at each node. The data structure is actually extended from strict hierarchies to directed acyclic graphs to handle more general kinematic dependencies. The top-down node traversals are pruned unless all the parents of the current node have been traversed already, so that nodes with multiple parents are traversed only once all their parents have been traversed. The bottom-up traversals are made in the reverse order.
Example: accumulating the forces. Accumulating forces is used to compute all the forces (internal or external) applied on our object. The solver then triggers the associate Visitor and the action is propagated through the graph and calls the appropriate (bottom-up) methods at each force and mapping node. All components able to compute forces will accumulate their contributions. This information is finally gathered in the MechanicalObject and the solver will use this "force" vector to solve the mathematical system.
Fig. 4 – Traversing Visitors triggered for the accumulateForce() action
Here is the usual sequence diagram of a SOFA simulation.
Fig. 5 – Sequence diagram of a SOFA simulation
More about the Visitors can be found here.
Last modified: 26 June 2017