Skip to content

Index

runtime

ActuatorControl

Bases: Load

Drives an actuator's control input each timestep, writing into mjData.ctrl. Use control_func for an arbitrary control law, or the constant factory for a simple, runtime-mutable set point.

name instance-attribute

Python
name: str

Unique label used for telemetry column naming and duplicate-registration warnings.

active class-attribute instance-attribute

Python
active: bool = True

When False the load is suppressed.

actuator instance-attribute

Python
actuator: SerializeAsAny[ActuatorBase]

The MJCF actuator this load drives.

control_func class-attribute instance-attribute

Python
control_func: Callable[
    [UserData | None, MjState], float
] = lambda ud, s: 0.0

Callable (user_data, state) -> control value written into mjData.ctrl for this actuator.

user_data class-attribute instance-attribute

Python
user_data: SerializeAsAny[UserData] | None = None

Optional strongly-typed payload accessible inside control_func. Passed through unchanged each timestep.

get_visuals

Python
get_visuals(state: MjState) -> list[ArrowConfig]

Returns a list of arrow configurations for the renderer.

Source code in src/mujoco_mojo/runtime/load.py
Python
def get_visuals(self, state: MjState) -> list[ArrowConfig]:
    """Returns a list of arrow configurations for the renderer."""
    return []

resolve_ids

Python
resolve_ids(state: MjState) -> None

Caches the actuator ID from the compiled MuJoCo model.

Source code in src/mujoco_mojo/runtime/load.py
Python
def resolve_ids(self, state: MjState) -> None:
    """Caches the actuator ID from the compiled MuJoCo model."""
    self._aid = self.actuator.get_id(state.model)

apply_load

Python
apply_load(state: MjState) -> None

Evaluates control_func and writes the result into mjData.ctrl[self._aid].

Source code in src/mujoco_mojo/runtime/load.py
Python
def apply_load(self, state: MjState) -> None:
    """Evaluates `control_func` and writes the result into `mjData.ctrl[self._aid]`."""
    if not self.active:
        self._last_ctrl = 0.0
        return

    self._last_ctrl = float(self.control_func(self.user_data, state))
    state.data.ctrl[self._aid] = self._last_ctrl

request

Python
request(
    signal_manager: SignalManager | None = None,
    metadata: dict[str, dict[str, Any]] | None = None,
) -> None

Registers the applied control value for logging under Loads/<name>:ctrl.

ctrl's units depend on the driven actuator's transmission and gear/dyntype (the same ambiguity as ActuatorBase.request()'s ctrl channel), so no built-in metadata default is applied. Supply metadata={"ctrl": {...}} yourself if you know it.

Parameters:

Name Type Description Default
signal_manager SignalManager

Manager to register the sampler with. If omitted, the SignalManager of the active RuntimeManager with block is used. If that RuntimeManager has no SignalManager configured, this is a no-op.

None
metadata dict[str, dict[str, Any]] | None

Metadata for the ctrl channel.

None
Source code in src/mujoco_mojo/runtime/load.py
Python
def request(
    self,
    signal_manager: SignalManager | None = None,
    metadata: dict[str, dict[str, Any]] | None = None,
) -> None:
    """
    Registers the applied control value for logging under `Loads/<name>:ctrl`.

    `ctrl`'s units depend on the driven actuator's transmission and `gear`/`dyntype` (the same ambiguity as `ActuatorBase.request()`'s `ctrl` channel), so no built-in metadata default is applied. Supply `metadata={"ctrl": {...}}` yourself if you know it.

    Args:
        signal_manager (SignalManager): Manager to register the sampler with. If omitted, the `SignalManager` of the active `RuntimeManager` `with` block is used. If that `RuntimeManager` has no `SignalManager` configured, this is a no-op.
        metadata: Metadata for the `ctrl` channel.

    """
    from mujoco_mojo.runtime.signal_manager import resolve_signal_manager

    signal_manager = resolve_signal_manager(signal_manager)
    if signal_manager is None:
        return

    def sample(state: MjState) -> None:
        signal_manager.post(
            value=self._last_ctrl if self.active else 0.0,
            category=SignalCategory.LOADS,
            subgroups=(self.name,),
            attr="ctrl",
            metadata=merge_signal_metadata(None, "ctrl", metadata),
        )

    signal_manager.register_sampler(sample)

constant classmethod

Python
constant(
    name: str,
    actuator: ActuatorBase,
    value: float | NamedValue[float],
) -> Self

Drives the actuator with a fixed set point.

Parameters:

Name Type Description Default
name str

Load name used for telemetry column labeling.

required
actuator ActuatorBase

The MJCF actuator to drive.

required
value float | NamedValue[float]

Control value written to mjData.ctrl every timestep. Accepts NamedValue[float] for runtime mutation.

required
Source code in src/mujoco_mojo/runtime/load.py
Python
@classmethod
def constant(
    cls,
    name: str,
    actuator: ActuatorBase,
    value: float | NamedValue[float],
) -> Self:
    """
    Drives the actuator with a fixed set point.

    Args:
        name (str): Load name used for telemetry column labeling.
        actuator (ActuatorBase): The MJCF actuator to drive.
        value (float | NamedValue[float]): Control value written to `mjData.ctrl` every timestep. Accepts `NamedValue[float]` for runtime mutation.

    """

    def func(ud: UserData | None, state: MjState) -> float:
        return value.value if isinstance(value, NamedValue) else value

    return cls(name=name, actuator=actuator, control_func=func)

BodyReactionForce

Bases: SiteLoad

Load that also applies an equal and opposite reaction to a second body. If xtion_body is None, only the action site receives the force.

name instance-attribute

Python
name: str

Unique label used for telemetry column naming and duplicate-registration warnings.

active class-attribute instance-attribute

Python
active: bool = True

When False the load is suppressed.

action_site instance-attribute

Python
action_site: AnySite

Site where the force is applied. Its parent body receives the generalized force.

rel_to_site class-attribute instance-attribute

Python
rel_to_site: AnySite | None = None

Coordinate frame for force/torque components returned by calculate(). When None, components are in the world frame.

user_data class-attribute instance-attribute

Python
user_data: SerializeAsAny[UserData] | None = None

Optional strongly-typed payload accessible inside calculate(). Passed through unchanged each timestep.

force_length_scale class-attribute instance-attribute

Python
force_length_scale: float | None = None

Length multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_length_scale.

force_width_scale class-attribute instance-attribute

Python
force_width_scale: float | None = None

Width multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_width_scale.

torque_length_scale class-attribute instance-attribute

Python
torque_length_scale: float | None = None

Length multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_length_scale.

torque_width_scale class-attribute instance-attribute

Python
torque_width_scale: float | None = None

Width multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_width_scale.

xtion_body class-attribute instance-attribute

Python
xtion_body: Body | None = None

Body that receives the reaction force. When None, no reaction is applied and the force acts on the world.

get_visuals

Python
get_visuals(state: MjState) -> list[ArrowConfig]

Returns a list of arrow configurations for the renderer.

Source code in src/mujoco_mojo/runtime/load.py
Python
def get_visuals(self, state: MjState) -> list[ArrowConfig]:
    """Returns a list of arrow configurations for the renderer."""
    if not self.active:
        return []

    visuals: list[ArrowConfig] = []
    action_pos = self.action_site.rt_pos(state)

    if self._last_f[3] > 1e-4 and self._vis.action_force:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_f[:3],
                color=Color[self._vis.action_force].rgba,
                is_torque=False,
                length_scale=self.force_length_scale
                if self.force_length_scale is not None
                else self._vis.force_length_scale,
                width_scale=self.force_width_scale
                if self.force_width_scale is not None
                else self._vis.force_width_scale,
            )
        )

    if self._last_t[3] > 1e-4 and self._vis.torque:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_t[:3],
                color=Color[self._vis.torque].rgba,
                is_torque=True,
                length_scale=self.torque_length_scale
                if self.torque_length_scale is not None
                else self._vis.torque_length_scale,
                width_scale=self.torque_width_scale
                if self.torque_width_scale is not None
                else self._vis.torque_width_scale,
            )
        )

    return visuals

calculate abstractmethod

Python
calculate(state: MjState) -> tuple[ndarray, ndarray]

Calculate the force for the timestep.

Parameters:

Name Type Description Default
state MjState

The paired MuJoCo model and data instance.

required

Returns:

Type Description
tuple[ndarray, ndarray]

tuple[np.ndarray, np.ndarray]: The force and toque vector output.

Source code in src/mujoco_mojo/runtime/load.py
Python
@abstractmethod
def calculate(self, state: MjState) -> tuple[np.ndarray, np.ndarray]:
    """
    Calculate the force for the timestep.

    Args:
        state: The paired MuJoCo model and data instance.

    Returns:
        tuple[np.ndarray, np.ndarray]: The force and toque vector output.

    """

request

Python
request(
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[
        Literal["force", "torque"], dict[str, Any] | None
    ] = ["force", "torque"],
)

Registers specific channels for logging.

Channel Description Type
force applied force in the world frame xyzm
torque applied torque in the world frame xyzm

Each channel is posted under subgroups=(load_name, channel).

  • An xyzm is a cartesian vector, posted as 4 values (x, y, z, and its magnitude m).

Each signal is tagged with built-in dimension metadata for its channel (force as force, torque as torque).

If signal_manager is omitted, the SignalManager of the active RuntimeManager with block is used. If that RuntimeManager has no SignalManager configured, this is a no-op.

Parameters:

Name Type Description Default
signal_manager SignalManager | None

The signal manager to register the sampler with.

None
channels list[Literal['force', 'torque']] | dict[Literal['force', 'torque'], dict[str, Any] | None]

The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or None) to select channels and attach per-channel metadata in one step.

['force', 'torque']
Source code in src/mujoco_mojo/runtime/load.py
Python
def request(
    self,
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[Literal["force", "torque"], dict[str, Any] | None] = ["force", "torque"],
):
    """
    Registers specific channels for logging.

    | Channel  | Description                        | Type |
    |:---------|:-----------------------------------|:-----|
    | `force`  | applied force in the world frame   | xyzm |
    | `torque` | applied torque in the world frame  | xyzm |

    Each channel is posted under `subgroups=(load_name, channel)`.

    * An `xyzm` is a cartesian vector, posted as 4 values (`x`, `y`, `z`, and its magnitude `m`).

    Each signal is tagged with built-in `dimension` metadata for its channel (`force` as force, `torque` as torque).

    If `signal_manager` is omitted, the `SignalManager` of the active `RuntimeManager` `with` block is used. If that `RuntimeManager` has no `SignalManager` configured, this is a no-op.

    Args:
        signal_manager: The signal manager to register the sampler with.
        channels: The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or `None`) to select channels and attach per-channel metadata in one step.

    """
    from mujoco_mojo.runtime.signal_manager import resolve_signal_manager

    signal_manager = resolve_signal_manager(signal_manager)
    if signal_manager is None:
        return

    if isinstance(channels, dict):
        _meta = cast("dict[str, dict[str, Any] | None]", channels)
        channels = list(channels.keys())
    else:
        _meta = {}

    channel_metadata = {
        "force": dim(Dimension.FORCE),
        "torque": torque_metadata(),
    }

    def sample(state: MjState):
        for channel in channels:
            source = self._last_f if channel == "force" else self._last_t
            meta = merge_signal_metadata(
                channel_metadata.get(channel), channel, _meta, unit_system=state.us
            )

            # iterate through x, y, z, and magnitude (pop. pop.)
            for i, attr in enumerate("xyzm"):
                signal_manager.post(
                    value=float(source[i]) if self.active else 0.0,
                    category=SignalCategory.LOADS,
                    # nest the force/torque under the function name
                    subgroups=(f"{self.name}", channel),
                    attr=attr,
                    metadata=meta,
                )

    signal_manager.register_sampler(sample)

GeneralLoad

Bases: VectorForce, VectorTorque

Full 6-DOF force and torque with independent callables for each component. Combines VectorForce and VectorTorque into a single load.

name instance-attribute

Python
name: str

Unique label used for telemetry column naming and duplicate-registration warnings.

active class-attribute instance-attribute

Python
active: bool = True

When False the load is suppressed.

action_site instance-attribute

Python
action_site: AnySite

Site where the force is applied. Its parent body receives the generalized force.

rel_to_site class-attribute instance-attribute

Python
rel_to_site: AnySite | None = None

Coordinate frame for force/torque components returned by calculate(). When None, components are in the world frame.

user_data class-attribute instance-attribute

Python
user_data: SerializeAsAny[UserData] | None = None

Optional strongly-typed payload accessible inside calculate(). Passed through unchanged each timestep.

force_length_scale class-attribute instance-attribute

Python
force_length_scale: float | None = None

Length multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_length_scale.

force_width_scale class-attribute instance-attribute

Python
force_width_scale: float | None = None

Width multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_width_scale.

torque_length_scale class-attribute instance-attribute

Python
torque_length_scale: float | None = None

Length multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_length_scale.

torque_width_scale class-attribute instance-attribute

Python
torque_width_scale: float | None = None

Width multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_width_scale.

xtion_body class-attribute instance-attribute

Python
xtion_body: Body | None = None

Body that receives the reaction force. When None, no reaction is applied and the force acts on the world.

tx class-attribute instance-attribute

Python
tx: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> X-axis torque component.

ty class-attribute instance-attribute

Python
ty: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> Y-axis torque component.

tz class-attribute instance-attribute

Python
tz: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> Z-axis torque component.

fx class-attribute instance-attribute

Python
fx: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> X-axis force component (N).

fy class-attribute instance-attribute

Python
fy: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> Y-axis force component (N).

fz class-attribute instance-attribute

Python
fz: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> Z-axis force component (N).

get_visuals

Python
get_visuals(state: MjState) -> list[ArrowConfig]

Returns a list of arrow configurations for the renderer.

Source code in src/mujoco_mojo/runtime/load.py
Python
def get_visuals(self, state: MjState) -> list[ArrowConfig]:
    """Returns a list of arrow configurations for the renderer."""
    if not self.active:
        return []

    visuals: list[ArrowConfig] = []
    action_pos = self.action_site.rt_pos(state)

    if self._last_f[3] > 1e-4 and self._vis.action_force:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_f[:3],
                color=Color[self._vis.action_force].rgba,
                is_torque=False,
                length_scale=self.force_length_scale
                if self.force_length_scale is not None
                else self._vis.force_length_scale,
                width_scale=self.force_width_scale
                if self.force_width_scale is not None
                else self._vis.force_width_scale,
            )
        )

    if self._last_t[3] > 1e-4 and self._vis.torque:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_t[:3],
                color=Color[self._vis.torque].rgba,
                is_torque=True,
                length_scale=self.torque_length_scale
                if self.torque_length_scale is not None
                else self._vis.torque_length_scale,
                width_scale=self.torque_width_scale
                if self.torque_width_scale is not None
                else self._vis.torque_width_scale,
            )
        )

    return visuals

request

Python
request(
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[
        Literal["force", "torque"], dict[str, Any] | None
    ] = ["force", "torque"],
)

Registers specific channels for logging.

Channel Description Type
force applied force in the world frame xyzm
torque applied torque in the world frame xyzm

Each channel is posted under subgroups=(load_name, channel).

  • An xyzm is a cartesian vector, posted as 4 values (x, y, z, and its magnitude m).

Each signal is tagged with built-in dimension metadata for its channel (force as force, torque as torque).

If signal_manager is omitted, the SignalManager of the active RuntimeManager with block is used. If that RuntimeManager has no SignalManager configured, this is a no-op.

Parameters:

Name Type Description Default
signal_manager SignalManager | None

The signal manager to register the sampler with.

None
channels list[Literal['force', 'torque']] | dict[Literal['force', 'torque'], dict[str, Any] | None]

The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or None) to select channels and attach per-channel metadata in one step.

