Skip to main content

Mass on Rod Spring

Experimental

Rod actors are part of the experimental API. Their API may change in future releases.

This example hangs a rigid cube from a helical spring modeled as an elastic rod. It demonstrates loading a curved rod centerline from a file, deriving rod material parameters from a circular cross-section, and coupling a rod to a rigid actor with position and rotation constraints.

Source: examples/example_mass_on_rod_spring.py

For the rod formulation and its parameter reference, see Rod Actors. For the penalty formulation shared by all constraint types, see Constraints.

The helical geometry illustrates how the rod model differs from specialized cable or rope models that are primarily intended for carrying axial tension, with little or no bending or torsional stiffness. In particular, the curved reference geometry causes the overall structural compliance to emerge mainly from torsion of the underlying wire, not axial stretching.

See Choosing a Tendon Model for an example of a rod actor being used in the cable-like regime.

Implementation

Load the Spring Centerline

helix_path = str(resolve_asset("rods/helix_with_visual.mochi.h5"))
shape = physics.load_shape_from_file(helix_path, bake_scale=[1.0, 1.0, 1.0])

The asset stores an eight-turn helix as a polyline of 128 segments spanning 1.25 m along the x axis, with a peak coil radius of 0.125 m. The coil radius tapers smoothly to zero over the first and last 10% of the length, so the centerline is a space curve that begins and ends on the x axis, leaving both endpoints in a convenient place to attach constraints.

The asset also carries a tubular visual mesh skinned to the polyline, so the spring renders as a solid wire without enabling the rod polyline debug-draw feature.

Such an asset can be generated for a given sequence of polyline nodes with physics.experimental.generate_tubular_rod_model_data(), which returns a ModelData holding the polyline simulation mesh, the per-element frame axes, and a tubular visual mesh skinned to the centerline. Leaving element_frame_axes empty lets the frames be generated automatically by parallel transport. The result can be written to disk with physics.model.save_to_file(model, path, physics.FileFormat.H5), or turned into a shape directly with physics.create_model_shape(model) without a round trip through a file. See Authoring Assets for the shared model validation and serialization workflow. However, the generation of the node positions themselves is left to users; there is currently no direct interface to digital content creation tools for space curves.

The core rod simulation functionality supports general cross-section shapes. The tubular visual mesh construction and automatic generation of element frame axes are provided as a convenience, because circular cross-sections are common in many applications. However, ModelData for rods with non-tubular skins and anisotropic cross-sections (where the orientations of element frame axes correspond to principal axes of cross-sections) must currently be generated by custom workflows.

Define the Rod Material

RodMaterialParams takes stiffness coefficients for different deformation modes rather than moduli of three-dimensional solid materials. Formulas for computing these stiffness coefficients from three-dimensional material properties and cross-section geometry are given in Parameter Selection. For a circular cross-section of given RADIUS:

area = math.pi * RADIUS**2
polar_moment_of_inertia = 0.5 * math.pi * RADIUS**4
second_moment_of_area = 0.25 * math.pi * RADIUS**4
torsion_constant = 0.5 * math.pi * RADIUS**4

# Stiffness coefficients: EA [N], GJ [N*m^2] and EI [N*m^2]
axial_stiffness = YOUNGS_MODULUS * area
torsional_stiffness = SHEAR_MODULUS * torsion_constant
flexural_stiffness = YOUNGS_MODULUS * second_moment_of_area

material_params = physics.experimental.RodMaterialParams(
linear_density=DENSITY * area,
linear_rotational_inertia=DENSITY * polar_moment_of_inertia,
axial_stiffness=axial_stiffness,
torsional_stiffness=torsional_stiffness,
flexural_stiffness=[flexural_stiffness, flexural_stiffness],
)

The three stiffness coefficients are named separately here because they are reused below to estimate appropriate constraint stiffnesses.

Read the Endpoints from the Reference Mesh

Rather than hard-coding the coordinates of the spring's ends, the example reads them back from the actor's reference mesh:

coordinates = list(rod_actor.get_mesh().coordinates)
num_nodes = len(coordinates) // 3
last_node_index = num_nodes - 1
last_element_index = num_nodes - 2
rod_near_end_position = coordinates[0:3]
rod_far_end_position = coordinates[-3:]

