Threadlike actuation¶
Threadlike actuation models fixed paths routed through the material-frame cross-section of a continuum robot. A routing may span any contiguous inclusive range of body segments. The same geometry supports pulling tendons, pushing rods, simplified contractile muscles, and pressure chambers represented by an equivalent axial volume coordinate.
Following the general threadlike-routing formulation of Renda et al. (2022), each actuator is represented by a path embedded in the continuum body. SoRoMoX specifies the path by its material-frame offset \(d(s)\) from the backbone and its derivative with respect to arc length. The host combines these routing fields with its backbone kinematics when evaluating path length and the moment matrix.
The transmission model assumes that:
- the routing is fixed in the material frame and depends only on arc length;
- the actuator coordinate is determined by path length, scaled by a constant effective area for the pressure-chamber preset; and
- each path carries one scalar work-conjugate effort, uniform along its route.
Consequently, configuration- or time-dependent rerouting, changes in chamber cross-section, along-path friction or slack, and internal actuator dynamics are outside the transmission model. Representing these effects requires an extended routing, transmission, or effort model.
Threadlike components require continuum segment topology and the host's native
path-integration hooks. PCS, PlanarPCS, and GVS provide this contract.
Articulated hosts do not, so installing a threadlike component on them raises a
descriptive TypeError during construction.
Routing¶
ThreadlikeRouting stores a vectorized family of paths. Linear paths use
in the local material frame. The intercept and slope arrays have shape
(num_paths, 3), with the final axis ordered along the local material-frame
axes. SoRoMoX aligns the backbone with the local x axis; the local-x
component of both arrays must therefore be zero, while the y and z
components locate the path in the cross-section. A (3,) vector defines a
single path.
from soromox.actuation import ThreadlikeRouting
routing = ThreadlikeRouting.linear(
intercept=jnp.array([[0.0, 0.01, 0.0], [0.0, -0.01, 0.0]]),
slope=jnp.zeros((2, 3)),
start_segment_index=(0, 0),
end_segment_index=(1, 1),
)
Planar PCS uses the local y offset and requires the local x and z
components to be zero.
Custom routing laws¶
The continuum hosts depend only on the routing contract, not on the linear parameterization. A custom fixed material-frame routing supplies an immutable parameter type plus offset and arc-length-derivative functions:
import equinox as eqx
import jax
import jax.numpy as jnp
from soromox.actuation import BaseThreadlikeRoutingParams, ThreadlikeRouting
class SinusoidalRoutingParams(BaseThreadlikeRoutingParams):
amplitude: jax.Array
frequency: jax.Array
start_segment_index: tuple[int, ...] = eqx.field(static=True)
end_segment_index: tuple[int, ...] = eqx.field(static=True)
@property
def num_paths(self):
return self.amplitude.shape[0]
def validate(self):
if self.frequency.shape != self.amplitude.shape:
raise ValueError("frequency must match amplitude")
def offset(params, s):
y = params.amplitude * jnp.sin(params.frequency * s)
return jnp.stack((jnp.zeros_like(y), y, jnp.zeros_like(y)), axis=-1)
def derivative(params, s):
y_s = params.amplitude * params.frequency * jnp.cos(params.frequency * s)
return jnp.stack((jnp.zeros_like(y_s), y_s, jnp.zeros_like(y_s)), axis=-1)
routing = ThreadlikeRouting(
params=SinusoidalRoutingParams(
amplitude=jnp.array([0.01]),
frequency=jnp.array([20.0]),
start_segment_index=(0,),
end_segment_index=(1,),
),
offset_fn=offset,
derivative_fn=derivative,
)
PCS, PlanarPCS, and GVS evaluate these two methods through the same host
integration hooks. Custom parameter types must retain the common contiguous
start_segment_index / end_segment_index topology contract.
Modality presets¶
Raw path length is ℓ. Presets select the signed or scaled coordinate while positive input always means the named physical action.
| Preset | Coordinate | Positive effort | Unit |
|---|---|---|---|
tendons |
\(-\ell\) | tension/pulling | N |
push_rods |
\(+\ell\) | compression/pushing | N |
muscles |
\(-\ell\) | contraction/tension | N |
pressure_chambers |
\(A_\mathrm{eff}\ell\) | pressure | Pa |
The pressure coordinate is volume (V=A_\mathrm{eff}\ell), so pressure and volume remain a work-conjugate pair.
from soromox.actuation import ThreadlikeActuator
from soromox.systems import PCS
actuator = ThreadlikeActuator.tendons(routing)
robot = PCS(body_params, structure=structure, actuators=actuator)
The same construction works with PlanarPCS and GVS. Mixed modalities are an
ordered tuple:
robot = PCS(
body_params,
structure=structure,
actuators=(
ThreadlikeActuator.tendons(tendon_routing),
ThreadlikeActuator.push_rods(rod_routing),
),
)
Geometry and work coordinates¶
Raw geometry is intentionally separate from the signed/scaled actuator coordinate:
actuator = robot.actuators[0]
lengths = actuator.path_lengths(robot, q)
length_rates = actuator.path_velocities(robot, q, qd)
points = actuator.path_poses(robot, q, s)
coordinates = robot.actuator_coordinates(q)
For tendons, coordinates == -lengths; for a pressure preset, coordinates are
equivalent volumes. In every case,
jax.jacrev(robot.actuator_coordinates)(q) == robot.actuation_matrix(q).T up
to quadrature tolerance.
PCS integrates the local path basis and performs the constant-strain projection once. GVS applies its strain basis inside the existing quadrature. The public behavior is common, while each host keeps its native evaluation path.
Passive paths¶
Use ThreadlikeImpedance for routed spring-damper mechanics:
from soromox.actuation import ThreadlikeImpedance
passive = ThreadlikeImpedance(
routing=passive_routing,
stiffness=jnp.array([50.0]),
damping=jnp.array([2.0]),
rest_length=jnp.array([0.19]),
)
robot = PCS(
body_params,
structure=structure,
actuators=actuator,
passive_elements=(passive,),
)
The passive element contributes to elastic force, damping, and elastic energy; it never consumes an active control channel.
Updating routing and actuator parameters¶
Updates follow the same shallow immutable replacement pattern as robot body parameters:
params = robot.actuators[0].params
routing_params = params.transmission.routing.replace(
intercept=new_intercepts,
)
transmission_params = params.transmission.replace(routing=routing_params)
robot = robot.update_actuator_params(
0,
transmission=transmission_params,
)
Routing coefficients, coordinate scales, effort bounds, and passive mechanical values are updateable. Segment spans and path counts are static topology and require reconstruction. Colors, radii, and line widths belong to renderer configuration rather than actuator parameters.
Rendering¶
The renderer-side actuation adapter converts threadlike path queries into semantic
ActuatorVisualLayer objects with path points, modality kind, and optional input
scalar data. Physics objects do not import renderer primitives or store visual
style. Generic renderers draw swept geometry when a radius is configured and fall
back to polylines otherwise.
from soromox.rendering import ActuatorStyleConfig, RendererColorConfig
visuals = RendererColorConfig(
actuators=ActuatorStyleConfig(
kind_colors={"tendon": (0.85, 0.2, 0.15)},
kind_radii={"tendon": 5e-4},
)
)
renderer.show(q, actuator_inputs=u, color_config=visuals)
I-SUPPORT retains its detailed bellows renderer. Its renderer accepts the same
actuator_inputs= argument and also supports pressures= as a mutually
exclusive convenience alias.
Current limits¶
Threadlike paths are fixed in the material frame, use contiguous segment spans,
and have one constant coordinate scale per path. The current implementation uses
stateless DirectEffort; nonlinear and stateful effort laws are future
extensions.