['force', 'torque']
Source code in src/mujoco_mojo/runtime/load.py
Python
def request(
    self,
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[Literal["force", "torque"], dict[str, Any] | None] = ["force", "torque"],
):
    """
    Registers specific channels for logging.

    | Channel  | Description                        | Type |
    |:---------|:-----------------------------------|:-----|
    | `force`  | applied force in the world frame   | xyzm |
    | `torque` | applied torque in the world frame  | xyzm |

    Each channel is posted under `subgroups=(load_name, channel)`.

    * An `xyzm` is a cartesian vector, posted as 4 values (`x`, `y`, `z`, and its magnitude `m`).

    Each signal is tagged with built-in `dimension` metadata for its channel (`force` as force, `torque` as torque).

    If `signal_manager` is omitted, the `SignalManager` of the active `RuntimeManager` `with` block is used. If that `RuntimeManager` has no `SignalManager` configured, this is a no-op.

    Args:
        signal_manager: The signal manager to register the sampler with.
        channels: The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or `None`) to select channels and attach per-channel metadata in one step.

    """
    from mujoco_mojo.runtime.signal_manager import resolve_signal_manager

    signal_manager = resolve_signal_manager(signal_manager)
    if signal_manager is None:
        return

    if isinstance(channels, dict):
        _meta = cast("dict[str, dict[str, Any] | None]", channels)
        channels = list(channels.keys())
    else:
        _meta = {}

    channel_metadata = {
        "force": dim(Dimension.FORCE),
        "torque": torque_metadata(),
    }

    def sample(state: MjState):
        for channel in channels:
            source = self._last_f if channel == "force" else self._last_t
            meta = merge_signal_metadata(
                channel_metadata.get(channel), channel, _meta, unit_system=state.us
            )

            # iterate through x, y, z, and magnitude (pop. pop.)
            for i, attr in enumerate("xyzm"):
                signal_manager.post(
                    value=float(source[i]) if self.active else 0.0,
                    category=SignalCategory.LOADS,
                    # nest the force/torque under the function name
                    subgroups=(f"{self.name}", channel),
                    attr=attr,
                    metadata=meta,
                )

    signal_manager.register_sampler(sample)

JointFriction

Bases: JointLoad

Joint friction with a pluggable formulation; use the class-method factories to construct.

Works for hinge, slide, and ball joints. The friction_func receives the full generalized velocity vector for the joint's DOFs and returns a generalized force vector of the same length. All built-in formulations use the velocity magnitude as the scalar speed and apply the resulting force magnitude along -vel/|vel|, so they generalize naturally to multi-DOF joints. Use a NamedValue inside the closure to make parameters mutable at runtime.

name instance-attribute

Python
name: str

Unique label used for telemetry column naming and duplicate-registration warnings.

active class-attribute instance-attribute

Python
active: bool = True

When False the load is suppressed.

joint instance-attribute

Python
joint: Joint

The MJCF joint this load acts on.

friction_func instance-attribute

Python
friction_func: Callable[[ndarray, MjState], ndarray]

(vel, state) -> friction force/torque vector in generalized coordinates. Constructed by the named class-method factories.

resolve_ids

Python
resolve_ids(state: MjState) -> None

Caches the joint ID, DOF address, and DOF count from the compiled MuJoCo model.

Source code in src/mujoco_mojo/runtime/load.py
Python
def resolve_ids(self, state: MjState) -> None:
    """Caches the joint ID, DOF address, and DOF count from the compiled MuJoCo model."""
    self._jid = self.joint.get_id(state.model)
    jnt_type = state.model.jnt_type[self._jid]
    match jnt_type:
        case mujoco.mjtJoint.mjJNT_HINGE | mujoco.mjtJoint.mjJNT_SLIDE:
            self._nv = 1
        case mujoco.mjtJoint.mjJNT_BALL:
            self._nv = 3
        case _:
            msg = f"JointLoad '{self.name}': joint '{self.joint.name}' must be hinge, slide, or ball (got type {jnt_type})"
            logger.error(msg)
            raise ValueError(msg)
    self._dof_adr = int(state.model.jnt_dofadr[self._jid])
    logger.debug(
        f"resolved joint '{self.joint.name}' to dof_adr={self._dof_adr} nv={self._nv}",
    )

get_visuals

Python
get_visuals(state: MjState) -> list[ArrowConfig]

Returns a list of arrow configurations for the renderer.

Source code in src/mujoco_mojo/runtime/load.py
Python
def get_visuals(self, state: MjState) -> list[ArrowConfig]:
    """Returns a list of arrow configurations for the renderer."""
    return []

apply_load

Python
apply_load(state: MjState) -> None

Evaluates friction_func at the current joint velocity vector and accumulates the result into qfrc_applied.

Source code in src/mujoco_mojo/runtime/load.py
Python
def apply_load(self, state: MjState) -> None:
    """Evaluates `friction_func` at the current joint velocity vector and accumulates the result into `qfrc_applied`."""
    if not self.active or self._dof_adr < 0:
        self._last_force = np.zeros(3)
        return

    vel = np.array(state.data.qvel[self._dof_adr : self._dof_adr + self._nv])
    frc = self.friction_func(vel, state)
    state.data.qfrc_applied[self._dof_adr : self._dof_adr + self._nv] += frc
    self._last_force = self._to_world_frc(frc, state)

request

Python
request(
    signal_manager: SignalManager | None = None,
    metadata: dict[str, dict[str, Any]] | None = None,
) -> None

Registers specific channels for logging.

Channel Description Type
friction applied friction force/torque in the world frame xyzm

Each channel is posted under subgroups=(load_name, channel).

  • An xyzm is a cartesian vector, posted as 4 values (x, y, z, and its magnitude m).

friction is tagged with built-in dimension metadata resolved from the joint's type (force for slide, torque for hinge/ball).

Parameters:

Name Type Description Default
signal_manager SignalManager

Manager to register the sampler with. If omitted, the SignalManager of the active RuntimeManager with block is used. If that RuntimeManager has no SignalManager configured, this is a no-op.

None
metadata dict[str, dict[str, Any]] | None

Metadata overriding or extending the built-in default for the friction channel.

None

Raises:

Type Description
ValueError

If the joint has no name.

Source code in src/mujoco_mojo/runtime/load.py
Python
def request(
    self,
    signal_manager: SignalManager | None = None,
    metadata: dict[str, dict[str, Any]] | None = None,
) -> None:
    """
    Registers specific channels for logging.

    | Channel    | Description                                       | Type |
    |:-----------|:--------------------------------------------------|:-----|
    | `friction` | applied friction force/torque in the world frame  | xyzm |

    Each channel is posted under `subgroups=(load_name, channel)`.

    * An `xyzm` is a cartesian vector, posted as 4 values (`x`, `y`, `z`, and its magnitude `m`).

    `friction` is tagged with built-in `dimension` metadata resolved from the joint's type (force for slide, torque for hinge/ball).

    Args:
        signal_manager (SignalManager): Manager to register the sampler with. If omitted, the `SignalManager` of the active `RuntimeManager` `with` block is used. If that `RuntimeManager` has no `SignalManager` configured, this is a no-op.
        metadata: Metadata overriding or extending the built-in default for the `friction` channel.

    Raises:
        ValueError: If the joint has no name.

    """
    from mujoco_mojo.runtime.signal_manager import resolve_signal_manager

    signal_manager = resolve_signal_manager(signal_manager)
    if signal_manager is None:
        return

    if self.joint.name is None:
        msg = f"Cannot request telemetry for JointFriction '{self.name}': joint has no name."
        logger.error(msg)
        raise ValueError(msg)

    def sample(state: MjState) -> None:
        jnt_id = self.joint.get_id(state.model)
        jnt_type = int(state.model.jnt_type[jnt_id])
        meta = merge_signal_metadata(
            force_or_torque(jnt_type), "friction", metadata, unit_system=state.us
        )

        frc = self._last_force if self.active else np.zeros(3)
        for v, attr in zip(frc, ("x", "y", "z")):
            signal_manager.post(
                value=float(v),
                category=SignalCategory.LOADS,
                subgroups=(self.name, "friction"),
                attr=attr,
                metadata=meta,
            )
        signal_manager.post(
            value=float(np.linalg.norm(frc)),
            category=SignalCategory.LOADS,
            subgroups=(self.name, "friction"),
            attr="m",
            metadata=meta,
        )

    signal_manager.register_sampler(sample)

coulomb_simple classmethod

Python
coulomb_simple(
    name: str,
    joint: Joint,
    magnitude: float | NamedValue[float],
) -> Self

Constant-magnitude Coulomb (dry) friction opposing motion. Zero force at standstill. Good for brake pads, dry contacts, and cable friction.

"Simple" because the magnitude is a fixed number you choose, not derived from any actual load on the joint. See karnopp for friction that responds to the joint's real bearing/pin reaction load.

Parameters:

Name Type Description Default
name str

Load name used for telemetry column labeling.

required
joint Joint

The MJCF joint to act on (slide, hinge, or ball).

required
magnitude float | NamedValue[float]

Friction force or torque magnitude. Accepts NamedValue[float] for runtime mutation.

required
Source code in src/mujoco_mojo/runtime/load.py
Python
@classmethod
def coulomb_simple(
    cls,
    name: str,
    joint: Joint,
    magnitude: float | NamedValue[float],
) -> Self:
    """
    Constant-magnitude Coulomb (dry) friction opposing motion. Zero force at standstill. Good for brake pads, dry contacts, and cable friction.

    "Simple" because the magnitude is a fixed number you choose, not derived from any actual load on the joint. See `karnopp` for friction that responds to the joint's real bearing/pin reaction load.

    Args:
        name (str): Load name used for telemetry column labeling.
        joint (Joint): The MJCF joint to act on (slide, hinge, or ball).
        magnitude (float | NamedValue[float]): Friction force or torque magnitude. Accepts `NamedValue[float]` for runtime mutation.

    """

    def func(vel: np.ndarray, state: MjState) -> np.ndarray:
        speed = float(np.linalg.norm(vel))
        return (
            -float(magnitude) * vel / speed if speed > 1e-9 else np.zeros_like(vel)
        )

    return cls(name=name, joint=joint, friction_func=func)

viscous_simple classmethod

Python
viscous_simple(
    name: str,
    joint: Joint,
    damping: float | NamedValue[float],
) -> Self

Velocity-proportional viscous damping. Force is continuous through zero with no discontinuity at standstill. Good for grease-lubricated joints and fluid drag.

Parameters:

Name Type Description Default
name str

Load name used for telemetry column labeling.

required
joint Joint

The MJCF joint to act on (slide, hinge, or ball).

required
damping float | NamedValue[float]

Damping coefficient (force per velocity, or torque per angular velocity). Accepts NamedValue[float] for runtime mutation.

required
Source code in src/mujoco_mojo/runtime/load.py
Python
@classmethod
def viscous_simple(
    cls,
    name: str,
    joint: Joint,
    damping: float | NamedValue[float],
) -> Self:
    """
    Velocity-proportional viscous damping. Force is continuous through zero with no discontinuity at standstill. Good for grease-lubricated joints and fluid drag.

    Args:
        name (str): Load name used for telemetry column labeling.
        joint (Joint): The MJCF joint to act on (slide, hinge, or ball).
        damping (float | NamedValue[float]): Damping coefficient (force per velocity, or torque per angular velocity). Accepts `NamedValue[float]` for runtime mutation.

    """

    def func(vel: np.ndarray, state: MjState) -> np.ndarray:
        return -float(damping) * vel

    return cls(name=name, joint=joint, friction_func=func)

coulomb_viscous_simple classmethod

Python
coulomb_viscous_simple(
    name: str,
    joint: Joint,
    coulomb: float | NamedValue[float],
    viscous: float | NamedValue[float],
) -> Self

Coulomb and viscous friction combined, both at fixed magnitudes you choose. Constant sliding friction plus a velocity-proportional drag term.

"Simple" because neither term is derived from any actual load on the joint. See karnopp for friction that responds to the joint's real bearing/pin reaction load.

Parameters:

Name Type Description Default
name str

Load name used for telemetry column labeling.

required
joint Joint

The MJCF joint to act on (slide, hinge, or ball).

required
coulomb float | NamedValue[float]

Coulomb friction force or torque magnitude. Accepts NamedValue[float] for runtime mutation.

required
viscous float | NamedValue[float]

Viscous damping coefficient (force per velocity, or torque per angular velocity). Accepts NamedValue[float] for runtime mutation.

required
Source code in src/mujoco_mojo/runtime/load.py
Python
@classmethod
def coulomb_viscous_simple(
    cls,
    name: str,
    joint: Joint,
    coulomb: float | NamedValue[float],
    viscous: float | NamedValue[float],
) -> Self:
    """
    Coulomb and viscous friction combined, both at fixed magnitudes you choose. Constant sliding friction plus a velocity-proportional drag term.

    "Simple" because neither term is derived from any actual load on the joint. See `karnopp` for friction that responds to the joint's real bearing/pin reaction load.

    Args:
        name (str): Load name used for telemetry column labeling.
        joint (Joint): The MJCF joint to act on (slide, hinge, or ball).
        coulomb (float | NamedValue[float]): Coulomb friction force or torque magnitude. Accepts `NamedValue[float]` for runtime mutation.
        viscous (float | NamedValue[float]): Viscous damping coefficient (force per velocity, or torque per angular velocity). Accepts `NamedValue[float]` for runtime mutation.

    """

    def func(vel: np.ndarray, state: MjState) -> np.ndarray:
        speed = float(np.linalg.norm(vel))
        coulomb_term = (
            -float(coulomb) * vel / speed if speed > 1e-9 else np.zeros_like(vel)
        )
        return coulomb_term - float(viscous) * vel

    return cls(name=name, joint=joint, friction_func=func)

stribeck_simple classmethod

Python
stribeck_simple(
    name: str,
    joint: Joint,
    coulomb: float | NamedValue[float],
    static: float | NamedValue[float],
    stribeck_velocity: float | NamedValue[float],
    viscous: float | NamedValue[float] = 0.0,
) -> Self

Full Stribeck friction model, at fixed magnitudes you choose. Friction peaks at standstill, drops to the kinetic level as motion begins, then rises with speed. Best for brake and clutch models where stick-slip matters.

F = -(coulomb + (static - coulomb) * exp(-|v| / stribeck_velocity) + viscous * |v|) * v / |v|

Parameters:

Name Type Description Default
name str

Load name used for telemetry column labeling.

required
joint Joint

The MJCF joint to act on (slide, hinge, or ball).

required
coulomb float | NamedValue[float]

Kinetic friction force or torque magnitude. Accepts NamedValue[float].

required
static float | NamedValue[float]

Peak static friction force or torque at zero velocity. Must be >= coulomb. Accepts NamedValue[float].

required
stribeck_velocity float | NamedValue[float]

Characteristic velocity at which friction transitions from static to kinetic. Smaller values give a sharper transition. Accepts NamedValue[float].

required
viscous float | NamedValue[float]

Velocity-proportional damping (force per velocity, or torque per angular velocity). Defaults to 0. Accepts NamedValue[float].

0.0
Source code in src/mujoco_mojo/runtime/load.py
Python
@classmethod
def stribeck_simple(
    cls,
    name: str,
    joint: Joint,
    coulomb: float | NamedValue[float],
    static: float | NamedValue[float],
    stribeck_velocity: float | NamedValue[float],
    viscous: float | NamedValue[float] = 0.0,
) -> Self:
    """
    Full Stribeck friction model, at fixed magnitudes you choose. Friction peaks at standstill, drops to the kinetic level as motion begins, then rises with speed. Best for brake and clutch models where stick-slip matters.

    F = -(coulomb + (static - coulomb) * exp(-|v| / stribeck_velocity) + viscous * |v|) * v / |v|

    Args:
        name (str): Load name used for telemetry column labeling.
        joint (Joint): The MJCF joint to act on (slide, hinge, or ball).
        coulomb (float | NamedValue[float]): Kinetic friction force or torque magnitude. Accepts `NamedValue[float]`.
        static (float | NamedValue[float]): Peak static friction force or torque at zero velocity. Must be >= `coulomb`. Accepts `NamedValue[float]`.
        stribeck_velocity (float | NamedValue[float]): Characteristic velocity at which friction transitions from static to kinetic. Smaller values give a sharper transition. Accepts `NamedValue[float]`.
        viscous (float | NamedValue[float], optional): Velocity-proportional damping (force per velocity, or torque per angular velocity). Defaults to 0. Accepts `NamedValue[float]`.

    """

    def func(vel: np.ndarray, state: MjState) -> np.ndarray:
        speed = float(np.linalg.norm(vel))
        if speed < 1e-9:
            return np.zeros_like(vel)
        fc, fs = float(coulomb), float(static)
        vs, fv = float(stribeck_velocity), float(viscous)
        mag = fc + (fs - fc) * np.exp(-speed / max(vs, 1e-9)) + fv * speed
        return -mag * vel / speed

    return cls(name=name, joint=joint, friction_func=func)

karnopp classmethod

Python
karnopp(
    name: str,
    joint: Joint,
    mu_kinetic: float | NamedValue[float],
    mu_static: float | NamedValue[float],
    velocity_threshold: float | NamedValue[float],
    viscous: float | NamedValue[float] = 0.0,
) -> Self