get_mesh returns reference positions in the actor's local frame, flattened as [x0, y0, z0, x1, y1, z1, ...]. Here that frame coincides with the world frame because world_from_local is the identity; in general the coordinates must be transformed before being used as world-frame constraint targets.

Estimate the Constraint Stiffnesses

Every constraint type shares the same penalty formulation, which has a single default stiffness coefficient from the generic constraint parameters. While this maintains a compact constraint API, it also means that one default value has to stand in for quantities with different physical dimensions: stiffness is interpreted as [N/m] for translational constraints and [N·m/rad] for rotational ones.

The default numerical value of 1e6 is chosen to work for most use cases involving human-scale rigid and articulated actors. However, it is not a good default for many thin rods, where the relevant length scale is the cross-section radius, which is typically much smaller than 1 m. A slender rod's translational (axial) and rotational (bending and twisting) stiffness coefficients scale with differing powers of the cross-section radius, meaning they are often separated by multiple orders of magnitude. A constraint that is far stiffer than the deformation mode it couples to degrades the conditioning of the system of equations solved in each implicit time step.

For rod actors, it is recommended to estimate appropriate values from dimensional analysis based on the rod's stiffness coefficients. Dividing each coefficient by a length scale gives exactly the units the constraint expects:

EA[N]L[m]=[N/m] ,GJ[Nm2]L[m]=[Nm/rad] .\frac{EA \,[\mathrm{N}]}{L \,[\mathrm{m}]} = [\mathrm{N/m}]~, \qquad \frac{GJ \,[\mathrm{N}\cdot\mathrm{m}^2]}{L \,[\mathrm{m}]} = [\mathrm{N}\cdot\mathrm{m/rad}]~.

The example introduces a constant length scale and divides by it:

CONSTRAINT_STIFFNESS_LENGTH_SCALE = 1.0  # [m]

position_stiffness = axial_stiffness / CONSTRAINT_STIFFNESS_LENGTH_SCALE
rotation_stiffness = torsional_stiffness / CONSTRAINT_STIFFNESS_LENGTH_SCALE

A smaller length scale makes the constraint stiffer. If convergence to a hard constraint is desired under refinement of the rod's polyline mesh, a multiple of the element length can be used instead of an O(1)O(1) value.

Pin One End

scene.create_deformable_node_position_constraint(
actor=rod_actor.get_handle(),
node_index=0,
position=rod_near_end_position,
stiffness=position_stiffness,
)

DeformableNodePosition pulls a single node toward a prescribed world-frame target, which this example sets to the node's reference position.

Attach the Mass

The cube is placed so that the center of its -x face coincides with the far end of the spring, and is then coupled to the rod by two constraints:

# Position coupling
scene.create_deformable_node_to_rigid_constraint(
deformable_actor=rod_actor.get_handle(),
rigid_actor=cube_actor.get_handle(),
deformable_node_index=last_node_index,
fix_to_deformable_pos=True,
stiffness=position_stiffness,
)

# Rotation coupling
scene.create_rod_element_rotation_to_rigid_constraint(
rigid_actor=cube_actor.get_handle(),
rod_actor=rod_actor.get_handle(),
element_index=last_element_index,
ref_frame_rot_vec=[0.0, 0.0, 0.0],
stiffness=rotation_stiffness,
)

DeformableNodeToRigid ties the rod's last node to the cube; fix_to_deformable_pos anchors the attachment at the rod node's current position instead of requiring a separate target. RodElementRotationToRigid then couples the material frame of the rod's last element to the cube's orientation.

Run Interactively

physics.initialize(num_worker_threads=0)
scene, _, _ = create_mass_on_rod_spring_simulation()

if not physics.debugger.attach():
physics.shutdown()
return

while physics.debugger.is_attached():
scene.step(time_step)

physics.shutdown()

The simulation steps at a fixed 60 Hz. This is feasible in spite of the high rod stiffness because SuperDex Physics uses stable implicit time integration.

Running

uv run --no-project examples/example_mass_on_rod_spring.py

This example launches or focuses the SuperDex Physics Debugger and runs while it remains connected. See Inspecting Scenes for connection, navigation, and playback controls.

  • Rod Actors — Formulation, parameter reference, and boundary conditions for rods.
  • Constraints — Penalty formulation, the full list of constraint types, and runtime parameter access.
  • Choosing a Tendon Model — When a rod is worth its cost compared with reduced tendon models.