Sim Body Advanced Programming Guide
Sim Body Advanced Programming Guide
Simbody Advanced
Programming Guide
Release 3.7
December, 2019
website: [Link]
Copyright and Permission Notice
Portions copyright (c) 2008-14 Stanford University, Peter Eastman, and Michael Sherman.
Permission is hereby granted, free of charge, to any person obtaining a copy of this document (the "Document"),
to deal in the Document without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Document, and to permit persons to whom the
Document is furnished to do so, subject to the following conditions:
This copyright and permission notice shall be included in all copies or substantial portions of the Document.
THE DOCUMENT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS,
CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE DOCUMENT OR THE USE OR OTHER DEALINGS IN THE DOCUMENT.
Acknowledgments
SimTK software and all related activities are funded by the Simbios National Center for
Biomedical Computing through the National Institutes of Health Roadmap for Medical
Research, Grant U54 GM072970. Information on the National Centers can be found at
[Link]
Table of Contents
1 INTRODUCTION ......................................................................................... 1
1.1 Extending Simbody ....................................................................................................................1
1. Define state variables, which can be categorized into generalized coordinates (q),
generalized speeds (u), auxiliary variables (z), and discrete variables (d).
You will learn how to write new Subsystems that may do any or all of these. This is the most
general way that you can extend Simbody Systems with custom code. It often isn’t the most
convenient way, though. If all you want is to define one new constraint type, you shouldn’t
need to write an entire Subsystem. Simbody provides simpler mechanisms for extending a
System in common ways. In fact, you have already seen one of them: rather than writing a
new Subsystem to define an event handler, you simply write an EventHandler or
1
Realization Revisited 2
EventReporter object, and then add it to the System. You also can write custom subclasses of
Force, Constraint, and MobilizedBody. We will see examples of all of these.
Stages can also be thought about in another way. Every state variable is associated with a
particular cache stage:
Variable Stage
t Time
q Position
u Velocity
z Dynamics
d any
(A discrete state variable may be associated with any stage except Empty or Topology. When
a Subsystem defines a discrete variable, it specifies what stage to associate it with.)
When a State is being realized to a particular stage, the values calculated and stored in the
cache can only be those that depend on state variables for that stage or earlier stages. They
may not depend in any way on state variables associated with later stages.
Why is this? Because whenever a state variable is modified, the cache is automatically
reverted back to the stage immediately before the stage associated with that variable. If you
modify a generalized coordinate q, the cache is reverted back to Time stage. If you modify a
generalized speed u, the cache is reverted back to Position stage. This ensures that any
information in the cache which might depend on that variable is discarded.
3 The First Four Computation Stages
Suppose that a Subsystem failed to obey this rule. Suppose that, while realizing a State to
Position stage, it made use of the generalized speeds. Then, at some later point, the speeds
were modified. Those cached values would no longer be consistent with the state variables.
But because the Position stage cache entries would not be discarded, they would still be
present in the cache and accessible to anyone who looked for them.
When you were simply using classes written by other people, you didn’t have to worry about
this. You simply trusted that information in the cache would always be correct. But now that
you are preparing to write extensions to Simbody, you need to be aware of it. You need to
know what promises are made by the System, and you have a responsibility to make sure
your code does not break them.
Empty: This is the stage a newly constructed State object is in before it has been realized. It
contains no information at all, and is not specific to any particular System.
Topology: When a State gets realized to Topology stage, it is configured to become a State
for a particular System. In practice, this usually means allocating space in the cache for
whatever data the System needs to store, and creating Model stage state variables. The
Topology stage is unique in that no state variables may correspond to it. There is no such
thing as a “Topology stage state variable” in a State. Logically, Topology stage “state
variables” are the data members of the System; that is, they are stored with the System not
with the State. The effect on the State when you realize it to Topology stage can only depend
on properties of the System (“topological properties”), not on the value of any variable in the
State.
Model: When a State is realized to Model stage, its complete set of state variables becomes
determined. This means that the set of state variables may depend on the value of a Model
The First Four Computation Stages 4
stage state variable. For example, Simbody allows rotations to be modeled with either
quaternions or Euler angles. You select which representation to use by calling
setUseEulerAngles() on the SimbodyMatterSubsystem. Your choice is stored in a Model
stage discrete state variable. If you select Euler angles, three generalized coordinates will be
created for each rotation. If you select quaternions, there will be four. In general you use
Model stage state variables (typically integers or boolean flags) to choose modeling options
that may affect the number and type of later-stage state variables that are allocated.
This has an important consequence: if you change Model stage variables during a simulation
(typically in an event handler), States created before the change may contain different state
variables than ones created after. More commonly, Model stage variables are used to
configure a System before beginning a simulation, not once the simulation has started.
Instance: At Instance stage, we finalize all physical parameters, such as mass, geometry,
spring constants, etc., creating an “instance” of some physical system. We also know which
force elements, constraints, and events are enabled, so the set of cache entries can be
finalized. This means that the set of active forces, constraints, and event handlers may
potentially change during a simulation. For example, Simbody uses an Instance stage
variable to record which Constraints are disabled.
Instance stage realization is a good time to precalculate values that won’t change, or will only
change during discrete events, not during continuous integration intervals.
2 Custom Forces,
Constraints, and
Mobilizers (joints)
Writing your own Subsystem as described in the next chapter provides the most flexibility,
but there is almost always an easier way. Specifically, most built-in Simbody Subsystem
manage a collection of “elements” that typically provide a great deal of functionality and
customizability. The GeneralForceSubsystem, for example, has springs and the like but also
a fully general “custom” force element that is easy to write. The SimbodyMatterSubsystem
has built-in constraints and mobilizers (joints) but also provides the ability to write custom
constraints and custom mobilizers.
5
A Custom Force 6
The calcForce() method is called to calculate the force. Notice that it has three different
arguments for storing forces into: bodyForces, particleForces, and mobilityForces. Use
bodyForces to apply Cartesian forces and torques to bodies. That is what we are doing in this
example. You also can use mobilityForces to apply forces directly to individual degrees of
freedom. That is, there is one scalar element corresponding to each generalized speed. A
Force object may apply either or both types of force.
(Currently, particleForces is ignored. That is because Simbody does not yet support particles
as a special case—you can include them as bodies, though. It is expected that they will be
7 A Custom Constraint
given special handling in a future version, so the interface includes them for forward
compatibility.)
Similarly, calcPotentialEnergy() is called to calculate the potential energy due to the force.
Finally, there is an optional method called dependsOnlyOnPosition(). The default
implementation returns false. If you override it to return true, that enables an optimization
to avoid recalculating the force and energy when a generalized speed or auxiliary state
variable is modified. Since our force depends only on q, not on u or z, we return true. This
will potentially make our simulations run faster.
MultibodySystem system;
SimbodyMatterSubsystem matter(system);
GeneralForceSubsystem forces(system);
Force::Custom(forces, new ExampleForce(matter));
As you see, it works just like any other Force object. We simply create a Force::Custom,
passing an instance of our Implementation class as an argument. You can also write a
“handle” class derived from Force::Custom which hides your force implementation class and
provides a nicer API for your force element that acts exactly like built-in force elements do.
See the Doxygen API documentation for Force::Custom for more information.
In principle, constraints are simple. As described in the User’s Guide, a constraint is just an
equation of the form c(d;t,y) = 0. How hard can that be to implement? Actually, there are
some constraints that really are as simple as that, and Simbody offers a special mechanism
that lets you implement them in a truly easy way. We will discuss it in the next section. The
problem is that, in many cases, the constraint function depends in some enormously
complex way on a very large set of state variables.
A Custom Constraint 8
On its own, that isn’t usually a problem. After all, Simbody will calculate the locations of the
points for you, and it’s easy enough to then calculate the distance between them. But there is
a second issue that complicates matters. Each constraint equation also implies that its time
derivatives are satisfied too. The Rod constraint, for example, generates three equations that
must be satisfied: a position-level constraint equation requiring the distance between two
points to be fixed; a velocity-level constraint equation requiring their relative velocity to be
zero; and an acceleration-level constraint equation requiring their relative acceleration to be
zero. You must implement all of these and make sure they are all consistent with each other.
Again, Simbody can provide all the information you need, but deciding exactly how to put
that information together correctly will take some math!
Let’s take a look at an example. Here is a custom constraint that requires the distance
between two bodies’ origins to remain fixed. This is just a special case of the more general
Rod (distance) constraint that is built in to Simbody. So this isn’t a very useful class, but it is
a fairly easy one to understand, so it makes a good example. An executable version of this
example is provided with Simbody as [Link]; a screenshot is
shown below. The example visualizes the custom constraint as a thick blue line between the
body origins.
9 A Custom Constraint
private:
ConstrainedBodyIndex body1, body2;
Real distance;
};
There’s a lot to discuss here. The constructor takes two bodies to constrain and the required
distance between them. Notice the three integers that get passed to the superclass
constructor. Those are the numbers of holonomic (position), nonholonomic (velocity), and
acceleration-only constraint equations defined by this class. We are creating a single
holonomic constraint equation, so we pass 1, 0, 0. Note that although there is a single
constraint equation here, because it is defined at the position level we are going to have to
implement three routines—the equation itself and its first and second time derivatives. We
also have to provide a routine that generates forces from the calculated constraint multiplier.
This optimization has a very profound impact on how you write constraints. If you look at
the example above, you will notice that none of the calculation routines ever reference a
MobilizedBody object, a MultibodySystem, or a SimbodyMatterSubsystem. Instead,
Constraint::Custom::Implementation defines its own methods that you use instead, like
getBodyOriginLocation() and getBodyVelocity(). These methods refer to bodies with a
ConstrainedBodyIndex, not a MobilizedBodyIndex. If you don’t see a method you need,
don’t figure out a clever loophole that lets you use SimbodyMatterSubsystem or
A Custom Constraint 12
MobilizedBody methods—that will not work correctly! Instead, post a question to the
Simbody help forum at [Link] Public Forums.
After you call addConstrainedBody() to register all of the constrained bodies, Simbody
identifies an “ancestor body” A, which is the nearest common ancestor shared by all the
constrained bodies. This allows it to define a “constrained system”, consisting of the
constrained bodies and all of their parents going back to the ancestor body. Often this will
only be a small fraction of the bodies in the full System, but all the other bodies are
guaranteed to have no impact on whether the constraint is satisfied. This can save a huge
amount of computation. When you call getBodyOriginLocation(), it actually returns the
location in the ancestor body’s reference frame, not in the ground frame. But that doesn’t
really matter—in your code you just treat the ancestor as though it were ground.
Now look at the methods that implement the constraint. This is a holonomic constraint, so it
involves three constraint equations. There is a virtual method corresponding to each one:
calcPositionErrors(), calcPositionDotErrors(), and calcPositionDotDotErrors(). Each one
calculates the error in the appropriate constraint equation. In this case, the position level
error is perr(t,q)=(r×r–d2)/2 and the corresponding constraint equation is perr=0. The
velocity level error is verr(t,q,u)=d/dt perr=v×r, with constraint verr=0. The acceleration
level error is aerr(t,q,u,u̇ )=d/dt verr=a×r+v×v, with constraint aerr=0. Each equation is just
the time derivative of the previous one, and that is an absolute requirement! Note that you
can’t just produce some equivalent equation (like leaving out the 2 in the first equation here)
because it is really the error term that we are returning and that is never zero. That is, the
code returns only the constraint errors and it is those errors that must be properly
differentiated. That said, there are still many sets of equations that define the same
constraint—for example, we could have used the actual distance |r|–d as the position level
error, rather than the difference of squares. That has some advantages, but simplicity of
exposition is not among them! Different choices will affect the meaning of the constraint
multiplier (for example, swapping the roles of bodies one and two will change the
multiplier’s sign), but corresponding changes to the force generation routine ensure that the
physical forces that result will be identical if the meaning of the constraint is unchanged.
An important thing to note about these methods is that the errors depend on Simbody-
supplied arguments at the same level, while earlier information is taken from the state. So
position errors are calculated from position arguments, but may pull time from the state.
13 Simple Constraints
Velocity errors depend on velocity arguments, but can get time and position from the state.
Acceleration errors depend on acceleration arguments and get time, position, and velocity
from the state. Finally, the force generation routines depend on multipliers supplied as
arguments, but get anything else they need from the state. Never extract anything at the
same level or higher from the state; that is incorrect and will produce non-physical behavior.
In addition to calculating the constraint errors, we also need to calculate the constraint
forces that should be applied at each time step to maintain the constraint. Simbody
automatically calculates the Lagrange multipliers corresponding to each constraint and
passes them to addInPositionConstraintForces(). We use them to calculate the force to apply
to each body. If you aren’t familiar with Lagrange multipliers, they are beyond the scope of
this document, but you can easily find descriptions of them online. For our purposes, just
think of them as scalar constraint forces (in this case, rod tension) that need to be calculated
in order to satisfy the constraint at the acceleration level. We call addInStationForce(),
which is a convenience method to apply a linear force at a specific point (“station”) on a
specific body. It works out the correct force and torque to apply, and adds them to the
appropriate element (the one corresponding to a particular ConstrainedBody) in the Array of
SpatialVecs.
There are three special cases for which Simbody offers simple constraint classes. The first is
a holonomic constraint that can be written as a simple function of the generalized
coordinates: c(q) = 0. This class is called Constraint::CoordinateCoupler, since it defines a
coupling between some set of coordinates.
The second is a nonholonomic constraint that can be written as a simple function of the
generalized coordinates and generalized speeds: c(q; u) = 0. This class is called
Constraint::SpeedCoupler. Although the constraint equation may involve coordinates, it is
strictly a constraint on the speeds. It considers q to be fixed as suggested by the “;” above,
and manipulates u to satisfy the equation given the current value of q.
The third case is a holonomic constraint that explicitly specifies the behavior of one
generalized coordinate as a function of time: qi = f(t). This class is called
Constraint::PrescribedMotion, since the motion of one coordinate is explicitly prescribed in
advance.
Each of these classes requires you to provide a function of some set of state variables. This is
done with the Function_<T> class. A Function object defines scalar or vector function of m
arguments. That is, it provides a method with the following signature:
Function is a templatized class, with the output type as a template parameter. Most useful
are types Real and short vector types like Vec3. All of the Constraint classes require a
Function_<Real>, for which there is a typedef abbreviation Function. So calcValue() will
return a Real.
Suppose we want a constraint that requires two generalized coordinates to always be equal
to each other. This is done with a Constraint::CoordinateCoupler that enforces c(q) = q1-q2 =
0. Here is a Function class that implements c(q):
The implemention of calcValue() is simple: it just returns the difference between its two
arguments. Since Function is templatized only on the type of the output value, not the
number of input arguments, we also must implement getArgumentSize() to return the
expected number of input arguments (2 in this case).
A Function class also must implement calcDerivative() to calculate the partial derivatives of
the function. This takes a Array_<int> (behaves like std::vector<int>), which lists all
arguments with respect to which to take the derivative. If the array is of length 1 (that is, a
first derivative), we return either 1 or -1, depending on whether a derivative with respect to
the first or second argument is requested. If the array length is greater than 1 (a second
derivative or higher), we return 0.
A Function need not calculate all possible derivatives, since usually only the first few orders
are required. It just needs to implement getMaxDerivativeOrder() to report the highest order
derivative it can calculate. In this example there is no limit to which ones we can calculate
(all derivatives higher than first order are 0), so we return the maximum possible integer
value. The Functions used for constraints must support derivatives up to second order.
Array_<MobilizedBodyIndex> coordBody(2);
Array_<MobilizerQIndex> coordIndex(2);
coordBody[0] = [Link]();
coordBody[1] = [Link]();
coordIndex[0] = MobilizerQIndex(0);
coordIndex[1] = MobilizerQIndex(0);
Constraint::CoordinateCoupler constraint(matter, new ConstraintFunction(),
coordBody, coordIndex);
In addition to telling the CoordinateCoupler what function to use, we also must tell it which
coordinates to pass as arguments. For each coordinate, we specify the MobilizedBody it
belongs to and the index of that coordinate for the MobilizedBody. In this example, we
constrain the first coordinate of body1 to always equal the first coordinate of body2.
A Custom Mobilizer (The Easy Case) 16
This example could actually be made even simpler. Simbody provides Function subclasses
for common function types, such as linear functions, polynomials, and splines.
Function::Linear represents a linear function of its arguments. For two arguments, for
example, the function is f(x, y) = Ax+By+C. You provide the coefficients. We want (A, B, C) =
(1, -1, 0), so we create the constraint as follows:
Vector coefficients(3);
coefficients[0] = 1;
coefficients[1] = -1;
coefficients[2] = 0;
Constraint::CoordinateCoupler constraint(matter,
new Function::Linear(coefficients), coordBody, coordIndex);
Now we don’t even need to write our own Function subclass! This is a Constraint that truly
is easy to implement.
can model this with a single generalized coordinate and no constraints, with performance
comparable to a simple Pin joint.
There is some good news and some bad news about this. First the bad news: in the general
case, writing a custom MobilizedBody is even more difficult than writing a custom
Constraint. Now the good news: as with Constraints, Simbody provides a class that lets you
implement some, but not all, MobilizedBody types in a fairly easy way.
This time we’ll start with the easy case. MobilizedBody::FunctionBased allows you to create
new MobilizedBodies that have the following properties:
3. The motion of the body can be described with six functions of the generalized
coordinates, where three of them return translations along fixed axes, and the other
three return rotation angles around fixed axes.
First, we need six Functions. Two of them will be linear functions (for the X translation and
Y rotation), and the other four will be constant functions that always return 0.
Vector coefficients(2);
coefficients[0] = 1;
coefficients[1] = 0;
Array_<const Function*> functions(6);
functions[0] = new Function::Constant(0, 0);
functions[1] = new Function::Linear(coefficients);
functions[2] = new Function::Constant(0, 0);
functions[3] = new Function::Linear(coefficients);
functions[4] = new Function::Constant(0, 0);
functions[5] = new Function::Constant(0, 0);
Notice that the linear functions are the second and fourth entries in the array. The first three
functions return the rotation angles and the last three return the translations. By default, the
A Custom Mobilizer (The Hard Case) 18
axes are X, Y, and Z respectively, but you can modify them to have translations along
arbitrary directions and rotations around arbitrary axes.
Next we need to tell Simbody what coordinates to pass to each function. We want to pass the
first generalized coordinate to function 1, the second one to function 3, and no coordinates at
all to the other functions. We create an Array_<int> (like std::vector<int>) for each function
listing the coordinates to pass to it:
There are several other constructors that let you specify other options, such as the axes to
use and the inboard and outboard transforms. Note that in general you can pass multiple
coordinates into each function or you can pass the same coordinate into each function to
create coupled rotational and translational motion driven by a common coordinate.
If you are interested in a deeper understanding of custom mobilizers for biological and other
complex joints, see this paper:
A. Seth, M.A. Sherman, P. Eastman, S.L. Delp, “Minimal formulation of joint motion for
biomechanisms,” Nonlinear Dynamics, vol. 62, no. 1, pp. 291-303, 2010.
You can find this paper on the Simbody Documents page, or a link to the journal article on
the Simbody Publications page.
with a quaternion, it will not work because that joint wouldn’t satisfy the q! = u condition we
described above. Instead, you need to use MobilizedBody::Custom.
We pass three integers to the superclass constructor: the number of generalized speeds (3),
the number of generalized coordinates (3), and the number of those coordinates that
correspond to rotation angles (0).
This vector gives the origin of the mobilizer’s M frame (on the MobilizedBody) as a vector
from the origin of its F frame (on the parent body), expressed in the F frame. See the
Simbody Tutorial for definitions of these frames, which are common to all mobilizers.
Finally, there are two methods that “best fit” q and u based on a Transform or spatial
velocity. These implement the standard MobilizedBody methods of the same names.
3 A Custom Subsystem
A Subsystem is Simbody’s most general element type. A System will typically contain a small
number of Subsystems. The System’s job is to dole out work to the Subsystems in a
predefined order, and to permit Subsystems to access one another’s state variables and
cache entries in a controlled fashion. Subsystems are not nested; they are a flat partitioning
of the System’s work. Typically a concrete System object will insist that certain types of
Subsystems be present. MultibodySystem, for example, requires a SimbodyMatterSubsystem
and a set of ForceSubsystems.
It is unusual to need a new Subsystem—be sure to check first whether you can achieve the
results you want with custom forces, custom constraints, or custom mobilizers as discussed
in the previous chapter. You may want to discuss your problem on the Simbody forum to see
how others have tackled similar issues. But if you do need to make your own Subsystem,
read on.
#include "Simbody.h"
To understand this code, you first need to know that a Subsystem is actually defined by two
different classes. Subsystem defines the “public interface” to it—those properties that most
23 A First Subsystem
people access most of the time. There also are many properties related to the
implementation, which most users of the Subsystem do not care about most of the time. To
keep the interface clean, these properties are split off into a separate class called
Subsystem::Guts. An object of this type is created automatically for each Subsystem and can
be accessed by calling getSubsystemGuts() on it.
To define a new type of Subsystem, you must create a subclass of each of these classes. The
Subsystem subclass defines the public API, while the Subsystem::Guts subclass defines the
implementation.
Now let’s look at the example above. We define a class called ExampleSubsystemImpl that
will provide our implementation. Since our Subsystem doesn’t currently do anything, there
isn’t much to implement. The only method it is required to implement is cloneImpl(). There
are many others which it can implement, and we will see some of those later. But all the
others are only required if you need to provide certain features in your Subsystem.
ExampleSubsystem(MultibodySystem& system) {
adoptSubsystemGuts(new ExampleSubsystemImpl());
[Link](*this);
}
The Subsystem we are planning to create will work only with MultibodySystems, so we
require one as a constructor argument. The constructor creates an ExampleSubsystemImpl,
registers it by calling adoptSubsystemGuts(), and adds itself to the System.
You can now create an ExampleSubsystem and add it to a System exactly as you would any
other Subsystem:
MultibodySystem system;
SimbodyMatterSubsystem matter(system);
ExampleSubsystem example(system);
Notice that the ExampleSubsystem is created as just a local variable. It will disappear as
soon as that variable goes out of scope. The ExampleSubsystemImpl is the true persistent
object. For this reason, you should never add fields to a Subsystem subclass or try to store
information in it. Instead, store all information in the Subsystem::Guts subclass. A
Subsystem object is merely a glorified pointer to it.
A ForceSubsystem 24
3.2 A ForceSubsystem
Now let’s make our Subsystem actually do something. We’re going to implement in a
Subsystem the same “mutual repulsion” capability we created in the previous chapter using a
custom force element; if this is all you need to do you should definitely use a custom force,
not a whole Subsystem! However, the example will serve nicely to illustrate the mechanics of
building a Subsystem.
So once again we’re going to cause all the MobilizedBodies in the System to repel each other
with a force proportional to 1/r2:
#include "Simbody.h"
The first thing to notice is that we are using different parent classes: ForceSubsystem and
ForceSubsystem::Guts. ForceSubsystem is a subclass of Subsystem defined by Simbody,
which you should use for any Subsystem that applies forces to bodies. This ensures that
Subsystems will be realized in the proper order, and also defines a calcPotentialEnergy()
method in which you can calculate your force’s contribution to the potential energy of the
system. Also notice that we add the Subsystem to the System by calling
addForceSubsystem(), instead of adoptSubsystem() like we did in the previous example.
Very little of the code in this method should look unfamiliar. We begin by calling
getSystem() to get a reference to the System this Subsystem is part of, and use
MultibodySystem’s downcast() method which verifies that we have the expected type of
System before returning a reference to it:
Now we have a pair of nested loops over each pair of bodies. We calculate the displacement
between them, use that to calculate a force, and add it to the appropriate entries in the
vector.
Note that the above routines are very similar to the ones we wrote for a custom force in the
previous chapter, but we have additional bookkeeping to do here.
You might wonder why this is necessary. If a topological change actually affects the data
stored in a State (such as the set of cache entries), obviously old States will no longer be
valid, but why does it matter for a simple change to the force constant?
The answer is that it is especially important for changes like this, because that is the only
way to catch a variety of errors. A topological change should only ever be made before the
start of a simulation, not in the middle. Otherwise, a saved State from earlier in the
simulation (created based on an old value of the force constant) could easily get passed to a
29 Creating a State Variable
routine that would try to analyze it based on the new value. This is a very insidious sort of
bug, because there is no way to detect it by looking at the State object itself. Calling
invalidateSubsystemTopologyCache() ensures that all such errors will be caught.
If this seems restrictive, don’t worry. There’s an easy solution, which we’ll see in the next
section: if you want to be able to change something in the middle of a simulation, make it a
state variable instead of a topological property.
The changes to ExampleSubsystem are very simple. We added two accessor methods, which
just invoke the corresponding methods of ExampleSubsystemImpl. For convenience, note
that we created two methods for looking up the ExampleSubsystemImpl:
ExampleSubsystemImpl& updImpl() {
return dynamic_cast<ExampleSubsystemImpl&>(updRep());
}
const ExampleSubsystemImpl& getImpl() const {
return dynamic_cast<const ExampleSubsystemImpl&>(getRep());
}
Since we will be accessing it many times, this saves us from having to write a dynamic_cast
every time.
...
That may seem strange. Isn’t the whole point of realization to modify the State and store new
information in it? The answer is that all cache entries are mutable. They are exactly what the
name suggests: a cache. The state variables are the true information. The cache entries can
always be regenerated based on them. So a const State reference still allows you to modify
the cache, but not the state variables. Logically nothing has changed when the cache is filled
in—you could delete the whole thing and regenerate it any time from the information in the
state variables.
31 Creating a State Variable
We call allocateDiscreteVariable() on the State to create a new discrete variable. There are
three arguments: the state, the stage that should be invalidated when this variable’s value is
changed, and a Value object to hold the actual value, with a default value given. We specify
Dynamics stage, since that is the earliest stage whose computations depend on this variable.
Notice that Value is a template. A discrete state variable can hold any type of value, even a
large data structure. It is not restricted to just real values the way continuous state variables
are.
The return value from allocateDiscreteVariable() is an index, which we will use whenever we
want to access the value. We store that index in a mutable field of ExampleSubsystemImpl.
I hope you just cried out in horror at that last sentence? If not, please reread it and think
carefully about why it’s such a shocking statement! A System (including all its Subsystems)
is supposed to be immutable during a simulation. All mutable information is supposed to go
into the State. And here we are, realizing a State object... and we modified a field of the
Subsystem? Didn’t we just violate that design principle? And what will happen when we
realize a different State, and write a new value to the field?
In any other situation, you would be completely correct. Topology stage is the only time
when we are allowed to modify the System during realization. And not just any change; only
properties that are calculated as a result of realizing the State during realizeTopology().
Indices are calculated for the state variables and cache entries as they are allocated, and we
need to store them for future reference.
But what happens when we realize another State? Won’t the indices get overwritten with
new values? That’s where Topology stage is special. Remember, there are no Topology stage
variables in a State. What happens in realizeTopology() can depend only on topological
properties of the System, which means you will get exactly the same indices every time it is
called (unless you modify a topological property, in which case your previously created
States will become invalid anyway.) Mutable fields in a System (or in a Subsystem that is
part of a System) are like Topology-stage cache variables—they are calculated but add no
new information. Everything they depend on is present as Topology-stage “state variables”,
Other Subsystem Features 32
i.e. non-mutable data members of System and Subsystem objects. You can recalculate them
any time and you’ll always get the same values.
But wait a minute—what about Model stage? You can also allocate state variables then. Does
that mean it’s also alright to store indices in the System while realizing Model stage?
The answer is a very emphatic NO! What happens during Model stage can depend on state
variables, so you might get different indices for different States. Instead, create a data
structure to hold any indices that will be calculated at Model stage. Then at Topology stage,
allocate a cache entry to hold an instance of that structure. That way, all the indices
calculated at Model stage will be stored in the State, not the System.
state variables. Each of these methods takes a Vector containing the initial values of the state
variables to allocate. The length of the Vector determines how many variables will be
allocated. The return value is the index of the start of the block within the Vector of state
variables for that Subsystem. For example, if you allocate a block of q’s by writing
then you would look up the value of the first one by calling
value = getQ(state)[qindex];
If you allocate continuous state variables, you will usually also want to implement
realizeSubsystemAccelerationImpl() to calculate their derivatives. You set them by calling
updQDot(), updUDot(), and updZDot() on the State. For q’s you also need to provide second
time derivatives via updQDotDot(). Those are often, but not always, the same as udots. The
qdot, udot, zdot, and qdotdot values are actually cache entries created and managed
automatically by the State as a result of the allocation of the corresponding continuous
variables.
Note that all the above routines are const, even though they may modify the cache entry’s
value; that’s because cache entries are always mutable since, as discussed above, they do not
contain any new information.
But sometimes you might prefer to have a Subsystem do its own event handling, especially if
the events are closely related to other functions of the Subsystem.
It is quite difficult to handle constraints correctly and we highly recommend that you not do
this from your own Subsystems but instead take advantage of the Constraint facility that is
Other Subsystem Features 36