Karnopp friction whose normal force is the joint's actual bearing/pin reaction load (Joint.rt_bearing_load), not a fixed magnitude. Responds to ordinary side loads (e.g. a body's weight pressing on a hinge pin, or a side load on a slider) not just a number you pick. Properly distinguishes "stuck" from "sliding" instead of picking a coefficient based on speed alone:

  • Sliding (|v| >= velocity_threshold): ordinary kinetic Coulomb friction, F = -mu_kinetic * bearing_load * v/|v|, plus an optional viscous term.
  • Stuck (|v| < velocity_threshold): rather than picking a direction from a near-zero (and possibly noisy) velocity, friction is set to exactly cancel whatever other smooth force is currently acting on the joint (qfrc_smooth: passive + actuator + applied + bias), clamped to the static limit mu_static * bearing_load. If the driving force exceeds that limit the joint breaks away and friction saturates at the limit, opposing the driving force.

This avoids the chattering a pure velocity-direction switch can produce right at standstill, since the held force no longer depends on the sign of a near-zero, noisy velocity.

The bearing load comes from Joint.rt_cfrc_int, which refreshes itself on demand via state.ensure_rne_post_constraint(), so no extra wiring is needed regardless of how the simulation is driven. Note that qfrc_smooth reflects the previous step's solve, since this step's values are not yet known when loads are applied; this mirrors the existing one-step lag already inherent in using vel to set this step's force.

Parameters:

Name Type Description Default
name str

Load name used for telemetry column labeling.

required
joint Joint

The MJCF joint to act on (slide, hinge, or ball).

required
mu_kinetic float | NamedValue[float]

Friction coefficient applied to the bearing load while sliding. Accepts NamedValue[float].

required
mu_static float | NamedValue[float]

Friction coefficient applied to the bearing load that the joint can hold against before breaking away. Accepts NamedValue[float].

required
velocity_threshold float | NamedValue[float]

Speed below which the joint is considered stuck rather than sliding. Accepts NamedValue[float].

required
viscous float | NamedValue[float]

Velocity-proportional damping added on top of the kinetic term while sliding. Defaults to 0. Accepts NamedValue[float].

0.0
Source code in src/mujoco_mojo/runtime/load.py
Python
@classmethod
def karnopp(
    cls,
    name: str,
    joint: Joint,
    mu_kinetic: float | NamedValue[float],
    mu_static: float | NamedValue[float],
    velocity_threshold: float | NamedValue[float],
    viscous: float | NamedValue[float] = 0.0,
) -> Self:
    """
    Karnopp friction whose normal force is the joint's actual bearing/pin reaction load (`Joint.rt_bearing_load`), not a fixed magnitude. Responds to ordinary side loads (e.g. a body's weight pressing on a hinge pin, or a side load on a slider) not just a number you pick. Properly distinguishes "stuck" from "sliding" instead of picking a coefficient based on speed alone:

    - Sliding (`|v| >= velocity_threshold`): ordinary kinetic Coulomb friction, `F = -mu_kinetic * bearing_load * v/|v|`, plus an optional `viscous` term.
    - Stuck (`|v| < velocity_threshold`): rather than picking a direction from a near-zero (and possibly noisy) velocity, friction is set to exactly cancel whatever other smooth force is currently acting on the joint (`qfrc_smooth`: passive + actuator + applied + bias), clamped to the static limit `mu_static * bearing_load`. If the driving force exceeds that limit the joint breaks away and friction saturates at the limit, opposing the driving force.

    This avoids the chattering a pure velocity-direction switch can produce right at standstill, since the held force no longer depends on the sign of a near-zero, noisy velocity.

    The bearing load comes from `Joint.rt_cfrc_int`, which refreshes itself on demand via `state.ensure_rne_post_constraint()`, so no extra wiring is needed regardless of how the simulation is driven. Note that `qfrc_smooth` reflects the previous step's solve, since this step's values are not yet known when loads are applied; this mirrors the existing one-step lag already inherent in using `vel` to set this step's force.

    Args:
        name (str): Load name used for telemetry column labeling.
        joint (Joint): The MJCF joint to act on (slide, hinge, or ball).
        mu_kinetic (float | NamedValue[float]): Friction coefficient applied to the bearing load while sliding. Accepts `NamedValue[float]`.
        mu_static (float | NamedValue[float]): Friction coefficient applied to the bearing load that the joint can hold against before breaking away. Accepts `NamedValue[float]`.
        velocity_threshold (float | NamedValue[float]): Speed below which the joint is considered stuck rather than sliding. Accepts `NamedValue[float]`.
        viscous (float | NamedValue[float], optional): Velocity-proportional damping added on top of the kinetic term while sliding. Defaults to 0. Accepts `NamedValue[float]`.

    """

    def func(vel: np.ndarray, state: MjState) -> np.ndarray:
        speed = float(np.linalg.norm(vel))
        normal = joint.rt_bearing_load(state)

        if speed < float(velocity_threshold):
            driving = np.array(joint.rt_qfrc_smooth(state))
            drive_mag = float(np.linalg.norm(driving))
            if drive_mag < 1e-9:
                return np.zeros_like(vel)
            max_static = float(mu_static) * normal
            return -min(drive_mag, max_static) * driving / drive_mag

        if speed < 1e-9:
            return np.zeros_like(vel)
        coulomb_term = -float(mu_kinetic) * normal * vel / speed
        return coulomb_term - float(viscous) * vel

    return cls(name=name, joint=joint, friction_func=func)

JointLoad

Bases: Load

Abstract base for loads applied directly in generalized-coordinate (joint) space via qfrc_applied. Subclass and implement apply_load().

name instance-attribute

Python
name: str

Unique label used for telemetry column naming and duplicate-registration warnings.

active class-attribute instance-attribute

Python
active: bool = True

When False the load is suppressed.

joint instance-attribute

Python
joint: Joint

The MJCF joint this load acts on.

get_visuals

Python
get_visuals(state: MjState) -> list[ArrowConfig]

Returns a list of arrow configurations for the renderer.

Source code in src/mujoco_mojo/runtime/load.py
Python
def get_visuals(self, state: MjState) -> list[ArrowConfig]:
    """Returns a list of arrow configurations for the renderer."""
    return []

resolve_ids

Python
resolve_ids(state: MjState) -> None

Caches the joint ID, DOF address, and DOF count from the compiled MuJoCo model.

Source code in src/mujoco_mojo/runtime/load.py
Python
def resolve_ids(self, state: MjState) -> None:
    """Caches the joint ID, DOF address, and DOF count from the compiled MuJoCo model."""
    self._jid = self.joint.get_id(state.model)
    jnt_type = state.model.jnt_type[self._jid]
    match jnt_type:
        case mujoco.mjtJoint.mjJNT_HINGE | mujoco.mjtJoint.mjJNT_SLIDE:
            self._nv = 1
        case mujoco.mjtJoint.mjJNT_BALL:
            self._nv = 3
        case _:
            msg = f"JointLoad '{self.name}': joint '{self.joint.name}' must be hinge, slide, or ball (got type {jnt_type})"
            logger.error(msg)
            raise ValueError(msg)
    self._dof_adr = int(state.model.jnt_dofadr[self._jid])
    logger.debug(
        f"resolved joint '{self.joint.name}' to dof_adr={self._dof_adr} nv={self._nv}",
    )

apply_load abstractmethod

Python
apply_load(state: MjState) -> None

Writes the load contribution into state.data.qfrc_applied.

Source code in src/mujoco_mojo/runtime/load.py
Python
@abstractmethod
def apply_load(self, state: MjState) -> None:
    """Writes the load contribution into `state.data.qfrc_applied`."""

Load

Bases: MojoBaseModel, ABC

Abstract base for all load types. Subclass and implement resolve_ids and apply_load.

name instance-attribute

Python
name: str

Unique label used for telemetry column naming and duplicate-registration warnings.

active class-attribute instance-attribute

Python
active: bool = True

When False the load is suppressed.

resolve_ids abstractmethod

Python
resolve_ids(state: MjState) -> None

Cache any integer IDs from the compiled MuJoCo model.

Source code in src/mujoco_mojo/runtime/load.py
Python
@abstractmethod
def resolve_ids(self, state: MjState) -> None:
    """Cache any integer IDs from the compiled MuJoCo model."""

apply_load abstractmethod

Python
apply_load(state: MjState) -> None

Write the load contribution into the MuJoCo force buffers.

Source code in src/mujoco_mojo/runtime/load.py
Python
@abstractmethod
def apply_load(self, state: MjState) -> None:
    """Write the load contribution into the MuJoCo force buffers."""

get_visuals

Python
get_visuals(state: MjState) -> list[ArrowConfig]

Returns a list of arrow configurations for the renderer.

Source code in src/mujoco_mojo/runtime/load.py
Python
def get_visuals(self, state: MjState) -> list[ArrowConfig]:
    """Returns a list of arrow configurations for the renderer."""
    return []

PointToPointForce

Bases: SiteLoad

Scalar force along the line-of-sight between two sites, with an equal and opposite reaction on xtion_site. Use the class-method factories for standard spring formulations, or supply a custom magnitude_func.

name instance-attribute

Python
name: str

Unique label used for telemetry column naming and duplicate-registration warnings.

active class-attribute instance-attribute

Python
active: bool = True

When False the load is suppressed.

action_site instance-attribute

Python
action_site: AnySite

Site where the force is applied. Its parent body receives the generalized force.

rel_to_site class-attribute instance-attribute

Python
rel_to_site: AnySite | None = None

Coordinate frame for force/torque components returned by calculate(). When None, components are in the world frame.

user_data class-attribute instance-attribute

Python
user_data: SerializeAsAny[UserData] | None = None

Optional strongly-typed payload accessible inside calculate(). Passed through unchanged each timestep.

force_length_scale class-attribute instance-attribute

Python
force_length_scale: float | None = None

Length multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_length_scale.

force_width_scale class-attribute instance-attribute

Python
force_width_scale: float | None = None

Width multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_width_scale.

torque_length_scale class-attribute instance-attribute

Python
torque_length_scale: float | None = None

Length multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_length_scale.

torque_width_scale class-attribute instance-attribute

Python
torque_width_scale: float | None = None

Width multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_width_scale.

xtion_site instance-attribute

Python
xtion_site: AnySite

Site that receives the equal-and-opposite reaction force. Named xtion to avoid ambiguity with rel_to_site.

magnitude_func instance-attribute

Python
magnitude_func: Callable[[UserData | None, MjState], float]

Callable (user_data, state) -> signed force magnitude (N). Positive pushes sites apart.

request

Python
request(
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[
        Literal["force", "torque"], dict[str, Any] | None
    ] = ["force", "torque"],
)

Registers specific channels for logging.

Channel Description Type
force applied force in the world frame xyzm
torque applied torque in the world frame xyzm

Each channel is posted under subgroups=(load_name, channel).

  • An xyzm is a cartesian vector, posted as 4 values (x, y, z, and its magnitude m).

Each signal is tagged with built-in dimension metadata for its channel (force as force, torque as torque).

If signal_manager is omitted, the SignalManager of the active RuntimeManager with block is used. If that RuntimeManager has no SignalManager configured, this is a no-op.

Parameters:

Name Type Description Default
signal_manager SignalManager | None

The signal manager to register the sampler with.

None
channels list[Literal['force', 'torque']] | dict[Literal['force', 'torque'], dict[str, Any] | None]

The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or None) to select channels and attach per-channel metadata in one step.

['force', 'torque']
Source code in src/mujoco_mojo/runtime/load.py
Python
def request(
    self,
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[Literal["force", "torque"], dict[str, Any] | None] = ["force", "torque"],
):
    """
    Registers specific channels for logging.

    | Channel  | Description                        | Type |
    |:---------|:-----------------------------------|:-----|
    | `force`  | applied force in the world frame   | xyzm |
    | `torque` | applied torque in the world frame  | xyzm |

    Each channel is posted under `subgroups=(load_name, channel)`.

    * An `xyzm` is a cartesian vector, posted as 4 values (`x`, `y`, `z`, and its magnitude `m`).

    Each signal is tagged with built-in `dimension` metadata for its channel (`force` as force, `torque` as torque).

    If `signal_manager` is omitted, the `SignalManager` of the active `RuntimeManager` `with` block is used. If that `RuntimeManager` has no `SignalManager` configured, this is a no-op.

    Args:
        signal_manager: The signal manager to register the sampler with.
        channels: The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or `None`) to select channels and attach per-channel metadata in one step.

    """
    from mujoco_mojo.runtime.signal_manager import resolve_signal_manager

    signal_manager = resolve_signal_manager(signal_manager)
    if signal_manager is None:
        return

    if isinstance(channels, dict):
        _meta = cast("dict[str, dict[str, Any] | None]", channels)
        channels = list(channels.keys())
    else:
        _meta = {}

    channel_metadata = {
        "force": dim(Dimension.FORCE),
        "torque": torque_metadata(),
    }

    def sample(state: MjState):
        for channel in channels:
            source = self._last_f if channel == "force" else self._last_t
            meta = merge_signal_metadata(
                channel_metadata.get(channel), channel, _meta, unit_system=state.us
            )

            # iterate through x, y, z, and magnitude (pop. pop.)
            for i, attr in enumerate("xyzm"):
                signal_manager.post(
                    value=float(source[i]) if self.active else 0.0,
                    category=SignalCategory.LOADS,
                    # nest the force/torque under the function name
                    subgroups=(f"{self.name}", channel),
                    attr=attr,
                    metadata=meta,
                )

    signal_manager.register_sampler(sample)

resolve_ids

Python
resolve_ids(state: MjState)

Caches the integer IDs from the compiled MuJoCo model.

Source code in src/mujoco_mojo/runtime/load.py
Python
def resolve_ids(self, state: MjState):
    """Caches the integer IDs from the compiled MuJoCo model."""
    super().resolve_ids(state)
    self.xtion_site.get_id(state.model)
    self._r0_mag = self.action_site.rt_dm(self.xtion_site, state)
    on_resolve = getattr(self.magnitude_func, "on_resolve", None)
    if callable(on_resolve):
        on_resolve(self._r0_mag)

get_visuals

Python
get_visuals(state: MjState) -> list[ArrowConfig]

Returns a list of arrow configurations for the renderer.

Source code in src/mujoco_mojo/runtime/load.py
Python
def get_visuals(self, state: MjState) -> list[ArrowConfig]:
    """Returns a list of arrow configurations for the renderer."""
    if not self.active:
        return []

    visuals = super().get_visuals(state)

    if self._last_f[3] > 1e-4 and self._vis.reaction_force:
        xtion_pos = self.xtion_site.rt_pos(state)
        visuals.append(
            ArrowConfig(
                pos=xtion_pos,
                vec=-self._last_f[:3],
                color=Color[self._vis.reaction_force].rgba,
                is_torque=False,
                length_scale=self.force_length_scale
                if self.force_length_scale is not None
                else self._vis.force_length_scale,
                width_scale=self.force_width_scale
                if self.force_width_scale is not None
                else self._vis.force_width_scale,
            )
        )

    return visuals

ideal_spring classmethod

Python
ideal_spring(
    name: str,
    action_site: AnySite,
    xtion_site: AnySite,
    stiffness: float | NamedValue[float] = 0.0,
    damping: float | NamedValue[float] = 0.0,
    rest_length: float = 0.0,
) -> Self

Bidirectional spring-damper active in both tension and compression. F = -k*(d - d0) - c*v.

Source code in src/mujoco_mojo/runtime/load.py
Python
@classmethod
def ideal_spring(
    cls,
    name: str,
    action_site: AnySite,
    xtion_site: AnySite,
    stiffness: float | NamedValue[float] = 0.0,
    damping: float | NamedValue[float] = 0.0,
    rest_length: float = 0.0,
) -> Self:
    """Bidirectional spring-damper active in both tension and compression. `F = -k*(d - d0) - c*v`."""

    def logic(ud: UserData | None, state: MjState) -> float:
        dr = action_site.rt_displacements(xtion_site, state)
        dist = float(np.linalg.norm(dr))
        unit_vec = dr / dist if dist > 1e-9 else np.zeros(3)
        vel = float(
            np.dot(
                action_site.rt_velocities(xtion_site, state)[3:6],
                unit_vec,
            )
        )
        return _ideal_force_logic(dist, vel, stiffness, damping, rest_length)

    return cls(
        name=name,
        action_site=action_site,
        xtion_site=xtion_site,
        magnitude_func=logic,
    )

stroke_compression_spring classmethod

Python
stroke_compression_spring(
    name: str,
    action_site: AnySite,
    xtion_site: AnySite,
    stiffness: float | NamedValue[float] = 0.0,
    damping: float | NamedValue[float] = 0.0,
    preload: float | NamedValue[float] = 0.0,
    max_stroke: float | NamedValue[float] = 0.1,
) -> Self

Compression spring with a finite stroke; only active while the sites are compressed between rest and (rest + max_stroke). Useful for preloaded gas springs and mechanical end-stops.

