Skip to content

Generalized-coordinate actuation

Generalized-coordinate transmissions map generalized configurations to work-conjugate actuator coordinates. The generic AffineGeneralizedCoordinateTransmission supports any host with a matching generalized-coordinate dimension. The articulated-tendon presets additionally require serial articulated routing, as provided by Pendulum and ArticulatedSoftRobot.

Affine transmission coordinates

AffineGeneralizedCoordinateTransmission defines

\[ y_\mathrm{a}(q) = R(q-q_\mathrm{ref}) + y_{\mathrm{a},0}, \qquad A = R^\mathsf{T}. \]

Here, each row of \(R\) is a signed actuator routing across the generalized coordinates. The offset \(y_{\mathrm{a},0}\) makes the coordinate value explicit at the reference configuration. The corresponding velocity, generalized force, and power are

\[ \dot y_\mathrm{a}=R\dot q, \qquad \tau=Ae, \qquad \dot q^\mathsf{T}\tau=\dot y_\mathrm{a}^\mathsf{T}e. \]

The generic affine transmission places no tendon-specific constraints on \(R\), so it is also suitable for custom generalized-coordinate work coordinates. For example, the transmission itself can map PCS coordinates when its matrix width matches the PCS degrees of freedom. The articulated-tendon preset adds stronger serial-joint semantics and intentionally rejects continuum hosts.

Signed direct generalized-coordinate effort

AffineGeneralizedCoordinateActuator combines the affine transmission with DirectEffort:

import jax.numpy as jnp

from soromox.actuation import AffineGeneralizedCoordinateActuator

actuator = AffineGeneralizedCoordinateActuator.from_routing(
    jnp.array([[1.0, 0.0, 0.0]]),
    lower_bounds=-10.0,
    upper_bounds=10.0,
    labels=("base_joint_torque",),
    units="N m",
)

The first generalized coordinate receives the signed input directly, while the other coordinates remain passive. More general rows apply a fixed linear combination of generalized efforts. Because DirectEffort returns the supplied control unchanged, tau = R.T @ u and power is preserved exactly. Bounds are descriptive metadata only; saturation belongs in the controller, optimizer, or future effort model rather than being applied implicitly here.

The coordinate_offset parameter specifies the actuator-space coordinate at the reference configuration:

\[ y_\mathrm{a}(q_\mathrm{ref}) = y_{\mathrm{a},0}. \]

It defines the origin of the affine coordinate, while the differential kinematics remain \(A=R^\mathsf{T}\). For an ArticulatedTendonActuator with DirectEffort, the generalized actuation force is \(\tau=R^\mathsf{T}u\). For ArticulatedTendonImpedance, \(y_\mathrm{a}\) is the elastic deformation; consequently, the elastic force at the reference configuration is \(\tau_\mathrm{elastic}=R^\mathsf{T}K y_{\mathrm{a},0}\).

Articulated tendons

ArticulatedTendonActuator interprets \(R\) as a signed tendon-contraction Jacobian. Positive control input is positive tendon tension.

import jax.numpy as jnp

from soromox.actuation import ArticulatedTendonActuator
from soromox.systems import Pendulum

routing = jnp.array([
    [-0.02, -0.02, 0.0],
    [0.02, 0.02, 0.02],
])

actuator = ArticulatedTendonActuator.from_routing(
    routing,
    reference_configuration=jnp.zeros(3),
    coordinate_offset=jnp.array([0.018, 0.025]),
)
robot = Pendulum(params=body_params, actuators=actuator)

Tendon routing rows must be linearly independent and may not skip an intermediate joint: after a tendon stops crossing joints, its remaining routing entries must be zero. These checks belong to the tendon preset; the generic affine transmission does not impose them.

Multiple actuator families can be installed as an ordered tuple. Their control slices, coordinates, moment-matrix columns, and metadata follow tuple order.

Passive tendon impedance

Passive routed spring-damper mechanics are represented independently of active control:

from soromox.actuation import ArticulatedTendonImpedance

passive = ArticulatedTendonImpedance.from_routing(
    routing,
    stiffness=jnp.array([80.0, 80.0]),
    damping=jnp.array([1.5, 1.5]),
    reference_configuration=jnp.zeros(3),
    coordinate_offset=jnp.array([0.018, 0.025]),
)

robot = Pendulum(
    params=body_params,
    actuators=actuator,
    passive_elements=(passive,),
)

For diagonal actuator-space stiffness (K) and damping (D),

\[ E=\tfrac12 y_\mathrm{a}^\mathsf{T}K y_\mathrm{a}, \qquad \tau_\mathrm{elastic}=R^\mathsf{T}K y_\mathrm{a}, \qquad D_q=R^\mathsf{T}DR. \]

Body stiffness and damping remain part of the articulated host. Passive-element contributions are composed with them in elastic force, damping, and energy; stiffness_matrix continues to describe the body alone.

Optional generalized velocity

The common calls are

yad = robot.actuator_velocities(q, qd)
effort = robot.actuator_efforts(q, u, qd=qd)
tau = robot.actuation_force(q, u, qd=qd)

The qd argument is optional for effort and generalized-force evaluation. If it is omitted, zero generalized velocity is used. This is exactly equivalent for DirectEffort, which returns the control without using coordinate or velocity. Velocity-dependent effort models require the actual qd; forward dynamics supplies it automatically.

Passive damping remains separate and contributes through passive_damping_matrix(q) @ qd.

Parameter updates

Body, actuator, and passive-element parameters are updated independently:

robot = robot.update_params(mass=new_mass)

params = robot.actuators[0].params
transmission = params.transmission.replace(
    coordinate_offset=new_coordinate_offset,
)
robot = robot.update_actuator_params(0, transmission=transmission)

robot = robot.update_passive_element_params(0, stiffness=new_stiffness)

Numeric values can be replaced immutably. Changing the transmission type, channel count, or routing-matrix shape changes structural topology and requires reconstructing the component and robot.