Skip to main content

Plain

This example builds a double pendulum on a rail entirely in code and uses it as a guided tour of the SuperDex Physics articulated actor API: building the joint/link chain, introspecting its topology, reading forward-kinematics state, manipulating the state directly, modeling joints live, actuating without a controller, reading the end-effector Jacobian, and controlling contact.

Source: examples/example_articulations_double_pendulum_on_rail.py

The scene is a single articulated actor — a serial 4-joint / 4-link chain welded to the ceiling:

ceiling -[Hard]-> RailHousing -[Prismatic]-> Cart -[Revolute]-> UpperArm -[Spherical]-> LowerArm

A cart slides on a horizontal prismatic rail; hanging from it is a double pendulum (a revolute upper hinge and a spherical lower joint) whose tip strikes a ball resting on the ground. It has 5 DoFs (Prismatic 1 + Revolute 1 + Spherical 3; the Hard root contributes 0).

Articulated actors vs. constraints

An articulated actor has a fixed joint topology and is more efficient and robust than an equivalent set of constraints. Prefer it whenever the set of joints does not change at runtime. If you need to add or remove joints at runtime, model the joints as constraints instead — see the Constraints example.

Implementation

Building the Articulated Chain

An articulated actor is described by parallel joints[] and links[] arrays, where joints[i] is the inbound joint of links[i] and parent_link = i - 1 forms a serial chain. joint.parent_link_from_joint places the joint frame in its parent link's frame; link.parent_joint_from_link places the link body relative to its inbound joint frame.

params = physics.ArticulatedActorParams(name="DoublePendulumOnRail")
params.world_from_root = physics.TransformRT(translation=[0, 0.75, 0])
params.joints = [
# Root weld: fixes the rail housing to the world (0 DoFs).
physics.ArticulatedJointParams(name="CeilingWeld", type=physics.ArticulatedJointType.HARD),
# Horizontal rail with soft limits, viscous friction, and armature inertia.
physics.ArticulatedJointParams(
name="Rail",
type=physics.ArticulatedJointType.PRISMATIC,
axis=[1, 0, 0],
min_limit=[-0.2, 0, 0], # scalar limit times axis
max_limit=[0.2, 0, 0],
limit_stiffness=250.0,
limit_damping=8.8,
friction=physics.ArticulatedJointFrictionParams(viscous=0.018),
inertia=0.125,
),
# Upper pendulum hinge (revolute about Z) and lower ball joint (spherical).
physics.ArticulatedJointParams(name="UpperSwing", type=physics.ArticulatedJointType.REVOLUTE, axis=[0, 0, -1]),
physics.ArticulatedJointParams(name="LowerSwing", type=physics.ArticulatedJointType.SPHERICAL),
]
params.links = [
# RailHousing, Cart, UpperArm, LowerArm — each a Box collider with a
# parent_link, a parent_joint_from_link offset, a layer, and a density.
# (See the example source for the exact geometry.)
# ...
]
articulation = scene.create_articulated_actor(params)

# Seed the swing: a small rail drift plus an upper-hinge kick.
articulation.set_articulated_joint_velocities(velocities=[0.3, 4.2, 0, 0, 0])

Introspecting the Articulation

get_articulated_shape_info is a one-stop dump of the topology. Each link is also a queryable rigid sub-actor, and joint limits are exposed as inspectable constraints.

info = articulation.get_articulated_shape_info()
for i in range(len(info.link_names)):
# parents[i] is the *parent link index* of link i (-1 for the root).
print(info.link_names[i], info.parents[i], info.joint_types[i])

articulation.get_num_dofs() # 5
articulation.get_nested_link_actors() # 4 rigid sub-actors
articulation.get_articulated_joint_limit_constraints() # limits as constraints

Reading the State (Forward Kinematics)

Read the joint-space pose, the world transforms of every link, and the joint velocities. These use pre-sized output containers.

num_dofs = articulation.get_num_dofs()

pose = physics.DynamicArrayReal(num_dofs)
articulation.get_articulated_pose(pose)

transforms = physics.DynamicArrayTransformRT(len(articulation.get_nested_link_actors()))
articulation.get_articulated_link_transforms(transforms)

velocities = physics.DynamicArrayReal(num_dofs)
articulation.get_articulated_joint_velocities(velocities)

Manipulating the State Directly

Set the pose from joint-space DoFs (e.g. slide the rail by hand), compose a joint-space delta in the tangent space (so spherical DoFs behave correctly), or set the pose from link transforms (IK-style).

# Slide the rail + rotate the upper hinge, then set the actor to that pose.
articulation.set_articulated_pose_from_joints(pose=poked)

# Tangent-space add: out_pose = poked (+) delta.
articulation.add_articulated_delta_to_pose(pose=poked, delta_dofs=delta, out_pose=out_pose)
articulation.set_articulated_pose_from_joints(pose=out_pose)

# IK-style: read link transforms, nudge the end-effector, and write them back.
articulation.set_articulated_pose_from_links(world_from_links=transforms)

# Kick it swinging.
articulation.set_articulated_joint_velocities(velocities=[0.3, 4.2, 0, 0, 0])

Modeling Joints Live