Source code in src/mujoco_mojo/runtime/load.py
Python
@classmethod
def stroke_compression_spring(
    cls,
    name: str,
    action_site: AnySite,
    xtion_site: AnySite,
    stiffness: float | NamedValue[float] = 0.0,
    damping: float | NamedValue[float] = 0.0,
    preload: float | NamedValue[float] = 0.0,
    max_stroke: float | NamedValue[float] = 0.1,
) -> Self:
    """Compression spring with a finite stroke; only active while the sites are compressed between rest and (rest + max_stroke). Useful for preloaded gas springs and mechanical end-stops."""

    class _Logic:
        def __init__(self) -> None:
            self._r0: float = 0.0

        def on_resolve(self, r0: float) -> None:
            self._r0 = r0

        def __call__(self, ud: UserData | None, state: MjState) -> float:
            dr = action_site.rt_displacements(xtion_site, state)
            dist = float(np.linalg.norm(dr))
            unit_vec = dr / dist if dist > 1e-9 else np.zeros(3)
            vel = float(
                np.dot(
                    action_site.rt_velocities(xtion_site, state)[3:6],
                    unit_vec,
                )
            )

            k = stiffness.value if isinstance(stiffness, NamedValue) else stiffness
            c = damping.value if isinstance(damping, NamedValue) else damping
            f_0 = preload.value if isinstance(preload, NamedValue) else preload
            d_f = (
                max_stroke.value
                if isinstance(max_stroke, NamedValue)
                else max_stroke
            )

            delta_d = dist - self._r0
            if 0 <= delta_d <= d_f:
                f_mag = f_0 - (k * delta_d) - (c * vel)
                return max(0.0, f_mag)
            return 0.0

    logic = _Logic()

    return cls(
        name=name,
        action_site=action_site,
        xtion_site=xtion_site,
        magnitude_func=logic,
    )

compression_spring classmethod

Python
compression_spring(
    name: str,
    action_site: AnySite,
    xtion_site: AnySite,
    stiffness: float | NamedValue[float] = 0.0,
    damping: float | NamedValue[float] = 0.0,
    rest_length: float = 0.0,
) -> Self

Creates a spring-damper that only acts when compressed (dist < rest_length). Useful for bumpers, feet, push-off springs, or end-stops.

Source code in src/mujoco_mojo/runtime/load.py
Python
@classmethod
def compression_spring(
    cls,
    name: str,
    action_site: AnySite,
    xtion_site: AnySite,
    stiffness: float | NamedValue[float] = 0.0,
    damping: float | NamedValue[float] = 0.0,
    rest_length: float = 0.0,
) -> Self:
    """
    Creates a spring-damper that only acts when compressed (dist < rest_length). Useful for bumpers, feet, push-off springs, or end-stops.
    """

    def logic(ud: UserData | None, state: MjState) -> float:
        dr = action_site.rt_displacements(xtion_site, state)
        dist = float(np.linalg.norm(dr))
        unit_vec = dr / dist if dist > 1e-9 else np.zeros(3)
        vel = float(
            np.dot(
                action_site.rt_velocities(xtion_site, state)[3:6],
                unit_vec,
            )
        )
        if dist < rest_length:
            return _ideal_force_logic(dist, vel, stiffness, damping, rest_length)
        return 0.0

    return cls(
        name=name,
        action_site=action_site,
        xtion_site=xtion_site,
        magnitude_func=logic,
    )

tension_spring classmethod

Python
tension_spring(
    name: str,
    action_site: AnySite,
    xtion_site: AnySite,
    stiffness: float | NamedValue[float] = 0.0,
    damping: float | NamedValue[float] = 0.0,
    rest_length: float = 0.0,
) -> Self

Creates a spring-damper that only acts when extended (dist > rest_length). Useful for cables, bungees, or tendons.

Source code in src/mujoco_mojo/runtime/load.py
Python
@classmethod
def tension_spring(
    cls,
    name: str,
    action_site: AnySite,
    xtion_site: AnySite,
    stiffness: float | NamedValue[float] = 0.0,
    damping: float | NamedValue[float] = 0.0,
    rest_length: float = 0.0,
) -> Self:
    """
    Creates a spring-damper that only acts when extended (dist > rest_length). Useful for cables, bungees, or tendons.
    """

    def logic(ud: UserData | None, state: MjState) -> float:
        dr = action_site.rt_displacements(xtion_site, state)
        dist = float(np.linalg.norm(dr))
        unit_vec = dr / dist if dist > 1e-9 else np.zeros(3)
        vel = float(
            np.dot(
                action_site.rt_velocities(xtion_site, state)[3:6],
                unit_vec,
            )
        )
        if dist > rest_length:
            return _ideal_force_logic(dist, vel, stiffness, damping, rest_length)
        return 0.0

    return cls(
        name=name,
        action_site=action_site,
        xtion_site=xtion_site,
        magnitude_func=logic,
    )

ScalarForce

Bases: BodyReactionForce

Force along the local X-axis of action_site, scaled by scalar_func each timestep.

name instance-attribute

Python
name: str

Unique label used for telemetry column naming and duplicate-registration warnings.

active class-attribute instance-attribute

Python
active: bool = True

When False the load is suppressed.

action_site instance-attribute

Python
action_site: AnySite

Site where the force is applied. Its parent body receives the generalized force.

rel_to_site class-attribute instance-attribute

Python
rel_to_site: AnySite | None = None

Coordinate frame for force/torque components returned by calculate(). When None, components are in the world frame.

user_data class-attribute instance-attribute

Python
user_data: SerializeAsAny[UserData] | None = None

Optional strongly-typed payload accessible inside calculate(). Passed through unchanged each timestep.

force_length_scale class-attribute instance-attribute

Python
force_length_scale: float | None = None

Length multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_length_scale.

force_width_scale class-attribute instance-attribute

Python
force_width_scale: float | None = None

Width multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_width_scale.

torque_length_scale class-attribute instance-attribute

Python
torque_length_scale: float | None = None

Length multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_length_scale.

torque_width_scale class-attribute instance-attribute

Python
torque_width_scale: float | None = None

Width multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_width_scale.

xtion_body class-attribute instance-attribute

Python
xtion_body: Body | None = None

Body that receives the reaction force. When None, no reaction is applied and the force acts on the world.

scalar_func class-attribute instance-attribute

Python
scalar_func: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> signed force magnitude (N). Positive is along the site's +X axis.

get_visuals

Python
get_visuals(state: MjState) -> list[ArrowConfig]

Returns a list of arrow configurations for the renderer.

Source code in src/mujoco_mojo/runtime/load.py
Python
def get_visuals(self, state: MjState) -> list[ArrowConfig]:
    """Returns a list of arrow configurations for the renderer."""
    if not self.active:
        return []

    visuals: list[ArrowConfig] = []
    action_pos = self.action_site.rt_pos(state)

    if self._last_f[3] > 1e-4 and self._vis.action_force:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_f[:3],
                color=Color[self._vis.action_force].rgba,
                is_torque=False,
                length_scale=self.force_length_scale
                if self.force_length_scale is not None
                else self._vis.force_length_scale,
                width_scale=self.force_width_scale
                if self.force_width_scale is not None
                else self._vis.force_width_scale,
            )
        )

    if self._last_t[3] > 1e-4 and self._vis.torque:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_t[:3],
                color=Color[self._vis.torque].rgba,
                is_torque=True,
                length_scale=self.torque_length_scale
                if self.torque_length_scale is not None
                else self._vis.torque_length_scale,
                width_scale=self.torque_width_scale
                if self.torque_width_scale is not None
                else self._vis.torque_width_scale,
            )
        )

    return visuals

request

Python
request(
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[
        Literal["force", "torque"], dict[str, Any] | None
    ] = ["force", "torque"],
)

Registers specific channels for logging.

Channel Description Type
force applied force in the world frame xyzm
torque applied torque in the world frame xyzm

Each channel is posted under subgroups=(load_name, channel).

  • An xyzm is a cartesian vector, posted as 4 values (x, y, z, and its magnitude m).

Each signal is tagged with built-in dimension metadata for its channel (force as force, torque as torque).

If signal_manager is omitted, the SignalManager of the active RuntimeManager with block is used. If that RuntimeManager has no SignalManager configured, this is a no-op.

Parameters:

Name Type Description Default
signal_manager SignalManager | None

The signal manager to register the sampler with.

None
channels list[Literal['force', 'torque']] | dict[Literal['force', 'torque'], dict[str, Any] | None]

The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or None) to select channels and attach per-channel metadata in one step.

['force', 'torque']
Source code in src/mujoco_mojo/runtime/load.py
Python
def request(
    self,
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[Literal["force", "torque"], dict[str, Any] | None] = ["force", "torque"],
):
    """
    Registers specific channels for logging.

    | Channel  | Description                        | Type |
    |:---------|:-----------------------------------|:-----|
    | `force`  | applied force in the world frame   | xyzm |
    | `torque` | applied torque in the world frame  | xyzm |

    Each channel is posted under `subgroups=(load_name, channel)`.

    * An `xyzm` is a cartesian vector, posted as 4 values (`x`, `y`, `z`, and its magnitude `m`).

    Each signal is tagged with built-in `dimension` metadata for its channel (`force` as force, `torque` as torque).

    If `signal_manager` is omitted, the `SignalManager` of the active `RuntimeManager` `with` block is used. If that `RuntimeManager` has no `SignalManager` configured, this is a no-op.

    Args:
        signal_manager: The signal manager to register the sampler with.
        channels: The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or `None`) to select channels and attach per-channel metadata in one step.

    """
    from mujoco_mojo.runtime.signal_manager import resolve_signal_manager

    signal_manager = resolve_signal_manager(signal_manager)
    if signal_manager is None:
        return

    if isinstance(channels, dict):
        _meta = cast("dict[str, dict[str, Any] | None]", channels)
        channels = list(channels.keys())
    else:
        _meta = {}

    channel_metadata = {
        "force": dim(Dimension.FORCE),
        "torque": torque_metadata(),
    }

    def sample(state: MjState):
        for channel in channels:
            source = self._last_f if channel == "force" else self._last_t
            meta = merge_signal_metadata(
                channel_metadata.get(channel), channel, _meta, unit_system=state.us
            )

            # iterate through x, y, z, and magnitude (pop. pop.)
            for i, attr in enumerate("xyzm"):
                signal_manager.post(
                    value=float(source[i]) if self.active else 0.0,
                    category=SignalCategory.LOADS,
                    # nest the force/torque under the function name
                    subgroups=(f"{self.name}", channel),
                    attr=attr,
                    metadata=meta,
                )

    signal_manager.register_sampler(sample)

ScalarTorque

Bases: BodyReactionForce

Torque about the local X-axis of action_site, scaled by scalar_func each timestep.

name instance-attribute

Python
name: str

Unique label used for telemetry column naming and duplicate-registration warnings.

active class-attribute instance-attribute

Python
active: bool = True

When False the load is suppressed.

action_site instance-attribute

Python
action_site: AnySite

Site where the force is applied. Its parent body receives the generalized force.

rel_to_site class-attribute instance-attribute

Python
rel_to_site: AnySite | None = None

Coordinate frame for force/torque components returned by calculate(). When None, components are in the world frame.

user_data class-attribute instance-attribute

Python
user_data: SerializeAsAny[UserData] | None = None

Optional strongly-typed payload accessible inside calculate(). Passed through unchanged each timestep.

force_length_scale class-attribute instance-attribute

Python
force_length_scale: float | None = None

Length multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_length_scale.

force_width_scale class-attribute instance-attribute

Python
force_width_scale: float | None = None

Width multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_width_scale.

torque_length_scale class-attribute instance-attribute

Python
torque_length_scale: float | None = None

Length multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_length_scale.

torque_width_scale class-attribute instance-attribute

Python
torque_width_scale: float | None = None

Width multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_width_scale.

xtion_body class-attribute instance-attribute

Python
xtion_body: Body | None = None

Body that receives the reaction force. When None, no reaction is applied and the force acts on the world.

scalar_func class-attribute instance-attribute

Python
scalar_func: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> signed torque magnitude. Positive follows the right-hand rule about the site's +X axis.

get_visuals

Python
get_visuals(state: MjState) -> list[ArrowConfig]

Returns a list of arrow configurations for the renderer.

Source code in src/mujoco_mojo/runtime/load.py
Python
def get_visuals(self, state: MjState) -> list[ArrowConfig]:
    """Returns a list of arrow configurations for the renderer."""
    if not self.active:
        return []

    visuals: list[ArrowConfig] = []
    action_pos = self.action_site.rt_pos(state)

    if self._last_f[3] > 1e-4 and self._vis.action_force:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_f[:3],
                color=Color[self._vis.action_force].rgba,
                is_torque=False,
                length_scale=self.force_length_scale
                if self.force_length_scale is not None
                else self._vis.force_length_scale,
                width_scale=self.force_width_scale
                if self.force_width_scale is not None
                else self._vis.force_width_scale,
            )
        )

    if self._last_t[3] > 1e-4 and self._vis.torque:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_t[:3],
                color=Color[self._vis.torque].rgba,
                is_torque=True,
                length_scale=self.torque_length_scale
                if self.torque_length_scale is not None
                else self._vis.torque_length_scale,
                width_scale=self.torque_width_scale
                if self.torque_width_scale is not None
                else self._vis.torque_width_scale,
            )
        )

    return visuals

request

Python
request(
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[
        Literal["force", "torque"], dict[str, Any] | None
    ] = ["force", "torque"],
)

Registers specific channels for logging.

Channel Description Type
force applied force in the world frame xyzm
torque applied torque in the world frame xyzm

Each channel is posted under subgroups=(load_name, channel).

  • An xyzm is a cartesian vector, posted as 4 values (x, y, z, and its magnitude m).

Each signal is tagged with built-in dimension metadata for its channel (force as force, torque as torque).

If signal_manager is omitted, the SignalManager of the active RuntimeManager with block is used. If that RuntimeManager has no SignalManager configured, this is a no-op.

Parameters:

Name Type Description Default
signal_manager SignalManager | None

The signal manager to register the sampler with.

None
channels list[Literal['force', 'torque']] | dict[Literal['force', 'torque'], dict[str, Any] | None]

The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or None) to select channels and attach per-channel metadata in one step.

['force', 'torque']
Source code in src/mujoco_mojo/runtime/load.py
Python
def request(
    self,
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[Literal["force", "torque"], dict[str, Any] | None] = ["force", "torque"],
):
    """
    Registers specific channels for logging.

    | Channel  | Description                        | Type |
    |:---------|:-----------------------------------|:-----|
    | `force`  | applied force in the world frame   | xyzm |
    | `torque` | applied torque in the world frame  | xyzm |

    Each channel is posted under `subgroups=(load_name, channel)`.

    * An `xyzm` is a cartesian vector, posted as 4 values (`x`, `y`, `z`, and its magnitude `m`).

    Each signal is tagged with built-in `dimension` metadata for its channel (`force` as force, `torque` as torque).

    If `signal_manager` is omitted, the `SignalManager` of the active `RuntimeManager` `with` block is used. If that `RuntimeManager` has no `SignalManager` configured, this is a no-op.

    Args:
        signal_manager: The signal manager to register the sampler with.
        channels: The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or `None`) to select channels and attach per-channel metadata in one step.

    """
    from mujoco_mojo.runtime.signal_manager import resolve_signal_manager

    signal_manager = resolve_signal_manager(signal_manager)
    if signal_manager is None:
        return

    if isinstance(channels, dict):
        _meta = cast("dict[str, dict[str, Any] | None]", channels)
        channels = list(channels.keys())
    else:
        _meta = {}

    channel_metadata = {
        "force": dim(Dimension.FORCE),
        "torque": torque_metadata(),
    }

    def sample(state: MjState):
        for channel in channels:
            source = self._last_f if channel == "force" else self._last_t
            meta = merge_signal_metadata(
                channel_metadata.get(channel), channel, _meta, unit_system=state.us
            )

            # iterate through x, y, z, and magnitude (pop. pop.)
            for i, attr in enumerate("xyzm"):
                signal_manager.post(
                    value=float(source[i]) if self.active else 0.0,
                    category=SignalCategory.LOADS,
                    # nest the force/torque under the function name
                    subgroups=(f"{self.name}", channel),
                    attr=attr,
                    metadata=meta,
                )

    signal_manager.register_sampler(sample)

SiteLoad

Bases: Load

Abstract base for Cartesian-space forcing functions applied at a named site. Subclass and implement calculate() to return (force_world, torque_world); the base class handles mj_applyFT, telemetry, and visualization.

name instance-attribute

Python
name: str

Unique label used for telemetry column naming and duplicate-registration warnings.

active class-attribute instance-attribute

Python
active: bool = True

When False the load is suppressed.

action_site instance-attribute

Python
action_site: AnySite

