Massless Routed Tendons#
Experimental
The massless-tendon API (TendonGuide, TendonGuideType, TendonGuideFlags,
ModelBuilder.add_tendon, the Model.tendon_* and State.tendon_* arrays, and the XPBD/VBD
tendon_* parameters and diagnostics) may change without a deprecation period.
Native routed-tendon USD authoring and MuJoCo lowering below are design sketches,
not implemented APIs. Existing MuJoCo-provider tendon import is supported separately.
Newton’s tendon model represents a massless cable routed through points on rigid bodies. It complements Newton’s rod-based cable model: rods discretize the cable itself into massive bodies, while a tendon stores only its route and applies forces to the bodies along that route.
The route geometry follows Müller et al., Cable Joints (SCA 2018), and the
Newton tendon model supports finite capstan friction at circular rollers and
pinholes. Both SolverXPBD and
SolverVBD consume the same tendon model and mutable
routing state.
Model construction#
Pass a complete ordered list of TendonGuide entries to
add_tendon(). A guide describes the cable’s interaction
with a body; it is not an articulation link. Every guide after the first owns
the incoming straight segment, so its compliance, damping, and rest_length
describe that segment. The first guide has no incoming segment: non-default
values there are rejected. For a linear span, compliance is 1 / stiffness
(m/N versus N/m); zero compliance requests an inextensible XPBD constraint.
The builder validates the entire route before changing any tendon arrays. At least two guides are required, all attached to bodies in the same world. Routes are open and do not implicitly form loops. Internal anchors are allowed; they prevent material transfer between their incoming and outgoing spans.
import newton
builder = newton.ModelBuilder()
anchor_body = builder.add_body(mass=0.0)
guide_body = builder.add_body(mass=0.0)
payload_body = builder.add_body(mass=1.0)
tendon = builder.add_tendon(
[
newton.TendonGuide(body=anchor_body, offset=(-1.0, 0.0, 0.0)),
newton.TendonGuide(
body=guide_body,
guide_type=newton.TendonGuideType.PINHOLE,
offset=(0.0, 0.0, 1.0),
mu=0.1,
compliance=1.0e-6,
damping=0.1,
),
newton.TendonGuide(
body=payload_body,
offset=(1.0, 0.0, 0.0),
compliance=1.0e-6,
damping=0.1,
),
]
)
# VBD requires tendon-connected bodies to receive different graph colors.
builder.color()
model = builder.finalize()
A negative rest_length measures the initial segment rest length from the
authored body poses. Explicit non-negative values use SI units of metres.
The returned index addresses model.tendon_start: a tendon’s guides occupy
[tendon_start[tendon], tendon_start[tendon + 1]) after finalization. Its
segment range starts at tendon_start[tendon] - tendon and has one fewer entry
than its guide range.
add_builder, add_world, and replicate copy guide and segment data and remap
body indices. Local offsets and axes follow the copied body transforms;
fixed-joint collapse transforms them into the retained body’s frame.
Guide types#
Type |
Behavior |
|---|---|
|
Fixes the cable to a body-local point. |
|
Routes the cable through a body-local point. Material can transfer between the adjacent spans according to the local bend angle and friction coefficient. |
|
Routes the cable around a circular roller. Tangent points and wrap length are updated from the current body poses. |
For a roller guide, offset is the roller center, axis is the roller axis,
radius is its contact radius, and orientation selects the winding side
(1 or -1). The friction coefficient mu limits the adjacent tension
ratio using the capstan relation
T_tight / T_slack <= exp(mu * theta)
where theta is the current wrap angle. A zero coefficient permits free slip;
larger values increasingly resist material transfer.
Dynamic routing#
Setting dynamic=True on an internal roller guide lets the solver activate or
bypass that roller from the accepted route geometry at the start of each time
step. orientation determines which signed side of the neighboring bypass
span engages the roller. tendon_activation_tol on the solver adds a
radius-relative activation gap to prevent state chattering near tangency.
Dynamic routing currently supports isolated rolling candidates only. Consecutive dynamic roller guides, endpoint candidates, and route reordering are not supported.
Solver controls#
Both tendon-capable solvers expose these controls:
tendon_max_sweeps: maximum capstan material-relaxation sweeps per solver iteration.tendon_settle_tol: relative tension-change tolerance for stopping those sweeps early.tendon_activation_tol: radius-relative activation gap for dynamic rollers. The default is2.0e-3.
XPBD solves tendon stretch and rolling slip as constraint rows. VBD evaluates
the same routed material state as body force and positive-semidefinite local
Hessian contributions. VBD approximates zero segment compliance with
1.0e-8 m/N.
XPBD applies joint_linear_relaxation to tendon stretch and rolling-slip
corrections. As with other parallel XPBD constraints, the value may need
adjustment for strongly coupled routes.
Reading tension (experimental diagnostics)#
The solver-owned arrays tendon_segment_material_tension and
tendon_segment_damping_tension report the constitutive and signed damping
components after a step. The instantaneous unilateral axial tension is
max(material_tension + damping_tension, 0). VBD does not solve XPBD multipliers, so
tendon_segment_lambda is zero on the VBD path.
These arrays are debugging/analysis outputs, not a stable solver-neutral
observation API on State or Model. Read them
after step; they are not prescribed forces or actuator inputs. For a dynamic
route, use solver.tendon_segment_active to filter bypassed segment slots and
solver.tendon_segment_active_guide_l/r to identify the current span endpoints.
Calling .numpy() synchronizes the device and belongs outside captured steps.
Choosing rods or tendons#
Use a massless tendon for a prescribed route through rigid-body guides when
cable inertia, bending stiffness, and free cable contact are negligible. Use
Rod with add_rod() when the cable’s own
shape and inertia matter, including bending, twisting, sagging, and contact along
its length. A tendon does not exert a bending force while slack or create new
guides when it touches arbitrary geometry. A roller’s radius describes its
routing geometry, not the cable thickness.
MuJoCo compatibility#
There is currently no automatic lowering between native Model.tendon_*
data and model.mujoco.tendon_*. Existing MJCF/provider tendons remain a separate
path; choosing SolverMuJoCo does not translate a native routed tendon.
The following is a prospective mapping, not a claim of identical solver results:
Native feature |
Potential MuJoCo mapping |
Fidelity |
|---|---|---|
End anchors and frictionless pinholes |
Spatial-tendon sites |
Exact point geometry. |
Circular roller in a fixed plane |
Cylinder wrapping geom, with side selection |
Conditional geometric match; routing conventions need validation. |
Per-span compliance and damping |
Whole-tendon stiffness, damping, and spring length |
Approximation; no general one-to-one material-state mapping. |
Guide capstan friction and rolling material transport |
No equivalent in tendon |
Unsupported as an exact mapping. |
Dynamic routing and activation gap |
Optional geom wrapping |
Different switching rules; not exact. |
Internal anchors |
Separate tendon paths |
Requires splitting the native route. |
Fixed joint-coordinate tendons or pulley divisors |
Existing MuJoCo provider path |
Outside this native guide model. |
MuJoCo’s spatial routes use sites and sphere/cylinder wrapping geoms; successive wrapping geoms require intervening sites. Its pulley element branches a path—it is not a native circular roller. See the MuJoCo tendon reference and wrapping overview. A future converter should start with the compatible frictionless geometry subset and reject unsupported semantics rather than silently discard them.
Current limitations#
Tendons are open ordered routes; branching and cyclic routes are not represented.
Roller guides use circular profiles.
Segment compliance applies to the straight free spans. Wrapped arc length is included in material transport but is not independently stretchable.
Free-span rest lengths are limited to at least
1.0e-6 mduring material transfer and dynamic-route transitions.Fixed roller wraps must remain in
[0, pi]. Zero wrap is a valid tangent boundary; crossing below zero or beyond pi is unsupported and emits a diagnostic from the accepted pose. The diagnostic does not repair the route.Dynamic candidates cannot change their authored order or attach between non-neighboring guides.
Native tendon data is not imported from USD/MuJoCo.
Tendon actuation is not part of
Control, and tendon diagnostics are solver-owned rather than part ofState.The tendon path is not differentiable.
State and runtime updates#
Tendons use native attribute domains TENDON, TENDON_GUIDE, and
TENDON_SEGMENT (one fewer segment than guides per route). add_tendon() accepts
label and route-level custom_attributes. Each TendonGuide accepts guide-level
custom_attributes and incoming-span segment_custom_attributes. Declare these
attributes with the corresponding frequency before authoring routes. Integer
custom references may target tendon, tendon_guide, or tendon_segment, as well
as existing domains such as body. Builder merging/replication preserves the
domains and remaps references; label prefixes apply to tendon names too.
Tendon routes currently cannot be partitioned by SolverCoupled or compacted
into partial ModelViews. These operations reject tendon-bearing models rather
than exposing unremapped references. Use a complete model with XPBD or VBD.
Persistent tendon history belongs to State: material rest-length
distribution, active guides, and previous body-local attachment points. Copying
a state with State.assign() copies this history too. The first solver step
initializes a new state’s tendon history from the authored route. Solver-owned
tensions, multipliers, and temporary routing arrays are diagnostics/workspace,
not the source of a trajectory’s material state.
solver.reset(state, world_mask=...) restores the selected worlds’ initial
material distribution and rebaselines attachment history at the supplied pose.
It is an initialization operation, not a checkpoint restore: use State.assign()
to restore saved tendon history. When reusing VBD after a trajectory discontinuity,
clear its non-tendon solver history first with solver.reset(state, flags=0), then
call state.assign(checkpoint) to restore the checkpoint, including tendon history.
After editing segment compliance/damping or guide friction in place, call
solver.notify_model_changed(ModelFlags.TENDON_PROPERTIES). These edits preserve
existing material history. Changing Model.tendon_segment_rest_length instead updates
the authored reset baseline: notify the solver, then reset a trajectory to apply
the new lengths. Initialized states keep their existing material distribution
until reset. This is not a tendon actuation interface. Negative
authored rest lengths are remeasured from the model’s initial pose. Notifications
must run outside CUDA graph capture; in-place edits and resets preserve the buffer
addresses used by captured steps. Replacing model arrays requires recapturing graphs.
Changing route topology or guide geometry requires rebuilding the model and solver.
References#
Müller, M., Chentanez, N., Jeschke, S., and Macklin, M. Cable Joints. Computer Graphics Forum 37(8), 2018.