Constraints
This example builds a double pendulum on a moving base and uses it as a guided
tour of the generic Constraint interface: creating
constraints, enumerating and inspecting them, tuning their parameters, reading per-step
diagnostics, animating a target, and destroying them.
Source: examples/example_constraints_double_pendulum.py
The scene has two rigid links:
Link1is pinned to a fixed world point by a rigid pivot-position constraint — a 3-DoF ball joint to a world anchor. Rotation is free, so the link swings; the anchor is animated over time to create a moving base.Link2hangs offLink1's free end via a rigid spherical joint — a body-to-body ball joint.
Modeling a double pendulum with constraints (as done here) is the preferred approach when joints are added or removed at runtime. For a fixed joint structure, prefer an articulated actor instead — it is more efficient and robust.
Implementation
Creating the Constraints
Both constraints share the same spring-damper parameters. The pivot pins a local point on Link1 to a world anchor; the spherical joint ties Link1's far end to Link2's near end. (The 0.0125 offsets place the pivots on each link's centerline — the box mesh is corner-anchored, so its centerline sits at half the cross-section thickness.)
pivot = scene.create_rigid_pivot_position_constraint(
physics.RigidPivotPositionConstraintParams(
actor=link1.get_handle(),
local_position=[0, 0.0125, 0.0125],
target_position=[0, 0.5, 0], # the world anchor
stiffness=2.5e4,
damping=3.5,
saturation=-1.0, # force cap disabled
)
)
spherical = scene.create_rigid_spherical_joint_constraint(
physics.RigidSphericalJointConstraintParams(
actor_a=link1.get_handle(),
actor_b=link2.get_handle(),
local_pos_a=[0.25, 0.0125, 0.0125], # Link1 far end
local_pos_b=[0, 0.0125, 0.0125], # Link2 near end
stiffness=2.5e4,
damping=3.5,
saturation=-1.0,
)
)
Enumerating and Looking Up Constraints
Every constraint has a stable handle. Enumerate all constraints in a scene with for_each_constraint, and look one up with get_constraint:
types = []
scene.for_each_constraint(lambda c: types.append(c.get_type()))
assert scene.get_constraint(pivot.get_handle()) == pivot
Introspecting a Constraint
The generic interface reports how a constraint is wired up. get_num_actors is a handy discriminator — the pivot involves one actor, the spherical joint two:
for i in range(constraint.get_num_actors()):
actor = constraint.get_actor(actor_index=i)
dofs = constraint.get_dof_indices_for_actor(actor_index=i)
print(actor.get_name(), list(dofs))
Tuning Parameters
Stiffness, damping, and saturation can be read and changed at runtime:
k = constraint.get_stiffness()
constraint.set_stiffness(stiffness=k * 2.0)
constraint.set_damping(damping=constraint.get_damping())
constraint.set_saturation(saturation=constraint.get_saturation())
Reading Diagnostics Each Step
get_deviation returns the current constraint error and needs no setup. get_force requires a CONSTRAINT_FORCE query registered before stepping:
pivot.register_query(physics.QueryType.CONSTRAINT_FORCE) # once, before stepping
# ... inside the loop, after scene.step(dt):
deviation = pivot.get_deviation() # position error [m]
force = pivot.get_force() # generalized force
The example attaches the debugger to visualize the scene and prints each constraint's force and the pivot deviation to the console roughly once per second.
Driving the Moving Base
Setting the pivot's target every step animates the anchor along a small circle, turning the fixed pendulum base into a moving one:
pivot.set_target_position(physics.Real3(x, y, z))
Only target-bearing constraints support this. Calling set_target_position on the spherical joint (which has no position target) raises a graceful error — the interface is uniform, but capabilities are type-specific:
try:
spherical.set_target_position(physics.Real3(0, 0.5, 0))
except physics.Error:
pass # not supported for a spherical joint
Destroying Constraints
Remove a constraint explicitly with destroy_constraint; destroying an actor auto-destroys the constraints attached to it:
scene.destroy_constraint(spherical) # Link2 detaches and falls away
scene.destroy_actor(link1) # also removes the pivot constraint
Features
- World-anchor constraint (
RigidPivotPosition) pins a rigid body to a world point with a free rotation. - Body-to-body joint (
RigidSphericalJoint) couples two rigid bodies at a shared pivot. - Full interface tour: creation,
for_each_constraintenumeration,get_constraintlookup, introspection (get_type/get_num_actors/get_actor/get_dof_indices_for_actor), parameter tuning, per-stepget_deviation/get_force, target animation, and destruction. - Moving base: a time-varying
set_target_positiondrives the anchor along a circle. - Live diagnostics: each constraint's force and the pivot deviation are printed to the console as the simulation runs.
- Scripted timeline: the example weakens the pivot (via stiffness), removes the middle joint, then destroys
Link1to show its constraint auto-destroy.
Running
uv run --no-project examples/example_constraints_double_pendulum.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 double pendulum, but without the code-only extras (the moving base, live diagnostics, and the scripted add/remove timeline). It's the source of truth for the scene's geometry and constraint parameters.
Source: assets/samples/constraints_double_pendulum.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/constraints_double_pendulum.mochi_scene")
The two constraints map directly onto prefab keys — rigidPivotPosition and rigidSphericalJoint — mirroring the programmatic parameters above (actors are referenced by name):
"constraints": {
"rigidPivotPosition": [
{
"actor": "Link1",
"localPosition": [0, 0.0125, 0.0125],
"targetPosition": [0, 0.5, 0],
"stiffness": 25000,
"damping": 3.5
}
],
"rigidSphericalJoint": [
{
"actorA": "Link1", "localPosA": [0.25, 0.0125, 0.0125],
"actorB": "Link2", "localPosB": [0, 0.0125, 0.0125],
"stiffness": 25000,
"damping": 3.5
}
]
}