Site where the force is applied. Its parent body receives the generalized force.

rel_to_site class-attribute instance-attribute

Python
rel_to_site: AnySite | None = None

Coordinate frame for force/torque components returned by calculate(). When None, components are in the world frame.

user_data class-attribute instance-attribute

Python
user_data: SerializeAsAny[UserData] | None = None

Optional strongly-typed payload accessible inside calculate(). Passed through unchanged each timestep.

force_length_scale class-attribute instance-attribute

Python
force_length_scale: float | None = None

Length multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_length_scale.

force_width_scale class-attribute instance-attribute

Python
force_width_scale: float | None = None

Width multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_width_scale.

torque_length_scale class-attribute instance-attribute

Python
torque_length_scale: float | None = None

Length multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_length_scale.

torque_width_scale class-attribute instance-attribute

Python
torque_width_scale: float | None = None

Width multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_width_scale.

resolve_ids

Python
resolve_ids(state: MjState)

Caches the integer IDs from the compiled MuJoCo model.

Source code in src/mujoco_mojo/runtime/load.py
Python
def resolve_ids(self, state: MjState):
    """Caches the integer IDs from the compiled MuJoCo model."""
    self._vis = MujocoMojoSettings().visualization
    self.action_site.get_id(state.model)

    if self.rel_to_site:
        self.rel_to_site.get_id(state.model)

calculate abstractmethod

Python
calculate(state: MjState) -> tuple[ndarray, ndarray]

Calculate the force for the timestep.

Parameters:

Name Type Description Default
state MjState

The paired MuJoCo model and data instance.

required

Returns:

Type Description
tuple[ndarray, ndarray]

tuple[np.ndarray, np.ndarray]: The force and toque vector output.

Source code in src/mujoco_mojo/runtime/load.py
Python
@abstractmethod
def calculate(self, state: MjState) -> tuple[np.ndarray, np.ndarray]:
    """
    Calculate the force for the timestep.

    Args:
        state: The paired MuJoCo model and data instance.

    Returns:
        tuple[np.ndarray, np.ndarray]: The force and toque vector output.

    """

request

Python
request(
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[
        Literal["force", "torque"], dict[str, Any] | None
    ] = ["force", "torque"],
)

Registers specific channels for logging.

Channel Description Type
force applied force in the world frame xyzm
torque applied torque in the world frame xyzm

Each channel is posted under subgroups=(load_name, channel).

  • An xyzm is a cartesian vector, posted as 4 values (x, y, z, and its magnitude m).

Each signal is tagged with built-in dimension metadata for its channel (force as force, torque as torque).

If signal_manager is omitted, the SignalManager of the active RuntimeManager with block is used. If that RuntimeManager has no SignalManager configured, this is a no-op.

Parameters:

Name Type Description Default
signal_manager SignalManager | None

The signal manager to register the sampler with.

None
channels list[Literal['force', 'torque']] | dict[Literal['force', 'torque'], dict[str, Any] | None]

The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or None) to select channels and attach per-channel metadata in one step.

['force', 'torque']
Source code in src/mujoco_mojo/runtime/load.py
Python
def request(
    self,
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[Literal["force", "torque"], dict[str, Any] | None] = ["force", "torque"],
):
    """
    Registers specific channels for logging.

    | Channel  | Description                        | Type |
    |:---------|:-----------------------------------|:-----|
    | `force`  | applied force in the world frame   | xyzm |
    | `torque` | applied torque in the world frame  | xyzm |

    Each channel is posted under `subgroups=(load_name, channel)`.

    * An `xyzm` is a cartesian vector, posted as 4 values (`x`, `y`, `z`, and its magnitude `m`).

    Each signal is tagged with built-in `dimension` metadata for its channel (`force` as force, `torque` as torque).

    If `signal_manager` is omitted, the `SignalManager` of the active `RuntimeManager` `with` block is used. If that `RuntimeManager` has no `SignalManager` configured, this is a no-op.

    Args:
        signal_manager: The signal manager to register the sampler with.
        channels: The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or `None`) to select channels and attach per-channel metadata in one step.

    """
    from mujoco_mojo.runtime.signal_manager import resolve_signal_manager

    signal_manager = resolve_signal_manager(signal_manager)
    if signal_manager is None:
        return

    if isinstance(channels, dict):
        _meta = cast("dict[str, dict[str, Any] | None]", channels)
        channels = list(channels.keys())
    else:
        _meta = {}

    channel_metadata = {
        "force": dim(Dimension.FORCE),
        "torque": torque_metadata(),
    }

    def sample(state: MjState):
        for channel in channels:
            source = self._last_f if channel == "force" else self._last_t
            meta = merge_signal_metadata(
                channel_metadata.get(channel), channel, _meta, unit_system=state.us
            )

            # iterate through x, y, z, and magnitude (pop. pop.)
            for i, attr in enumerate("xyzm"):
                signal_manager.post(
                    value=float(source[i]) if self.active else 0.0,
                    category=SignalCategory.LOADS,
                    # nest the force/torque under the function name
                    subgroups=(f"{self.name}", channel),
                    attr=attr,
                    metadata=meta,
                )

    signal_manager.register_sampler(sample)

get_visuals

Python
get_visuals(state: MjState) -> list[ArrowConfig]

Returns a list of arrow configurations for the renderer.

Source code in src/mujoco_mojo/runtime/load.py
Python
def get_visuals(self, state: MjState) -> list[ArrowConfig]:
    """Returns a list of arrow configurations for the renderer."""
    if not self.active:
        return []

    visuals: list[ArrowConfig] = []
    action_pos = self.action_site.rt_pos(state)

    if self._last_f[3] > 1e-4 and self._vis.action_force:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_f[:3],
                color=Color[self._vis.action_force].rgba,
                is_torque=False,
                length_scale=self.force_length_scale
                if self.force_length_scale is not None
                else self._vis.force_length_scale,
                width_scale=self.force_width_scale
                if self.force_width_scale is not None
                else self._vis.force_width_scale,
            )
        )

    if self._last_t[3] > 1e-4 and self._vis.torque:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_t[:3],
                color=Color[self._vis.torque].rgba,
                is_torque=True,
                length_scale=self.torque_length_scale
                if self.torque_length_scale is not None
                else self._vis.torque_length_scale,
                width_scale=self.torque_width_scale
                if self.torque_width_scale is not None
                else self._vis.torque_width_scale,
            )
        )

    return visuals

VectorForce

Bases: BodyReactionForce

3-axis force with independent per-axis callables. Components are expressed in rel_to_site frame, or world frame if None.

name instance-attribute

Python
name: str

Unique label used for telemetry column naming and duplicate-registration warnings.

active class-attribute instance-attribute

Python
active: bool = True

When False the load is suppressed.

action_site instance-attribute

Python
action_site: AnySite

Site where the force is applied. Its parent body receives the generalized force.

rel_to_site class-attribute instance-attribute

Python
rel_to_site: AnySite | None = None

Coordinate frame for force/torque components returned by calculate(). When None, components are in the world frame.

user_data class-attribute instance-attribute

Python
user_data: SerializeAsAny[UserData] | None = None

Optional strongly-typed payload accessible inside calculate(). Passed through unchanged each timestep.

force_length_scale class-attribute instance-attribute

Python
force_length_scale: float | None = None

Length multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_length_scale.

force_width_scale class-attribute instance-attribute

Python
force_width_scale: float | None = None

Width multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_width_scale.

torque_length_scale class-attribute instance-attribute

Python
torque_length_scale: float | None = None

Length multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_length_scale.

torque_width_scale class-attribute instance-attribute

Python
torque_width_scale: float | None = None

Width multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_width_scale.

xtion_body class-attribute instance-attribute

Python
xtion_body: Body | None = None

Body that receives the reaction force. When None, no reaction is applied and the force acts on the world.

fx class-attribute instance-attribute

Python
fx: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> X-axis force component (N).

fy class-attribute instance-attribute

Python
fy: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> Y-axis force component (N).

fz class-attribute instance-attribute

Python
fz: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> Z-axis force component (N).

get_visuals

Python
get_visuals(state: MjState) -> list[ArrowConfig]

Returns a list of arrow configurations for the renderer.

Source code in src/mujoco_mojo/runtime/load.py
Python
def get_visuals(self, state: MjState) -> list[ArrowConfig]:
    """Returns a list of arrow configurations for the renderer."""
    if not self.active:
        return []

    visuals: list[ArrowConfig] = []
    action_pos = self.action_site.rt_pos(state)

    if self._last_f[3] > 1e-4 and self._vis.action_force:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_f[:3],
                color=Color[self._vis.action_force].rgba,
                is_torque=False,
                length_scale=self.force_length_scale
                if self.force_length_scale is not None
                else self._vis.force_length_scale,
                width_scale=self.force_width_scale
                if self.force_width_scale is not None
                else self._vis.force_width_scale,
            )
        )

    if self._last_t[3] > 1e-4 and self._vis.torque:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_t[:3],
                color=Color[self._vis.torque].rgba,
                is_torque=True,
                length_scale=self.torque_length_scale
                if self.torque_length_scale is not None
                else self._vis.torque_length_scale,
                width_scale=self.torque_width_scale
                if self.torque_width_scale is not None
                else self._vis.torque_width_scale,
            )
        )

    return visuals

request

Python
request(
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[
        Literal["force", "torque"], dict[str, Any] | None
    ] = ["force", "torque"],
)

Registers specific channels for logging.

Channel Description Type
force applied force in the world frame xyzm
torque applied torque in the world frame xyzm

Each channel is posted under subgroups=(load_name, channel).

  • An xyzm is a cartesian vector, posted as 4 values (x, y, z, and its magnitude m).

Each signal is tagged with built-in dimension metadata for its channel (force as force, torque as torque).

If signal_manager is omitted, the SignalManager of the active RuntimeManager with block is used. If that RuntimeManager has no SignalManager configured, this is a no-op.

Parameters:

Name Type Description Default
signal_manager SignalManager | None

The signal manager to register the sampler with.

None
channels list[Literal['force', 'torque']] | dict[Literal['force', 'torque'], dict[str, Any] | None]

The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or None) to select channels and attach per-channel metadata in one step.

['force', 'torque']
Source code in src/mujoco_mojo/runtime/load.py
Python
def request(
    self,
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[Literal["force", "torque"], dict[str, Any] | None] = ["force", "torque"],
):
    """
    Registers specific channels for logging.

    | Channel  | Description                        | Type |
    |:---------|:-----------------------------------|:-----|
    | `force`  | applied force in the world frame   | xyzm |
    | `torque` | applied torque in the world frame  | xyzm |

    Each channel is posted under `subgroups=(load_name, channel)`.

    * An `xyzm` is a cartesian vector, posted as 4 values (`x`, `y`, `z`, and its magnitude `m`).

    Each signal is tagged with built-in `dimension` metadata for its channel (`force` as force, `torque` as torque).

    If `signal_manager` is omitted, the `SignalManager` of the active `RuntimeManager` `with` block is used. If that `RuntimeManager` has no `SignalManager` configured, this is a no-op.

    Args:
        signal_manager: The signal manager to register the sampler with.
        channels: The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or `None`) to select channels and attach per-channel metadata in one step.

    """
    from mujoco_mojo.runtime.signal_manager import resolve_signal_manager

    signal_manager = resolve_signal_manager(signal_manager)
    if signal_manager is None:
        return

    if isinstance(channels, dict):
        _meta = cast("dict[str, dict[str, Any] | None]", channels)
        channels = list(channels.keys())
    else:
        _meta = {}

    channel_metadata = {
        "force": dim(Dimension.FORCE),
        "torque": torque_metadata(),
    }

    def sample(state: MjState):
        for channel in channels:
            source = self._last_f if channel == "force" else self._last_t
            meta = merge_signal_metadata(
                channel_metadata.get(channel), channel, _meta, unit_system=state.us
            )

            # iterate through x, y, z, and magnitude (pop. pop.)
            for i, attr in enumerate("xyzm"):
                signal_manager.post(
                    value=float(source[i]) if self.active else 0.0,
                    category=SignalCategory.LOADS,
                    # nest the force/torque under the function name
                    subgroups=(f"{self.name}", channel),
                    attr=attr,
                    metadata=meta,
                )

    signal_manager.register_sampler(sample)

VectorTorque

Bases: BodyReactionForce

3-axis torque with independent per-axis callables. Components are expressed in rel_to_site frame, or world frame if None.

name instance-attribute

Python
name: str

Unique label used for telemetry column naming and duplicate-registration warnings.

active class-attribute instance-attribute

Python
active: bool = True

When False the load is suppressed.

action_site instance-attribute

Python
action_site: AnySite

Site where the force is applied. Its parent body receives the generalized force.

rel_to_site class-attribute instance-attribute

Python
rel_to_site: AnySite | None = None

Coordinate frame for force/torque components returned by calculate(). When None, components are in the world frame.

user_data class-attribute instance-attribute

Python
user_data: SerializeAsAny[UserData] | None = None

Optional strongly-typed payload accessible inside calculate(). Passed through unchanged each timestep.

force_length_scale class-attribute instance-attribute

Python
force_length_scale: float | None = None

Length multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_length_scale.

force_width_scale class-attribute instance-attribute

Python
force_width_scale: float | None = None

Width multiplier for this load's force arrow(s), on top of MuJoCo's native scaling. None falls back to VisualizationSettings.force_width_scale.

torque_length_scale class-attribute instance-attribute

Python
torque_length_scale: float | None = None

Length multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_length_scale.

torque_width_scale class-attribute instance-attribute

Python
torque_width_scale: float | None = None

Width multiplier for this load's torque arrow, on top of MuJoCo's native scaling. None falls back to VisualizationSettings.torque_width_scale.

xtion_body class-attribute instance-attribute

Python
xtion_body: Body | None = None

Body that receives the reaction force. When None, no reaction is applied and the force acts on the world.

tx class-attribute instance-attribute

Python
tx: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> X-axis torque component.

ty class-attribute instance-attribute

Python
ty: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> Y-axis torque component.

tz class-attribute instance-attribute

Python
tz: Callable[[UserData | None, MjState], float] = (
    lambda ud, s: 0.0
)

Callable (user_data, state) -> Z-axis torque component.

get_visuals

Python
get_visuals(state: MjState) -> list[ArrowConfig]

Returns a list of arrow configurations for the renderer.

Source code in src/mujoco_mojo/runtime/load.py
Python
def get_visuals(self, state: MjState) -> list[ArrowConfig]:
    """Returns a list of arrow configurations for the renderer."""
    if not self.active:
        return []

    visuals: list[ArrowConfig] = []
    action_pos = self.action_site.rt_pos(state)

    if self._last_f[3] > 1e-4 and self._vis.action_force:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_f[:3],
                color=Color[self._vis.action_force].rgba,
                is_torque=False,
                length_scale=self.force_length_scale
                if self.force_length_scale is not None
                else self._vis.force_length_scale,
                width_scale=self.force_width_scale
                if self.force_width_scale is not None
                else self._vis.force_width_scale,
            )
        )

    if self._last_t[3] > 1e-4 and self._vis.torque:
        visuals.append(
            ArrowConfig(
                pos=action_pos,
                vec=self._last_t[:3],
                color=Color[self._vis.torque].rgba,
                is_torque=True,
                length_scale=self.torque_length_scale
                if self.torque_length_scale is not None
                else self._vis.torque_length_scale,
                width_scale=self.torque_width_scale
                if self.torque_width_scale is not None
                else self._vis.torque_width_scale,
            )
        )

    return visuals

request

Python
request(
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[
        Literal["force", "torque"], dict[str, Any] | None
    ] = ["force", "torque"],
)

Registers specific channels for logging.

Channel Description Type
force applied force in the world frame xyzm
torque applied torque in the world frame xyzm

Each channel is posted under subgroups=(load_name, channel).

  • An xyzm is a cartesian vector, posted as 4 values (x, y, z, and its magnitude m).

Each signal is tagged with built-in dimension metadata for its channel (force as force, torque as torque).

If signal_manager is omitted, the SignalManager of the active RuntimeManager with block is used. If that RuntimeManager has no SignalManager configured, this is a no-op.

Parameters:

Name Type Description Default
signal_manager SignalManager | None

The signal manager to register the sampler with.

None
channels list[Literal['force', 'torque']] | dict[Literal['force', 'torque'], dict[str, Any] | None]

The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or None) to select channels and attach per-channel metadata in one step.

