Composite Class#
Composite objects enable the creation of hierarchical models by grouping multiple lower-level entities into a single reusable unit. These composites can be used like any other msolve object, enabling reuse, modular design and sharing across your organization.
A Composite class exposes a clearly defined interface, a set of named attributes which allows it to interact with the rest of the model.
In addition to the standard data types (e.g. Int, Str, Bool, Double), a special external dependency type called References allows the Composite to receive links to other msolve objects(e.g., parts, markers, forces) from the parent model.
This enables clean separation of concerns: the Composite encapsulates internal logic while remaining configurable and connectable through a well-defined external interface. The automatic reference resolution process described below ensures that references are dynamically resolved based on naming conventions and hierarchy, simplifying model construction and maintenance.
- class Composite(**kwds)#
Base class for defining higher-level modeling entities (HLEs) in msolve.
Composite is a special class that allows you to aggregate modeling entities, such as Parts, Markers, Forces and other MotionSolve primitives, into a higher-level modeling entity (HLE).
- Key characteristics of a Composite:
Behaves as an atomic entity.
Can be instantiated multiple times.
Accepts all properties via the constructor.
Properties are accessed and modified using Python’s dot notation.
Supports custom validation via an optional validateSelf method.
- To define a new Composite:
Derive your class from Composite.
Declare the class interface by defining its properties and their types.
Implement createChildren() to instantiate internal entities.
Implement updateChildren() to populate or modify those objects in response to input.
Optionally, implement validateSelf() to enforce constraints on input values.
Note
When defining an object property, setting its topology attribute to True indicates that the Composite instance has dependencies on other solver objects. This ensures that all internal solver objects are updated appropriately based on new topology.
Warning
All solver object creation must occur in createChildren(). The updateChildren() method should only modify existing objects—not create new ones.
- createChildren()#
Create the internal MotionSolve objects for this Composite.
This method is called automatically once, when the Composite instance is created. It should be used to instantiate all internal MotionSolve objects and define their immutable properties. The method will be called again in case any Composite properties have the topology attribute set to True.
Note
If any Composite property is declared with topology=True, modifying that property causes the entire Composite instance to be destroyed and re-created from scratch. As a result, createChildren() will be invoked again during the re-creation process.
Warning
Only MotionSolve objects (e.g., Body, Joint, Force) should be created in this method. Supporting types such as Point, Vector, or Matrix44 are not intended to be instantiated here.
- updateChildren()#
Update the mutable properties of the internal MotionSolve objects.
This method is called after createChildren and is used to configure or update the mutable properties of the MotionSolve objects created within the Composite. It allows the Composite to react to external inputs such as parameter changes or resolved references.
- validateSelf(validator)#
Validate the inputs provided to the Composite interface.
This method performs checks to ensure that input data passed to the Composite complies with expected modeling constraints. It is optional to implement and is typically used to catch configuration errors early.
The validator object passed to this method gives access to utility functions from the Object Validator Module, which can be used to perform common checks.
Note
This method should call self.checkReferences() to ensure that all external Reference attributes marked with required=True are properly specified.
Reference Resolution Process#
Composite classes may depend on entities defined elsewhere in the model. In such cases, the `Reference` type should be used. A `Reference` serves as a placeholder for an entity that will be resolved at a later stage. It must be associated with a target type (e.g., Marker, Joint, etc.) and can be marked as required using the required=[True|False] flag.
Optionally, a Reference can be initialized with a default value (a string), which serves as a search path for reference resolution. This path corresponds to the name of the target entity and is evaluated using a strict resolution order, ensuring consistent and predictable lookup behavior.
The `default` value represents the target entity’s name and guides the search process.
Reference Resolution Follows This Order:#
Explicit Assignment Bypass:
If the reference is explicitly assigned when creating an instance of Composite, the default is ignored.
The explicitly assigned object is used directly.
Flat Model-Wide Search (No Dots in `default`):
If the default string contains no dots (e.g., “lower_control_arm”):
msolve performs a flat search among top-level entities that are direct children of the model.
Does not recursively search inside Composite objects.
If a match is found, it is returned. Otherwise, the reference remains unresolved.
Hierarchical Resolution (Dots in `default`):
If the default string contains one or more dots (e.g.,
"comp.lower_control_arm"
):The dot (
.
) indicates a hierarchical path through Composite instances.Resolution Process:
One Dot:
Resolves the first segment as a Composite and searches for the entity inside it.
Multiple Dots:
Recursively resolves each composite in the path before accessing the final entity.
No Fallback to Flat Names:
If any part of the hierarchical path fails (e.g., missing composite), the system does not fallback to searching for the dotted name as a flat entity.
Unresolved References Remain as Raw Strings:
If no match is found after all resolution attempts, the reference remains as the original `default` string.
Examples#
Example 1: Explicit Assignment
In this example, the Reference “array” is resolved by direct explicit assignment and name is ignored.
from msolve import *
model = Model()
s = Array(numbers=[1, 2, 3], name="array")
class MyComposite(Composite):
arr = Reference(type=Array, default="array")
m = MyComposite(arr=s)
m.arr
Example 2: Flat Name Resolution (No Dot in `default`)
In this example, the default value of the Reference “array” is resolved by performing a flat model-wide search. It matches the top-level entity named “array”.
from msolve import *
model = Model()
s = Array(numbers=[1, 2, 3], name="array")
class MyComposite(Composite):
arr = Reference(type=Array, default="array")
m = MyComposite()
m.arr # Resolves to top-level Array named "array"
Example 3: Hierarchical Resolution (Dots in `default`)
Here, the default value “comp.array” contains a dot, triggering hierarchical resolution. The resolution locates the Composite named “comp” and then resolves to the entity “array” inside it.
from msolve import *
model = Model()
class MyArray(Composite):
def createChildren(self):
self.arr = Array(numbers=[1, 2, 3], name="array")
class MyComposite(Composite):
arr = Reference(type=Array, default="comp.array")
# Create the composite "comp"
comp = MyArray(name="comp")
m = MyComposite()
m.arr # Resolves to "array" inside "comp"
Example 4: No Fallback on Failed Hierarchy
This example shows that if the hierarchy is invalid, the system does not fallback to a flat name match. The default value “comp.array” expects a composite named “comp”, but since it doesn’t exist, the reference remains unresolved.
from msolve import *
model = Model()
# Top-level Array named "comp.array"
array = Array(numbers=[1, 2, 3], name="comp.array")
class MyComposite(Composite):
arr = Reference(type=Array, default="comp.array")
m = MyComposite()
m.arr # Remains unresolved as 'comp.array' (no fallback to flat name)
Key Takeaways#
The default value in a Reference acts as the search path for resolution.
Dots (`.`) in the default string trigger hierarchical resolution through composites.
No dots trigger a flat, model-wide search.
Explicit assignment always overrides the default and skips the resolution process.
No fallback from hierarchical paths to flat names if the hierarchy is invalid.
If no match is found, the reference remains as the raw string.
In the following example, you will learn how to implement and use a complete Composite class that creates a friction force on a translational joint using the LuGre friction model. A Composite class is a higher level entity that behaves just like any other MotionSolve object. The advantage of using the composite class is that the LuGre object inherits behaviors from the base class. In particular, you will be able to modify the LuGre object at run-time, just like you would issue a modify command on any other MotionSolve primitive object, such as a part or a marker. The example below illustrates how you can create your own composite class.
Example
from msolve import *
# Create a LuGre Composite Class
class LuGre (Composite):
joint = Reference (Joint, default="the_joint", required=True)
vs = Double (1.E-3)
mus = Double (0.3)
mud = Double (0.2)
k0 = Double (1e5)
k1 = Double (math.sqrt(1e5))
k2 = Double (0.4)
def createChildren (self):
"""This method is called when the instance of the Composite
is created. It is used to create all its children objects.
"""
# The DIFF defining bristle deflection
self.diff = Diff (routine=self.lugre_diff, ic=[0,0])
# The MARKER on which the friction force acts
self.im = Marker ()
# The FORCE defining the friction force
self.friction = Sforce (
type = "TRANSLATION",
actiononly = True,
routine = self.lugre_force,
)
# The REQUEST capturing the friction force
self.request = Request (type="FORCE", comment="Friction force")
def updateChildren (self):
# Set topology
self.friction.i = self.joint.i
self.friction.j = self.joint.j
self.im.setValues (body=self.joint.i.body, qp=self.joint.i.qp, zp=self.joint.i.zp)
self.request.setValues (i=self.im, j=self.joint.j, rm=self.joint.j)
def validateSelf (self, validator):
validator.checkReferences()
validator.checkGe0 ("vs")
validator.checkGe0 ("mus")
validator.checkGe0 ("mud")
validator.checkGe0 ("k0")
validator.checkGe0 ("k1")
validator.checkGe0 ("k2")
if self.mud > self.mus:
msg = f"Mu dynamic '{self.mud}' needs to be smaller than Mu static '{self.mus}'"
validator.sendError(msg)
# Now that the class has been defined, the next step is to implement
# its details. The LuGre friction model represents the interaction
# between two bodies using a single-component force and a first-order
# differential equation.
# This equation models the average deflection of the bristles as a
# function of slip velocity, friction coefficients, and normal force.
def lugre_diff (self, id, time, par, npar, dflag, iflag):
"Diff user function"
i = self.joint.i
j = self.joint.j
vs = self.vs
mus = self.mus
mud = self.mud
k0 = self.k0
z = DIF(self)
v = VZ(i,j,j,j)
N = math.sqrt (FX(i,j,j)**2 + FY(i,j,j)**2)
fs = mus*N
fc = mud*N
p = -(v/vs)**2
g = (fc + (fs - fc) * math.exp(p))/k0
def smooth_fabs(x):
return math.fabs(x) # temp solution
if iflag or math.fabs(g) <1e-8:
return v
else:
return v - smooth_fabs(v) * z / g
# Next, let's focus on the Single Component Force.
# The purpose of the Sforce function (commonly referred to as SFOSUB)
# is to compute and return the scalar value of the friction force.
# The three components depicted here are calculated separately
# within the force function.
def lugre_force (self, id, time, par, npar, dflag, iflag):
"Friction Force user function"
i = self.joint.i
j = self.joint.j
diff = self.diff
k0 = self.k0
k1 = self.k1
k2 = self.k2
v = VZ(i,j,j,j)
z = DIF(diff)
zdot = DIF1(diff)
F = k0*z + k1*zdot + k2*v
#print "Time=", time, " V=", v, " Z=", z, " ZDOT=", zdot, " F=", F
#print "k0*z=", k0*z, " k1*zdot=", k1*zdot, " k2*v=", k2*v
return -F
################################################################################
# Model definition #
################################################################################
def sliding_block ():
"""Test case for the LuGre friction model
Slide a block across a surface.
The block is attached to ground with a translational joint that has
LuGre friction
"""
model = Model (output="lugre_friction")
Units (mass="KILOGRAM", length="METER", time="SECOND", force="NEWTON")
Accgrav (jgrav=-9.800)
Integrator (error=1e-5)
Output (reqsave=True)
# Location for Part cm and markers used for the joint markers
qp = Point (10,0,0)
xp = Point (10,1,0)
zp = Point (11,0,0)
ground = Part (ground=True)
ground.jm = Marker (part=ground, qp=qp, zp=zp, xp=xp, label="Joint Marker")
block = Part (mass=1, ip=[1e3,1e3,1e3], label="Block")
block.cm = Marker (part=block, qp=qp, zp=zp, xp=xp, label="Block CM")
# Attach the block to ground
joint = Joint (
name = "the_joint",
type = "TRANSLATIONAL",
i = block.cm,
j = ground.jm,
)
# Apply the LuGre friction to the joint
# NOTE: we can pass the joint directly (explicit definition) or
# let the automatic resolution take care of resolving the Reference
model.lugre = LuGre ()
# to ensure that the joint was resolved correctly we can validate the Composite
model.lugre.validate()
# Push the block by applying a simple harmonic function +- 3 N of amplitude
Sforce (
type = "TRANSLATION",
actiononly = True,
i = block.cm,
j = ground.jm,
function = "3*sin(2*pi*time)",
label = "Actuation Force",
)
# Request the DISPLACEMENT, VELOCITY and FORCE of the Joint
model.r1 = Request (
type = "DISPLACEMENT",
i = block.cm,
j = ground.jm,
rm = ground.jm,
comment ="Joint displacement",
)
model.r2 = Request (
type = "VELOCITY",
i = block.cm,
j = ground.jm,
rm = ground.jm,
comment ="Joint velocity",
)
model.r3 = Request (
type = "FORCE",
i = block.cm,
j = ground.jm,
rm = ground.jm,
comment ="Joint force",
)
return model
# first let's create a model by calling the function
model = sliding_block ()
# run a 4 second simulation
# store the results of the run in an appropriate container
run = model.simulate (type="DYNAMICS", returnResults=True, end=4, dtout=.01)
# define a plot function
import matplotlib.pyplot as plt
def plot(model):
# get the channels from the model
disp = run.getObject (model.r1)
velo = run.getObject (model.r3)
force = run.getObject (model.r2)
plt.subplot(3, 1, 1)
plt.plot(force.times, force.getComponent (3))
plt.title('Friction force')
plt.ylabel('force [N]')
plt.subplot(3, 1, 2)
plt.plot(velo.times, velo.getComponent (3), 'r')
plt.title('Block Velocity')
plt.ylabel('velocity [m/s]')
plt.subplot(3, 1, 3)
plt.plot(disp.times, disp.getComponent (3), 'g')
plt.title('Block Displacement')
plt.xlabel('time (s)')
plt.ylabel('travel [m]')
plt.grid(True)
plt.tight_layout()
# Show the plot
plt.show()
# plot the results
plot(model)
# change the static friction coefficient, simulate and plot the new results
model.lugre.mus= 0.4
run = model.simulate (type="DYNAMICS", returnResults=True, duration=4, dtout=.01)
# replot the quantities and observe the changes to the Friction Force
plot(model)