Per-joint friction (viscous/coulomb) and armature inertia can be read and changed mid-simulation — for example, to damp the pendulum by raising the swing joints' friction.

friction = list(articulation.get_articulated_joint_friction_params())  # one per joint
friction[2] = physics.ArticulatedJointFrictionParams(viscous=0.022) # damp the revolute
articulation.set_articulated_joint_friction_params(friction)

inertia = articulation.get_articulated_joint_inertia_params() # one per joint
articulation.set_articulated_joint_inertia_params(list(inertia))

Actuating without a Controller

Apply generalized forces to specific DoFs, or pin DoFs with a boundary condition (e.g. freeze the rail so only the pendulum swings).

# Push the cart along the rail with a constant DoF force.
articulation.set_external_forces_on_dofs(dof_indices=[0], force_values=[1.0])
articulation.clear_external_forces()

# Freeze the rail DoF at its current value, then release it.
articulation.add_boundary_condition_dofs_world(dof_indices=[0], dof_values_world=[rail_pos])
articulation.clear_boundary_conditions()

Mass, Root, and Center of Mass

Mass and the root transform are whole-articulation queries; center of mass and velocity are per-rigid-body queries, so read them from a nested link sub-actor rather than the top-level articulated actor. (The articulated equivalent of set_velocity is set_articulated_joint_velocities.)

articulation.get_mass()                 # total mass of the chain
articulation.get_root_transform() # whole-articulation pose
articulation.set_root_transform(t) # teleports the whole articulation

links = articulation.get_nested_link_actors()
lower_arm = scene.get_actor(links[len(links) - 1])
lower_arm.get_center_of_mass_transform() # per-link (rigid) query

The End-Effector Jacobian

The Jacobian (joint motion → link motion) is read from a nested link sub-actor. It is a flattened 6 x num_dofs (3 translation + 3 rotation rows per joint DoF).

links = articulation.get_nested_link_actors()
end_effector = scene.get_actor(links[len(links) - 1])
jacobian = end_effector.get_articulated_jacobian()

Controlling Contact

The scene uses string layers for coarse control and per-actor overrides for the finest control. Here only the end-effector tip collides with the ball (and the ball with the ground); everything else is disabled.

scene.enable_layer_contact_symmetric("Pendulum", "Ball", enable=False)
scene.is_layer_contact_enabled("EndEffector", "Ball") # left enabled
scene.get_num_contact_layers()
scene.enumerate_contact_layer_names(lambda name: ...)

# Finest-grained: toggle the specific LowerArm sub-actor against the ball.
scene.enable_actor_contact_symmetric(
links[len(links) - 1],
ball.get_handle(),
enable=True,
include_nested_actors=physics.IncludeNestedActors.NO,
)

Features

  • Build-time modeling: all joint types (Hard / Prismatic / Revolute / Spherical), joint limits, per-joint friction and armature inertia, and per-link shape / collider / layer / density.
  • Introspection: get_articulated_shape_info, get_num_dofs, get_nested_link_actors, and joint limits as inspectable constraints.
  • Forward kinematics: read pose, link transforms, and joint velocities.
  • Direct state manipulation: set the pose from joints or from link transforms, add a tangent-space delta, and set joint velocities.
  • Live joint modeling: read/write per-joint friction and armature inertia mid-simulation.
  • Actuation without a controller: external DoF forces and DoF boundary conditions.
  • End-effector Jacobian read from a nested link sub-actor.
  • Contact control: layer-level and per-actor-pair enable/disable.
  • Live console output: the example prints the rail position and the upper-hinge angle once per simulated second.
  • Scripted timeline: freeze the rail → release → push the cart with an external force → damp the pendulum by raising joint friction.

Running

uv run --no-project examples/example_articulations_double_pendulum_on_rail.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.

Also Available as a Prefab

The same scene ships as a declarative prefab — the static version of this example. It builds the identical chain, but without the code-only extras (the console reporting and the scripted timeline). It's the source of truth for the scene's geometry and joint parameters.

Source: assets/samples/articulations_double_pendulum_on_rail.mochi_scene

Load it into a fresh scene (or use physics.prefab.add_to_scene(...) / C++ prefab::AddToScene(...) to add it into an existing one):

from superdex.physics.utils.scene_helpers import create_scene_from_prefab

scene = create_scene_from_prefab("samples/articulations_double_pendulum_on_rail.mochi_scene")

The joints and links map directly onto prefab keys (joint limits are encoded as scalar · axis):

"actors": {
"articulated": [
{
"name": "DoublePendulumOnRail",
"translation": [0, 0.75, 0],
"jointVelocities": [0.3, 4.2, 0, 0, 0],
"joints": [
{ "name": "CeilingWeld", "type": "Hard" },
{
"name": "Rail", "type": "Prismatic", "axis": [1, 0, 0],
"minLimit": [-0.2, 0, 0], "maxLimit": [0.2, 0, 0],
"limitStiffness": 250, "limitDamping": 8.8,
"friction": { "viscous": 0.018 }, "inertia": 0.125
},
{ "name": "UpperSwing", "type": "Revolute", "axis": [0, 0, -1] },
{ "name": "LowerSwing", "type": "Spherical" }
],
"links": [ /* RailHousing, Cart, UpperArm, LowerArm */ ]
}
]
}