['force', 'torque']
Source code in src/mujoco_mojo/runtime/load.py
Python
def request(
    self,
    signal_manager: SignalManager | None = None,
    channels: list[Literal["force", "torque"]]
    | dict[Literal["force", "torque"], dict[str, Any] | None] = ["force", "torque"],
):
    """
    Registers specific channels for logging.

    | Channel  | Description                        | Type |
    |:---------|:-----------------------------------|:-----|
    | `force`  | applied force in the world frame   | xyzm |
    | `torque` | applied torque in the world frame  | xyzm |

    Each channel is posted under `subgroups=(load_name, channel)`.

    * An `xyzm` is a cartesian vector, posted as 4 values (`x`, `y`, `z`, and its magnitude `m`).

    Each signal is tagged with built-in `dimension` metadata for its channel (`force` as force, `torque` as torque).

    If `signal_manager` is omitted, the `SignalManager` of the active `RuntimeManager` `with` block is used. If that `RuntimeManager` has no `SignalManager` configured, this is a no-op.

    Args:
        signal_manager: The signal manager to register the sampler with.
        channels: The load data channels to log. Pass a list to select channels, or a dict mapping channel name to metadata overrides (or `None`) to select channels and attach per-channel metadata in one step.

    """
    from mujoco_mojo.runtime.signal_manager import resolve_signal_manager

    signal_manager = resolve_signal_manager(signal_manager)
    if signal_manager is None:
        return

    if isinstance(channels, dict):
        _meta = cast("dict[str, dict[str, Any] | None]", channels)
        channels = list(channels.keys())
    else:
        _meta = {}

    channel_metadata = {
        "force": dim(Dimension.FORCE),
        "torque": torque_metadata(),
    }

    def sample(state: MjState):
        for channel in channels:
            source = self._last_f if channel == "force" else self._last_t
            meta = merge_signal_metadata(
                channel_metadata.get(channel), channel, _meta, unit_system=state.us
            )

            # iterate through x, y, z, and magnitude (pop. pop.)
            for i, attr in enumerate("xyzm"):
                signal_manager.post(
                    value=float(source[i]) if self.active else 0.0,
                    category=SignalCategory.LOADS,
                    # nest the force/torque under the function name
                    subgroups=(f"{self.name}", channel),
                    attr=attr,
                    metadata=meta,
                )

    signal_manager.register_sampler(sample)

RequirementSatisfied

Bases: SimulationStopped

Raised when one or more live requirements with terminate_on_pass=True pass, ending the trial early as a normal (successful) completion rather than a failure.

RequirementTerminated

Bases: SimulationStopped

Raised when one or more live requirements with terminate_on_fail=True fail. Subclasses SimulationStopped so existing catch blocks need no changes.

RuntimeManager dataclass

Python
RuntimeManager(
    signal_manager: SignalManager | None = None,
    loads: list[Load] = list(),
    proximities: list[Proximity] = list(),
    tracers: list[Tracer] = list(),
    video_recorders: list[VideoRecorder] = list(),
    requirements: RequirementsManager = RequirementsManager(),
    playback_speed: float = 1.0,
    _sync_hook: SyncHook | None = None,
    _viewer_lock: Callable[[], AbstractContextManager[Any]]
    | None = None,
    _skip_recording: bool = False,
    _stop_event: Event | None = None,
    _resolved: bool = False,
)

__enter__

Python
__enter__() -> Self

Prime the model and prepare results. Also makes this instance available via RuntimeManager.current() for the duration of the with block.

Source code in src/mujoco_mojo/runtime/runtime_manager.py
Python
def __enter__(self) -> Self:
    """Prime the model and prepare results. Also makes this instance available via `RuntimeManager.current()` for the duration of the `with` block."""
    self._context_token = _current.set(self)
    return self

__exit__

Python
__exit__(exc_type, exc, tb)

Ensure all telemetry is flushed even if the simulation crashed. Also saves recordings

Source code in src/mujoco_mojo/runtime/runtime_manager.py
Python
def __exit__(self, exc_type, exc, tb):
    """Ensure all telemetry is flushed even if the simulation crashed. Also saves recordings"""
    assert self._context_token is not None
    _current.reset(self._context_token)
    self._context_token = None

    if self.signal_manager:
        self.signal_manager.close()

    if self.video_recorders:
        self.save_recordings()

    controlled_exit = exc_type is None or (
        exc_type is not None and issubclass(exc_type, SimulationStopped)
    )
    if (
        controlled_exit
        and self.requirements._requirements
        and self._mojo_model is not None
        and self.signal_manager is not None
        and self._last_state is not None
    ):
        self.requirements.run_end_of_trial_evaluation(
            self._last_state,
            signal_manager=self.signal_manager,
            mojo_model=self._mojo_model,
        )

current classmethod

Python
current() -> RuntimeManager

Returns the RuntimeManager of the innermost enclosing with block. Raises if called outside of one.

Source code in src/mujoco_mojo/runtime/runtime_manager.py
Python
@classmethod
def current(cls) -> RuntimeManager:
    """Returns the `RuntimeManager` of the innermost enclosing `with` block. Raises if called outside of one."""
    try:
        return _current.get()
    except LookupError:
        msg = "No active RuntimeManager context. Call this from within a `with runtime_manager as rm:` block, or pass the runtime_manager/signal_manager explicitly."
        logger.error(msg)
        raise RuntimeError(msg) from None

resolve

Python
resolve(state: MjState)

Call this once after mj_loadXML to prime the caches.

Source code in src/mujoco_mojo/runtime/runtime_manager.py
Python
def resolve(self, state: MjState):
    """Call this once after mj_loadXML to prime the caches."""
    for load in self.loads:
        load.resolve_ids(state)
    self._resolved = True

add_requirement

Python
add_requirement(
    fn: RequirementFn,
    *,
    name: str | None = None,
    every: int | None = None,
    terminate_on_fail: bool = False,
    terminate_on_pass: bool = False,
    latch_on_fail: bool = True,
    latch_on_pass: bool = False,
    post_result: bool = True,
) -> None

Register a named pass/fail check evaluated at end of trial and optionally during simulation.

Parameters:

Name Type Description Default
fn RequirementFn

Callable (MojoModel, MjState, MojoDataFrame | None) -> (passed, message) where passed is True, False, or None (undetermined; only meaningful during live checks).

required
name str | None

Label for this check in results and telemetry. Defaults to fn.__name__ when omitted or empty.

None
every int | None

If None (default), the check runs once at end of trial. If set to N, the check also runs every N steps during simulation, posting a telemetry signal under Requirements/{name}:result.

None
terminate_on_fail bool

When every is set, a failing live check raises RequirementTerminated and the trial is marked TERMINATED (a requirement failure).

False
terminate_on_pass bool

When every is set, a passing live check raises RequirementSatisfied and the trial completes early as a normal success (subject to the other requirements).

False
latch_on_fail bool

When every is set, once a live check fails, fn is never called again (live or end-of-trial); the failing verdict is replayed. Defaults to True since it's a pure compute-saving option: a live failure already dooms the trial regardless of anything that happens afterward, so nothing about the outcome changes. Pass False to keep calling fn after a failure anyway (e.g. for logging/telemetry from later steps).

True
latch_on_pass bool

When every is set, once a live check passes, fn is never called again; the passing verdict is replayed for the rest of the trial. Unlike latch_on_fail, this changes the outcome: without it, a later False would still fail the requirement even after an earlier True (any live failure is sticky); with it, no evaluation after the pass can ever produce that later failure. Defaults to False for this reason.

False
post_result bool

Post a telemetry column to the signal manager after evaluating the requirement if True.

True
Source code in src/mujoco_mojo/runtime/runtime_manager.py
Python
def add_requirement(
    self,
    fn: RequirementFn,
    *,
    name: str | None = None,
    every: int | None = None,
    terminate_on_fail: bool = False,
    terminate_on_pass: bool = False,
    latch_on_fail: bool = True,
    latch_on_pass: bool = False,
    post_result: bool = True,
) -> None:
    """
    Register a named pass/fail check evaluated at end of trial and optionally during simulation.

    Args:
        fn: Callable `(MojoModel, MjState, MojoDataFrame | None) -> (passed, message)` where `passed` is `True`, `False`, or `None` (undetermined; only meaningful during live checks).
        name: Label for this check in results and telemetry. Defaults to `fn.__name__` when omitted or empty.
        every: If `None` (default), the check runs once at end of trial. If set to N, the check also runs every N steps during simulation, posting a telemetry signal under `Requirements/{name}:result`.
        terminate_on_fail: When `every` is set, a failing live check raises `RequirementTerminated` and the trial is marked `TERMINATED` (a requirement failure).
        terminate_on_pass: When `every` is set, a passing live check raises `RequirementSatisfied` and the trial completes early as a normal success (subject to the other requirements).
        latch_on_fail: When `every` is set, once a live check fails, `fn` is never called again (live or end-of-trial); the failing verdict is replayed. Defaults to `True` since it's a pure compute-saving option: a live failure already dooms the trial regardless of anything that happens afterward, so nothing about the outcome changes. Pass `False` to keep calling `fn` after a failure anyway (e.g. for logging/telemetry from later steps).
        latch_on_pass: When `every` is set, once a live check passes, `fn` is never called again; the passing verdict is replayed for the rest of the trial. Unlike `latch_on_fail`, this changes the outcome: without it, a later `False` would still fail the requirement even after an earlier `True` (any live failure is sticky); with it, no evaluation after the pass can ever produce that later failure. Defaults to `False` for this reason.
        post_result: Post a telemetry column to the signal manager after evaluating the requirement if True.

    """
    self.requirements.add(
        fn,
        name=name,
        every=every,
        terminate_on_fail=terminate_on_fail,
        terminate_on_pass=terminate_on_pass,
        latch_on_fail=latch_on_fail,
        latch_on_pass=latch_on_pass,
        post_result=post_result,
    )

requirement

Python
requirement(
    name: str | None = None,
    *,
    every: int | None = None,
    terminate_on_fail: bool = False,
    terminate_on_pass: bool = False,
    latch_on_fail: bool = True,
    latch_on_pass: bool = False,
    post_result: bool = True,
) -> Callable[[RequirementFn], RequirementFn]

Registers a pass/fail check that runs automatically when the trial ends. Results are collected in requirements.results and written to requirements.json alongside the trial telemetry.

The decorated function must accept (mojo_model: MojoModel, state: MjState, df: MojoDataFrame | None) and return (passed, message) where passed is True, False, or None (undetermined yet; a no-op during live checks, a failure if still undetermined at end of trial). mojo_model provides access to user data and named values. state is the last simulation state. df is the full trial telemetry parquet at end-of-trial, or None during live evaluations (see every).

Parameters:

Name Type Description Default
name str | None

Label for this check in results and telemetry. Defaults to the function's __name__.

None
every int | None

When set, the check also runs inside step() every N steps, posting 1.0 (pass) or 0.0 (fail) under Requirements/{name}:result in the telemetry parquet. df is None during these live calls; use state instead.

None
terminate_on_fail bool

When every is set, a failing live check raises RequirementTerminated, unwinds the simulation, and marks the trial TERMINATED.

False
terminate_on_pass bool

When every is set, a passing live check raises RequirementSatisfied, unwinds the simulation, and lets the trial complete early as a normal success (subject to the other requirements).

False
latch_on_fail bool

When every is set, once a live check fails, fn is never called again; the failing verdict is replayed for free. Defaults to True, purely a compute-saving option, since the trial's outcome is already identical to the default sticky-failure behavior regardless of latching. Pass False to keep calling fn after a failure anyway.

True
latch_on_pass bool

When every is set, once a live check passes, fn is never called again; the passing verdict is replayed for the rest of the trial. This is the only way to get "once passed, stays passed" semantics without ending the whole simulation via terminate_on_pass. Defaults to False since, unlike latch_on_fail, it changes the outcome (a later failure would otherwise still be observed and would still fail the requirement).

False
post_result bool

Post a telemetry column to the signal manager after evaluating the requirement if True.

True
Example

@rm.requirement(every=100, terminate_on_fail=True) def upright(mojo_model, state, df): return state.data.qpos[2] > 0.1, "ok"

Source code in src/mujoco_mojo/runtime/runtime_manager.py
Python
def requirement(
    self,
    name: str | None = None,
    *,
    every: int | None = None,
    terminate_on_fail: bool = False,
    terminate_on_pass: bool = False,
    latch_on_fail: bool = True,
    latch_on_pass: bool = False,
    post_result: bool = True,
) -> Callable[[RequirementFn], RequirementFn]:
    """
    Registers a pass/fail check that runs automatically when the trial ends. Results are collected in `requirements.results` and written to `requirements.json` alongside the trial telemetry.

    The decorated function must accept `(mojo_model: MojoModel, state: MjState, df: MojoDataFrame | None)` and return `(passed, message)` where `passed` is `True`, `False`, or `None` (undetermined yet; a no-op during live checks, a failure if still undetermined at end of trial). `mojo_model` provides access to user data and named values. `state` is the last simulation state. `df` is the full trial telemetry parquet at end-of-trial, or `None` during live evaluations (see `every`).

    Args:
        name: Label for this check in results and telemetry. Defaults to the function's `__name__`.
        every: When set, the check also runs inside `step()` every N steps, posting `1.0` (pass) or `0.0` (fail) under `Requirements/{name}:result` in the telemetry parquet. `df` is `None` during these live calls; use `state` instead.
        terminate_on_fail: When `every` is set, a failing live check raises `RequirementTerminated`, unwinds the simulation, and marks the trial `TERMINATED`.
        terminate_on_pass: When `every` is set, a passing live check raises `RequirementSatisfied`, unwinds the simulation, and lets the trial complete early as a normal success (subject to the other requirements).
        latch_on_fail: When `every` is set, once a live check fails, `fn` is never called again; the failing verdict is replayed for free. Defaults to `True`, purely a compute-saving option, since the trial's outcome is already identical to the default sticky-failure behavior regardless of latching. Pass `False` to keep calling `fn` after a failure anyway.
        latch_on_pass: When `every` is set, once a live check passes, `fn` is never called again; the passing verdict is replayed for the rest of the trial. This is the only way to get "once passed, stays passed" semantics without ending the whole simulation via `terminate_on_pass`. Defaults to `False` since, unlike `latch_on_fail`, it changes the outcome (a later failure would otherwise still be observed and would still fail the requirement).
        post_result: Post a telemetry column to the signal manager after evaluating the requirement if True.

    Example:
        `@rm.requirement(every=100, terminate_on_fail=True)`
        `def upright(mojo_model, state, df): return state.data.qpos[2] > 0.1, "ok"`

    """
    return self.requirements.decorator(
        name,
        every=every,
        terminate_on_fail=terminate_on_fail,
        terminate_on_pass=terminate_on_pass,
        latch_on_fail=latch_on_fail,
        latch_on_pass=latch_on_pass,
        post_result=post_result,
    )

last_passed

Python
last_passed(
    name_or_fn: str | RequirementFn, state: MjState
) -> bool | None

Returns the cached live-check result for a requirement at the current sim time, or None if there is no verdict yet.

Accepts either the requirement's name or the function itself. The decorator returns the original function unchanged, so the reference from @rm.requirement(...) can be passed directly instead of retyping its name. Passing a function that was never registered raises ValueError.

None covers two cases callers can't tell apart: the check hasn't run at this exact sim time (every > 1 only evaluates on some steps), or it ran and explicitly returned undetermined (None). Either way, treat None as "no verdict yet", not as failure.

Example
Python
1
2
3
4
5
if rm.last_passed("upright", state) is False:
    apply_recovery()

if rm.last_passed(upright, state) is False:  # or pass the function itself
    apply_recovery()
Source code in src/mujoco_mojo/runtime/runtime_manager.py
Python
def last_passed(
    self, name_or_fn: str | RequirementFn, state: MjState
) -> bool | None:
    """
    Returns the cached live-check result for a requirement at the current sim time, or `None` if there is no verdict yet.

    Accepts either the requirement's name or the function itself. The decorator returns the original function unchanged, so the reference from `@rm.requirement(...)` can be passed directly instead of retyping its name. Passing a function that was never registered raises `ValueError`.

    `None` covers two cases callers can't tell apart: the check hasn't run at this exact sim time (`every > 1` only evaluates on some steps), or it ran and explicitly returned undetermined (`None`). Either way, treat `None` as "no verdict yet", not as failure.

    Example:
        ```python
        if rm.last_passed("upright", state) is False:
            apply_recovery()

        if rm.last_passed(upright, state) is False:  # or pass the function itself
            apply_recovery()
        ```

    """
    return self.requirements.last_passed(name_or_fn, state)

step

Python
step(
    state: MjState,
    clear_xfrc_applied: bool = True,
    clear_qfrc_applied: bool = True,
    clear_ctrl: bool = True,
)

Calculates forces, integratess physics, and handles telemetry.

Parameters:

Name Type Description Default
state MjState

The paired MuJoCo model and data instance.

required
clear_xfrc_applied bool

If True, zero xfrc_applied (external forces) before applying loads.

True
clear_qfrc_applied bool

If True, zero qfrc_applied (user-defined forces) before applying loads.

True
clear_ctrl bool

If True, zero ctrl (actuator controls) before applying loads. Set to False if controls are set externally and should persist across steps, e.g. when not driven by an ActuatorControl every timestep.

True
Source code in src/mujoco_mojo/runtime/runtime_manager.py
Python
def step(
    self,
    state: MjState,
    clear_xfrc_applied: bool = True,
    clear_qfrc_applied: bool = True,
    clear_ctrl: bool = True,
):
    """
    Calculates forces, integratess physics, and handles telemetry.

    Args:
        state: The paired MuJoCo model and data instance.
        clear_xfrc_applied: If True, zero `xfrc_applied` (external forces) before applying loads.
        clear_qfrc_applied: If True, zero `qfrc_applied` (user-defined forces) before applying loads.
        clear_ctrl: If True, zero `ctrl` (actuator controls) before applying loads. Set to False if controls are set externally and should persist across steps, e.g. when not driven by an `ActuatorControl` every timestep.

    """
    logger.debug(f"Computing simulation step t={state.data.time:.6f}")

    if self._stop_event is not None and self._stop_event.is_set():
        logger.debug("'stop_event' is set; raising SimulationStopped")
        raise SimulationStopped("Simulation stopped by user request.")

    # while a live viewer is attached, its GUI thread can read model/data at any
    # time (e.g. re-rendering when the user interacts with the view), so all
    # physics mutations happen under the viewer lock. released again before the
    # sync hook and the pacing sleep so the GUI thread is never starved
    viewer_lock = (
        self._viewer_lock() if self._viewer_lock is not None else nullcontext()
    )
    with viewer_lock:
        # clear buffers for next timestep
        if clear_xfrc_applied:
            state.data.xfrc_applied.fill(0)  # external forces
        if clear_qfrc_applied:
            state.data.qfrc_applied.fill(0)  # user-defined forces
        if clear_ctrl:
            state.data.ctrl.fill(0)  # actuator forces

        # sync state variables and clear render buffer
        # invalidate first: mj_forward changes qacc, which cfrc_int/cacc are derived from
        state.invalidate_rne_post_constraint()
        mujoco.mj_forward(state.model, state.data)

        if state.data.time == 0.0 or self._start_sim_time == 0.0:
            self._start_sim_time = state.data.time
            self._start_wall_time = time.time()

        # resolve IDs and initial distances
        # it is critical this is done after mj_forward to update site positions
        if not self._resolved:
            logger.debug("First step; resolving load IDs")
            self.resolve(state)

        # apply user forcing functions
        for load in self.loads:
            load.apply_load(state)

        # record data
        if self.signal_manager and not self._skip_recording:
            # invalidate first: loads may have changed qfrc_applied/xfrc_applied since the last mj_forward
            state.invalidate_rne_post_constraint()
            mujoco.mj_forward(state.model, state.data)
            self.signal_manager.record(state)

        # record any frames which are due
        all_arrows = None
        all_lines = None
        all_traces = None
        if self.video_recorders or self._sync_hook:
            # gather arrows for forcing functions
            all_arrows: list[ArrowConfig] | None = []
            all_lines: list[LineConfig] | None = []
            all_traces: list[LineConfig] | None = []

            for load in self.loads:
                all_arrows.extend(load.get_visuals(state))

            for proximity in self.proximities:
                visual = proximity.get_visuals(state, self.signal_manager)
                if visual is not None:
                    all_lines.append(visual)

            # tracer.get_visuals() rebuilds its whole trail every call, which is only
            # cheap relative to *rendered frames* (tens per second), not physics steps
            # (potentially thousands per second) - so only pay for it on steps where
            # something will actually consume it. update() stays unconditional so the
            # trail's position history doesn't lose resolution between rendered frames.
            needs_traces = self._sync_hook is not None or any(
                r.is_due(state) for r in self.video_recorders
            )
            for tracer in self.tracers:
                tracer.update(state)
                if needs_traces:
                    all_traces.extend(tracer.get_visuals(state))

        if self.video_recorders:
            assert (
                all_arrows is not None
                and all_lines is not None
                and all_traces is not None
            )
            for recorder in self.video_recorders:
                recorder.capture_frame(
                    state=state,
                    custom_arrows=all_arrows,
                    custom_lines=all_lines,
                    custom_traces=all_traces,
                )

        # integrate physics and advance the time
        state.invalidate_rne_post_constraint()
        mujoco.mj_step(state.model, state.data)
        logger.debug(f"Physics integrated; new t={state.data.time:.6f}")

    if self._sync_hook:
        assert (
            all_arrows is not None
            and all_lines is not None
            and all_traces is not None
        )
        # the live-viewer sync hook has no per-category toggle, so just merge everything it should draw
        self._sync_hook(state, all_arrows, all_lines + all_traces)

    # track last state and evaluate live requirements
    self._last_state = state
    self.requirements.step(
        state,
        signal_manager=self.signal_manager,
        mojo_model=self._mojo_model,
    )

    if self.playback_speed > 0:
        sim_elapsed = state.data.time - self._start_sim_time

        # how much time we want to have passed
        target_wall_elapsed = sim_elapsed / self.playback_speed

        # how much time has actually passed
        actual_wall_elapsed = time.time() - self._start_wall_time

        sleep_time = target_wall_elapsed - actual_wall_elapsed
        if sleep_time > 0:
            logger.debug(
                f"Pacing sleep {sleep_time:.4f}s "
                f"(sim={sim_elapsed:.6f}s, wall={actual_wall_elapsed:.6f}s)"
            )
            if self._stop_event is not None:
                # use wait() instead of sleep() so a stop request interrupts
                # the pacing delay immediately, rather than after it elapses
                self._stop_event.wait(timeout=sleep_time)
            else:
                time.sleep(sleep_time)

SimulationStopped

Bases: Exception

Raised by RuntimeManager.step to unwind a running simulation when the user requests a stop.

SignalManager dataclass

Python
SignalManager(
    export_path: Path,
    target_buffer_bytes: int = 8 * 1024 * 1024,
    record_decimation: int = 1,
    unit_system: UnitSystem | None = None,
    _buffer_row_idx: int = 0,
    _step_count: int = -1,
    _n_cols: int = 0,
)

export_path instance-attribute

Python
export_path: Path

Where the output file should be saved.

target_buffer_bytes class-attribute instance-attribute

Python
target_buffer_bytes: int = 8 * 1024 * 1024

Approximate in-memory buffer size, in bytes, before flushing to a part file. The actual row capacity is derived from this and the current column count (see _recompute_capacity), so flush frequency stays roughly memory/file-size bounded as signals are registered, rather than fixed at a row count regardless of width. Defaults to 8 MB.

record_decimation class-attribute instance-attribute

Python
record_decimation: int = 1

How many steps between each recording should be performed.

unit_system class-attribute instance-attribute

Python
unit_system: UnitSystem | None = None

When set, the time column's concrete unit is resolved from unit_system.time (e.g. "second", "millisecond"). Always tagged with dimension="[time]" regardless.

track

Python
track(
    getter: Callable[[], float],
    category: SignalCategory | str,
    subgroups: tuple[str, ...] = (),
    *,
    attr: str | None = None,
    metadata: dict[str, Any] | None = None,
)

Registers getter to be called and posted on every recorded step, under the same category/subgroups/attr namespace as post.

getter is called fresh on every recorded step, so it should look up a value that changes over the course of the simulation (e.g. a variable updated each step, an attribute, or an indexing operation) rather than a constant computed once. If the underlying value never changes after registration, track will simply keep posting that same value every step.

Examples:

Python Console Session
>>> # Becomes "Custom/MyGroup:value", re-read from `obj.value` every step
>>> manager.track(lambda: obj.value, "Custom", ("MyGroup",), attr="value")
Python Console Session
1
2
3
4
5
6
>>> # Also works: `level` is a variable reassigned each step in the same scope
>>> level = 0.0
>>> manager.track(lambda: level, "Custom", ("MyGroup",), attr="level")
>>> for _ in range(n_steps):
...     level = compute_level(state)
...     rm.step(state)
Source code in src/mujoco_mojo/runtime/signal_manager.py
Python
def track(
    self,
    getter: Callable[[], float],
    category: SignalCategory | str,
    subgroups: tuple[str, ...] = (),
    *,
    attr: str | None = None,
    metadata: dict[str, Any] | None = None,
):
    """
    Registers `getter` to be called and posted on every recorded step, under the same `category`/`subgroups`/`attr` namespace as `post`.

    `getter` is called fresh on every recorded step, so it should look up a value that changes over the course of the simulation (e.g. a variable updated each step, an attribute, or an indexing operation) rather than a constant computed once. If the underlying value never changes after registration, `track` will simply keep posting that same value every step.

    Examples:
        >>> # Becomes "Custom/MyGroup:value", re-read from `obj.value` every step
        >>> manager.track(lambda: obj.value, "Custom", ("MyGroup",), attr="value")

        >>> # Also works: `level` is a variable reassigned each step in the same scope
        >>> level = 0.0
        >>> manager.track(lambda: level, "Custom", ("MyGroup",), attr="level")
        >>> for _ in range(n_steps):
        ...     level = compute_level(state)
        ...     rm.step(state)

    """

    def _sample(_: MjState):
        self.post(getter(), category, subgroups, attr=attr, metadata=metadata)

    self.register_sampler(_sample)

post

Python
post(
    value: float,
    category: SignalCategory | str,
    subgroups: tuple[str, ...] = (),
    *,
    attr: str | None = None,
    metadata: dict[str, Any] | None = None,
)

Injects a value into the telemetry ledger using a hierarchical namespace.

This method constructs a structured key that the dashboard uses to build a navigable tree view. The naming convention follows a folder-like structure to group related signals (e.g., all axes of a body's position).

Format

Category/Subgroup:Attribute (e.g., "Bodies/Link_1:xpos_x")

Parameters:

Name Type Description Default
value float

The numeric data to record.

required
category SignalCategory | str

Top level category (e.g., "Bodies")

required
subgroups tuple[str, ...]

The second-level organizational folders. Defaults to an empty tuple.

()
attr str | None

The specific signal or component name (e.g., "qpos" or "x"). Defaults to None.

None
metadata dict[str, Any] | None

Arbitrary metadata for this signal, persisted into the telemetry file's footer. Only consulted the first time this signal is registered (ignored on later calls for the same signal). Two keys are validated via Pint if present: dimension (e.g. "[length] / [time]"), for tagging the physical quantity type when the concrete unit isn't knowable (the right choice for built-in signals, since the user's modeling unit system isn't known here), and unit (e.g. "meter / second"), for the rarer case the concrete unit truly is known. Any other keys (e.g. display_name, comment) pass through unvalidated. Defaults to None.

None

Examples:

Python Console Session
>>> # Becomes "Bodies/Hand/xpos:x"
>>> manager.post(1.2, SignalCategory.BODIES, ("Hand", "xpos"), "x")
Python Console Session
>>> # Becomes "Sensors/IMU/Accel:z"
>>> manager.post(9.81, "Sensors", ("IMU", "Accel"), attr="z")
Python Console Session
>>> # Tag a custom signal's physical quantity type without committing to a unit system
>>> manager.post(0.4, "Custom", ("Spring",), attr="stiffness", metadata={"dimension": "[force] / [length]"})
Source code in src/mujoco_mojo/runtime/signal_manager.py
Python
def post(
    self,
    value: float,
    category: SignalCategory | str,
    subgroups: tuple[str, ...] = (),
    *,
    attr: str | None = None,
    metadata: dict[str, Any] | None = None,
):
    """
    Injects a value into the telemetry ledger using a hierarchical namespace.

    This method constructs a structured key that the dashboard uses to build a navigable tree view. The naming convention follows a folder-like structure to group related signals (e.g., all axes of a body's position).

    Format:
        Category/Subgroup:Attribute
        (e.g., "Bodies/Link_1:xpos_x")

    Args:
        value (float): The numeric data to record.
        category (SignalCategory | str): Top level category (e.g., "Bodies")
        subgroups (tuple[str, ...], optional): The second-level organizational folders. Defaults to an empty tuple.
        attr (str | None, optional): The specific signal or component name (e.g., "qpos" or "x"). Defaults to None.
        metadata (dict[str, Any] | None, optional): Arbitrary metadata for this signal, persisted into the telemetry file's footer. Only consulted the first time this signal is registered (ignored on later calls for the same signal). Two keys are validated via Pint if present: `dimension` (e.g. `"[length] / [time]"`), for tagging the physical quantity type when the concrete unit isn't knowable (the right choice for built-in signals, since the user's modeling unit system isn't known here), and `unit` (e.g. `"meter / second"`), for the rarer case the concrete unit truly is known. Any other keys (e.g. `display_name`, `comment`) pass through unvalidated. Defaults to None.

    Examples:
        >>> # Becomes "Bodies/Hand/xpos:x"
        >>> manager.post(1.2, SignalCategory.BODIES, ("Hand", "xpos"), "x")

        >>> # Becomes "Sensors/IMU/Accel:z"
        >>> manager.post(9.81, "Sensors", ("IMU", "Accel"), attr="z")

        >>> # Tag a custom signal's physical quantity type without committing to a unit system
        >>> manager.post(0.4, "Custom", ("Spring",), attr="stiffness", metadata={"dimension": "[force] / [length]"})

    """
    # use tuple as cache key to avoid string construction
    cache_lookup = (str(category), subgroups, attr if attr is not None else "")

    if cache_lookup in self._key_cache:
        # fast path for cached signal
        full_key = self._key_cache[cache_lookup]
    else:
        # slow path for a new signal
        path_parts = [str(category)] + [str(s) for s in subgroups if s]
        full_key = "/".join(path_parts)
        if attr:
            full_key += f":{attr}"

        self._key_cache[cache_lookup] = full_key

    # get column index
    if full_key in self._key_to_idx:
        idx = self._key_to_idx[full_key]
    else:
        # register a new signal column
        idx = self._n_cols
        self._key_to_idx[full_key] = idx
        self._n_cols += 1

        if metadata:
            self._column_metadata[full_key] = _validate_signal_metadata(metadata)

        logger.debug(f"New signal registered: {full_key} at index {idx}")

        # grow buffer if exceeding the initial guess
        if self._n_cols > self._data_buffer.shape[1]:
            n_cols_to_add = 50
            new_width = self._data_buffer.shape[1] + n_cols_to_add
            logger.debug(f"Growing telemetry buffer width to {new_width} columns.")

            growth = np.full(
                (self._data_buffer.shape[0], n_cols_to_add),
                np.nan,
                dtype=np.float64,
            )
            self._data_buffer = np.hstack([self._data_buffer, growth])

        # more columns means more bytes per row, so the row budget shrinks
        self._recompute_capacity()

    # write value to buffer for next flush
    self._data_buffer[self._buffer_row_idx, idx] = value

record

Python
record(state: MjState)

Executes all samplers and advances the buffer index. Flushes if due.

Source code in src/mujoco_mojo/runtime/signal_manager.py
Python
def record(self, state: MjState):
    """Executes all samplers and advances the buffer index. Flushes if due."""
    logger.debug(f"Recording telemetry at t={state.data.time:.6f}")

    self._step_count += 1
    if self._step_count % self.record_decimation != 0:
        return

    # record simulation time
    self._data_buffer[self._buffer_row_idx, 0] = state.data.time

    # run samplers
    for task in self._sample_tasks:
        task(state)

    self._buffer_row_idx += 1

    if self._buffer_row_idx >= self._capacity:
        self.flush()

flush

Python
flush()

Writes the memory buffer to a new part file; parts are merged into export_path on close().

Source code in src/mujoco_mojo/runtime/signal_manager.py
Python
def flush(self):
    """Writes the memory buffer to a new part file; parts are merged into `export_path` on `close()`."""
    if self._buffer_row_idx == 0:
        return

    # build column names from mapping
    sorted_keys = sorted(self._key_to_idx.keys(), key=lambda x: self._key_to_idx[x])

    # slice only the used portion of the buffer
    new_df = pl.from_numpy(
        data=self._data_buffer[: self._buffer_row_idx, : self._n_cols],
        schema=sorted_keys,
    )

    part_path = self._part_path(len(self._part_paths))
    logger.info(f"Flushing {self._buffer_row_idx} steps to {part_path.name}")
    # each part is a brand new file, never read back until close()'s merge,
    # so flushing stays O(buffer capacity) instead of O(total rows written
    # so far) and never reads-then-rewrites a file (avoiding a Windows file lock)
    new_df.write_parquet(part_path, compression="zstd")
    self._part_paths.append(part_path)

    # reset buffer for next batch
    self._buffer_row_idx = 0
    self._data_buffer.fill(np.nan)

Tracer dataclass

Python
Tracer(
    target: Traceable,
    duration: float = 1.0,
    color: Vec4 | None = None,
    width: float = 0.005,
    fade: bool = True,
    record_decimation: int = 1,
)

Draws a fading trail of recent world positions behind a Traceable object (a Body, Site, or Geom).

Typical usage inside a runtime function, within a with runtime_manager as rm: block::

Text Only
1
Tracer(target=box1, duration=2.0).register_to_rm()

Sampled once per RuntimeManager.step() and drawn by any VideoRecorder with show_traces=True.

target instance-attribute

Python
target: Traceable

The object whose world position is followed over time.

duration class-attribute instance-attribute

Python
duration: float = 1.0

How many seconds of trailing history to keep.

color class-attribute instance-attribute

Python
color: Vec4 | None = None

RGBA color of the trail. Defaults to the user's visualization.trace_line setting.

width class-attribute instance-attribute

Python
width: float = 0.005

Width of the trail line.

fade class-attribute instance-attribute

Python
fade: bool = True

If True, older segments are drawn more transparent than newer ones.

record_decimation class-attribute instance-attribute

Python
record_decimation: int = 1

How many steps between each recorded point. Increase for long, fine-timestep simulations to reduce the number of trail segments.

update

Python
update(state: MjState) -> None

Records the target's current position, then drops points older than duration. Call once per step.

Source code in src/mujoco_mojo/runtime/tracer.py
Python
def update(self, state: MjState) -> None:
    """Records the target's current position, then drops points older than `duration`. Call once per step."""
    self._step_count += 1
    if self._step_count % self.record_decimation != 0:
        return

    self._history.append((state.data.time, np.array(self.target.rt_pos(state))))

    cutoff = state.data.time - self.duration
    while self._history and self._history[0][0] < cutoff:
        self._history.popleft()

get_visuals

Python
get_visuals(state: MjState) -> list[LineConfig]

Returns one LineConfig per consecutive pair of recorded positions.

Source code in src/mujoco_mojo/runtime/tracer.py
Python
def get_visuals(self, state: MjState) -> list[LineConfig]:
    """Returns one `LineConfig` per consecutive pair of recorded positions."""
    if not self._vis_loaded:
        self._vis = MujocoMojoSettings().visualization
        self._vis_loaded = True

    color = self.color
    if color is None:
        if not self._vis.trace_line:
            return []
        color = Color[self._vis.trace_line].rgba

    points = list(self._history)
    n = len(points)
    if n < 2:
        return []

    lines = []
    for i in range(n - 1):
        _, p1 = points[i]
        _, p2 = points[i + 1]

        segment_color = color
        if self.fade:
            segment_color = np.array(color, dtype=float)
            segment_color[3] *= (i + 1) / (n - 1)

        lines.append(
            LineConfig(pos1=p1, pos2=p2, color=segment_color, width=self.width)
        )

    return lines

LabelConfig

Bases: TypedDict

Describes one frame label, returned fresh per-frame by VideoRecorder.frame_label.

text instance-attribute

Python
text: str

The label text to burn into the frame.

position instance-attribute

Python
position: NotRequired[tuple[int, int]]

Top-left pixel coordinate to draw at. Defaults to (10, 10).

color instance-attribute

Python
color: NotRequired[Vec3 | Vec4]

Text color, normalized [0, 1] RGB(A) (e.g. Color.WHITE.rgba). Defaults to opaque white.

background_color instance-attribute

Python
background_color: NotRequired[Vec3 | Vec4 | None]

Optional fill behind the text, normalized [0, 1] RGB(A). An RGBA alpha < 1 is true-blended with the frame beneath it. Defaults to no background.

border_color instance-attribute

Python
border_color: NotRequired[Vec3 | Vec4 | None]

Optional 1px outline around the padded label box, normalized [0, 1] RGB(A). Defaults to no border.

font_size instance-attribute

Python
font_size: NotRequired[int]

Font size in pixels. Defaults to 14.

font_path instance-attribute

Python
font_path: NotRequired[str | Path | None]

Path to a TrueType/OpenType font file, for custom styles/weights. Defaults to PIL's built-in font.

padding instance-attribute

Python
padding: NotRequired[int]

Padding in pixels around the text when drawing background_color. Defaults to 4.

VideoRecorder dataclass

Python
VideoRecorder(
    path: Path,
    camera_name: CameraName,
    show_loads: bool = False,
    show_net_force: bool = False,
    show_contacts: bool = False,
    show_proximities: bool = False,
    show_traces: bool = False,
    fps: float = 30,
    playback_speed: float = 1.0,
    width: int = 640,
    height: int = 480,
    recording_trigger: Callable[
        [MjState], bool
    ] = lambda state: True,
    frame_label: Callable[[MjState], LabelConfig]
    | None = None,
    max_frames: int | None = None,
    quality: int | None = None,
    encode_speed: int | None = None,
    _frames: list = list(),
)

Records a MuJoCo simulation to a video file.

Frames are captured at a fixed rate (fps) relative to simulation time, not wall-clock time, so the output plays back at the exact rate specified regardless of how fast the simulation runs. The recorder skips capture_frame calls that fall between the interval boundaries, which prevents duplicate frames when the physics step is finer than 1/fps.

Typical usage inside a runtime function, within a with runtime_manager as rm: block::

Text Only
1
2
3
4
5
6
7
recorder = VideoRecorder(
    path=model.trial_dir / "camera.mp4",
    camera_name="top_view",
    fps=30,
    width=1280,
    height=720,
).setup(mj_model).register_to_rm()

register_to_rm wires the recorder into the RuntimeManager so that capture_frame is called automatically on every physics step and save is called when the simulation finishes. If no runtime_manager is passed, it registers to the RuntimeManager of the active with block.

Supported output formats (determined by the path extension):

  • .mp4: H.264 via ffmpeg; widest browser and player compatibility.
  • .webm: VP9 via ffmpeg; smaller files, fully seekable in the Dojo viewer. Requires ffmpeg with libvpx-vp9 support.
  • .gif: via PIL; no audio, loops automatically; large file size, not seekable.

.mp4 and .webm frames are piped to ffmpeg as they're captured rather than buffered in memory, so recording length isn't limited by available RAM. .gif (and any other extension, which falls back to mediapy) still buffers every frame until save is called. quality and encode_speed tune the ffmpeg encoder for .mp4/.webm; both have no effect on .gif.

Visual overlays (contact forces, net forces, custom arrows/lines) are controlled by the show_* flags and the show_loads flag passed to capture_frame.

playback_speed scales the frame rate of the saved video relative to fps: 0.5 plays back in slow motion, 1 is real time, and 2 plays back at double speed. It does not change how many frames are captured per second of simulation time, only how quickly they are played back.

recording_trigger is a function of the current MjState that gates whether a due frame is actually captured, e.g. lambda state: 5.0 <= state.data.time <= 10.0 to only record a window of the simulation.

frame_label, if set, is called with the current MjState and the returned LabelConfig is burned into the frame - text, position, color, an optional (alpha-blended) background, and font are all configurable per-frame.

max_frames caps the number of frames held in memory; once reached, further capture_frame calls are silently ignored (with a one-time warning).

Call close once save has finished to release the renderer's GL context.

path instance-attribute

Python
path: Path

Output file path. The extension determines the container and codec.

camera_name instance-attribute

Python
camera_name: CameraName

Name of the MuJoCo camera to render from (must exist in the model).

show_loads class-attribute instance-attribute

Python
show_loads: bool = False

Whether to render custom arrow overlays (passed via custom_arrows in capture_frame).

show_net_force class-attribute instance-attribute

Python
show_net_force: bool = False

Whether to render net force visualizations (mjVIS_PERTFORCE).

show_contacts class-attribute instance-attribute

Python
show_contacts: bool = False

Whether to render contact force visualizations (mjVIS_CONTACTFORCE).

show_proximities class-attribute instance-attribute

Python
show_proximities: bool = False

Whether to render custom line overlays (passed via custom_lines in capture_frame).

show_traces class-attribute instance-attribute

Python
show_traces: bool = False

Whether to render Tracer trails (passed via custom_traces in capture_frame).

fps class-attribute instance-attribute

Python
fps: float = 30

Target frame rate of the output video. Frames are sampled every 1/fps seconds of simulation time.

playback_speed class-attribute instance-attribute

Python
playback_speed: float = 1.0

Target for playback speed. If using 0.5 the video will record in "slow motion", 2 will record as occurring twice as fast.

width class-attribute instance-attribute

Python
width: int = 640

Render width in pixels.

height class-attribute instance-attribute

Python
height: int = 480

Render height in pixels.

recording_trigger class-attribute instance-attribute

Python
recording_trigger: Callable[[MjState], bool] = (
    lambda state: True
)

Function evaluated against the current MjState on every step. Frames are only captured while it returns True.

frame_label class-attribute instance-attribute

Python
frame_label: Callable[[MjState], LabelConfig] | None = None

Optional function returning a LabelConfig to burn into each captured frame, e.g. lambda state: {"text": f"t={state.data.time:.2f}s"}.

max_frames class-attribute instance-attribute

Python
max_frames: int | None = None

Optional cap on the number of frames captured. Once reached, further capture_frame calls are ignored. For .gif and other buffered formats this also bounds memory use; .mp4/.webm are streamed to disk as they're captured, so it only bounds recording length for those.

quality class-attribute instance-attribute

Python
quality: int | None = None

Override for ffmpeg's -crf (constant rate factor) on .mp4/.webm: lower means higher quality and a larger file. Defaults to a codec-specific value (23 for .mp4/libx264, 33 for .webm/libvpx-vp9) when unset. Useful range is roughly 18-32; has no effect on .gif.

encode_speed class-attribute instance-attribute

Python
encode_speed: int | None = None

Override for the .mp4/.webm encoder's speed-vs-compression trade-off, on a 0-8 scale where 0 is slowest/best compression and 8 is fastest/worst (passed straight through as -cpu-used for .webm; mapped to an x264 -preset name for .mp4). Defaults to a codec-specific value (4 for .mp4, 2 for .webm) when unset. Has no effect on .gif.

setup

Python
setup(state: MjState) -> Self

Initializes the MuJoCo renderer for this model. Must be called before the simulation loop.

Source code in src/mujoco_mojo/runtime/video_recorder.py
Python
def setup(self, state: MjState) -> Self:
    """Initializes the MuJoCo renderer for this model. Must be called before the simulation loop."""
    self._validate(state)

    try:
        self._renderer = mujoco.Renderer(
            model=state.model, height=self.height, width=self.width
        )
    except Exception as e:
        msg = "Failed to initialize the MuJoCo Renderer. If on a server, try setting 'export MUJOCO_GL=egl' in your terminal."
        logger.error(msg)
        raise RuntimeError(msg) from e

    # initialize the vopt with defaults
    mujoco.mjv_defaultOption(self._vopt)

    # whether or not to show debug graphics
    self._vopt.flags[mujoco.mjtVisFlag.mjVIS_PERTFORCE] = int(self.show_net_force)
    self._vopt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTFORCE] = int(self.show_contacts)
    return self

is_due

Python
is_due(state: MjState) -> bool

Returns whether capture_frame would actually capture a frame for state right now, without any side effects. Lets callers skip expensive work (e.g. building custom_traces) that would otherwise go unused on the steps between frames.

Source code in src/mujoco_mojo/runtime/video_recorder.py
Python
def is_due(self, state: MjState) -> bool:
    """Returns whether `capture_frame` would actually capture a frame for `state` right now, without any side effects. Lets callers skip expensive work (e.g. building `custom_traces`) that would otherwise go unused on the steps between frames."""
    if state.data.time < self._next_record_time:
        return False
    if not self.recording_trigger(state):
        return False
    if self.max_frames is not None and self._frame_count >= self.max_frames:
        return False
    return True

capture_frame

Python
capture_frame(
    state: MjState,
    custom_arrows: list[ArrowConfig],
    custom_lines: list[LineConfig],
    custom_traces: list[LineConfig],
)

Captures the current state as a video frame.

Source code in src/mujoco_mojo/runtime/video_recorder.py
Python
def capture_frame(
    self,
    state: MjState,
    custom_arrows: list[ArrowConfig],
    custom_lines: list[LineConfig],
    custom_traces: list[LineConfig],
):
    """Captures the current state as a video frame."""
    if state.data.time < self._next_record_time:
        return

    if not self.recording_trigger(state):
        return

    if self.max_frames is not None and self._frame_count >= self.max_frames:
        if not self._max_frames_warned:
            logger.warning(
                f"VideoRecorder for {self.path} reached max_frames={self.max_frames}; "
                "no further frames will be captured."
            )
            self._max_frames_warned = True
        return

    frame = self._render_frame(state, custom_arrows, custom_lines, custom_traces)

    if self.path.suffix.lower() in STREAMED_VIDEO_FORMAT:
        if self._encoder_proc is None:
            self._open_encoder()
        w, h = self._encode_size
        assert (
            self._encoder_proc is not None and self._encoder_proc.stdin is not None
        )
        self._encoder_proc.stdin.write(frame[:h, :w].tobytes())
    else:
        self._frames.append(frame)

    # increment the clock for the next frame
    self._frame_count += 1
    self._next_record_time += 1 / self.fps

snapshot

Python
snapshot(
    state: MjState,
    path: Path,
    custom_arrows: list[ArrowConfig] | None = None,
    custom_lines: list[LineConfig] | None = None,
    custom_traces: list[LineConfig] | None = None,
)

Renders the current state and saves it as a single image to path, regardless of recording_trigger or fps timing.

Source code in src/mujoco_mojo/runtime/video_recorder.py
Python
def snapshot(
    self,
    state: MjState,
    path: Path,
    custom_arrows: list[ArrowConfig] | None = None,
    custom_lines: list[LineConfig] | None = None,
    custom_traces: list[LineConfig] | None = None,
):
    """Renders the current state and saves it as a single image to `path`, regardless of `recording_trigger` or `fps` timing."""
    from PIL import Image

    frame = self._render_frame(
        state, custom_arrows or [], custom_lines or [], custom_traces or []
    )
    Image.fromarray(frame).save(path)
    logger.info(f"Snapshot saved to {path}")

save

Python
save()

Finishes writing the video file.

Supported formats: - .mp4: H.264 via ffmpeg; universally compatible. - .webm: VP9 via ffmpeg; smaller files and fully seekable. - .gif: via PIL; no audio, loops automatically, large file size, not seekable.

.mp4 and .webm are encoded incrementally: each frame is piped to ffmpeg as it's captured, so save only needs to close that pipe and wait for ffmpeg to finish. .gif (and any other format) buffers every frame in memory and is only encoded here.

The output format is determined by the extension of path.

Source code in src/mujoco_mojo/runtime/video_recorder.py
Python
def save(self):
    """
    Finishes writing the video file.

    Supported formats:
    - `.mp4`: H.264 via ffmpeg; universally compatible.
    - `.webm`: VP9 via ffmpeg; smaller files and fully seekable.
    - `.gif`: via PIL; no audio, loops automatically, large file size, not seekable.

    `.mp4` and `.webm` are encoded incrementally: each frame is piped to ffmpeg as it's captured, so `save` only needs to close that pipe and wait for ffmpeg to finish. `.gif` (and any other format) buffers every frame in memory and is only encoded here.

    The output format is determined by the extension of `path`.
    """
    if self._frame_count == 0:
        return

    if self._encoder_proc is not None:
        self._finish_encoding()
    elif self.path.suffix.lower() == ".gif":
        from PIL import Image

        # convert arrays to PIL images
        pil_images = [Image.fromarray(frame) for frame in self._frames]

        # save gif
        pil_images[0].save(
            self.path,
            save_all=True,
            append_images=pil_images[1:],
            duration=int(1000 / self._output_fps),  # ms per frame
            loop=0,  # loop forever
        )
    else:
        import mediapy as media

        media.write_video(path=self.path, images=self._frames, fps=self._output_fps)
    logger.info(f"Video saved to {self.path}")

close

Python
close() -> None

Releases the GL context held by the underlying MuJoCo renderer, and kills the ffmpeg encoder if save was never called. Call once recording is finished and save has been called.

Source code in src/mujoco_mojo/runtime/video_recorder.py
Python
def close(self) -> None:
    """Releases the GL context held by the underlying MuJoCo renderer, and kills the ffmpeg encoder if `save` was never called. Call once recording is finished and `save` has been called."""
    if self._encoder_proc is not None and self._encoder_proc.poll() is None:
        proc = self._encoder_proc
        try:
            if proc.stdin is not None:
                proc.stdin.close()
        except BrokenPipeError:
            pass
        proc.kill()
        proc.wait()
    self._renderer.close()