Skip to content

Engine

eden.engine holds Genesis engine components that upstream Genesis does not ship: code that subclasses a Genesis engine class and runs inside scene.step(). It has two parts, the audio sensors and the MochiSolver. The engine addons guide explains what belongs here and how each part plugs into stock Genesis; the physics guide covers when to use Mochi.

Audio sensors

Importing eden.engine.sensors installs every sensor below: a Genesis Sensor subclass registers itself with Genesis' sensor manager on definition, so you pass these options as SensorOptions(sensor=...) like any gs.sensors.* options. The sensors form one audio pipeline: synthesis (ActuationAudio, ContactAudio, PianoAudio), propagation (AudioContext) and rendering (SpatialAudio).

from eden.engine.sensors import ContactAudio, SpatialAudio
from eden.options.sensors import SensorOptions

Motor and gearbox noise

Motor and gearbox noise: the synthesis stage of the audio pipeline.

Ported from the neuroagents-lab/tactile-genesis fork, where it is a standalone AudioSource added through a Genesis-side scene.add_audio_source. Eden has no such API and does not want to add one, so the source is wrapped in a sensor: the sensor declares itself the usual way through SensorsOptions, hands its voice to :class:~eden.engine.sensors.audio.AudioContext at build, and exposes the synthesized block through read() as a bonus — per-joint motor audio is a usable observation in its own right, which the upstream source cannot provide.

The sound model is unchanged: each actuated DOF radiates from its joint's child link, voiced by a velocity-pitched whine (gear mesh), an idle electrical hum, velocity-scaled bearing hiss, and a click on direction reversal, with loudness driven by joint torque and mechanical power.

Provenance

Ported from neuroagents-lab/tactile-genesis @ 3023bcd (Genesis/genesis/engine/audio/actuation_source.py).

Deviations from upstream: a sensor rather than a standalone source (above); entity_idx is validated against the scene rather than at options construction, since Eden injects it at build; every oscillator partial is kept below Nyquist, where upstream bounds only the whine fundamental; a gain tuple longer than n_partials raises instead of being truncated; the whine fades out below 20 Hz, where upstream's frozen oscillators turn the holding torque of a still joint into a DC offset; a block is synthesized in closed form rather than sample by sample (same signal, to float rounding); and the friction_freq / friction_bandwidth / click_freq defaults are lowered from upstream's 1200 / 800 / 3000 Hz, which the default 4 kHz audio rate cannot carry.

Limitation: parametric synthesis, not a measured motor model. The timbre is plausible rather than calibrated to any real actuator, so treat absolute amplitudes as arbitrary units.

Classes

ActuationAudioProperties

Bases: NamedTuple

Timbre of one joint's actuator: how a motor and gearbox sound as a function of torque and speed.

PARAMETER DESCRIPTION
pitch_slope

Whine fundamental in Hz per rad/s of joint speed, bundling gear ratio and mesh teeth. The whine pitch is pitch_slope * |omega|.

TYPE: float

harmonic_gains

Relative gains of the whine partials (fundamental, 2x, 3x, ...). Zero-padded to n_partials; more gains than n_partials raises at build. The whine pitch saturates where the highest partial with a non-zero gain reaches the audio rate's ceiling, so fewer partials buy a higher top pitch.

TYPE: tuple[float, ...]

idle_freq

Frequency in Hz of the idle electrical hum (PWM, magnetostriction).

TYPE: float

idle_gain

Hum amplitude while the motor is energized, i.e. whenever |tau| > 0. This is the static component: with torque roughly constant while a joint holds position it is a constant tone. Use idle_velocity_gain instead to keep a held joint quiet. 0 disables it.

TYPE: float

idle_velocity_gain

Adds idle_velocity_gain * |omega| to the hum amplitude, so the hum's fixed-pitch lines are driven by motion rather than holding torque: quiet when still, louder when moving.

TYPE: float

idle_harmonic_gains

Relative gains of the hum's partials at idle_freq. A real servo hum is harmonic-rich, so something like (1.0, 0.6, 0.4) sounds buzzy rather than like a pure sine. Same length rule as harmonic_gains.

TYPE: tuple[float, ...]

friction_gain

Amplitude of the velocity-scaled bearing and friction hiss. 0 disables it.

TYPE: float

friction_freq

Center frequency in Hz of the friction noise band. Like click_freq and idle_freq, the default fits the default audio rate (audio_substeps=20 at Eden's default sim_dt=0.005, a 900 Hz ceiling); a value the scene's rate cannot carry is clamped, with a warning at build if it is audible.

TYPE: float

friction_bandwidth

Spectral width in Hz of the friction noise band.

TYPE: float

load_gain

Whine loudness per unit |torque| (1/N·m): a loaded joint whines louder than a free-running one at the same speed. Like all of the whine it needs motion, fading out as the pitch drops below 20 Hz; a joint straining in place is voiced by idle_gain.

TYPE: float

power_gain

Loudness per unit mechanical power |tau*omega| (1/W): the working-whine component.

TYPE: float

reversal_click_gain

Amplitude of the click emitted when joint velocity changes sign (backlash takeup), scaled by |omega|. 0 disables it.

TYPE: float

click_freq

Center frequency in Hz of the reversal click resonator.

TYPE: float

click_decay

Decay rate in 1/s of the reversal click resonator.

TYPE: float

slew_coeff

One-pole smoothing in (0, 1] applied to torque and velocity before synthesis, to suppress the controller's per-step ripple. 1 is no smoothing; smaller is heavier.

TYPE: float

Attributes

pitch_slope class-attribute instance-attribute
pitch_slope: float = 120.0
harmonic_gains class-attribute instance-attribute
harmonic_gains: tuple[float, ...] = (1.0, 0.5, 0.25)
idle_freq class-attribute instance-attribute
idle_freq: float = 120.0
idle_gain class-attribute instance-attribute
idle_gain: float = 0.0
idle_velocity_gain class-attribute instance-attribute
idle_velocity_gain: float = 0.0
idle_harmonic_gains class-attribute instance-attribute
idle_harmonic_gains: tuple[float, ...] = (1.0,)
friction_gain class-attribute instance-attribute
friction_gain: float = 0.0
friction_freq class-attribute instance-attribute
friction_freq: float = 600.0
friction_bandwidth class-attribute instance-attribute
friction_bandwidth: float = 400.0
load_gain class-attribute instance-attribute
load_gain: float = 0.2
power_gain class-attribute instance-attribute
power_gain: float = 0.1
reversal_click_gain class-attribute instance-attribute
reversal_click_gain: float = 0.0
click_freq class-attribute instance-attribute
click_freq: float = 800.0
click_decay class-attribute instance-attribute
click_decay: float = 900.0
slew_coeff class-attribute instance-attribute
slew_coeff: float = 0.3

ActuationAudio

Bases: RigidEntitySensorOptionsMixin['ActuationAudioSensor'], SimpleSensorOptions['ActuationAudioSensor']

Motor noise radiated by a robot's actuated joints, and readable as a per-joint signal.

read() returns (B, n_dofs, audio_substeps): this step's block per covered DOF, in the order the joints appear on the entity. The same blocks are registered with the scene's audio registry, so any :class:~eden.engine.sensors.spatial_audio.SpatialAudio microphone hears them from the right place — each DOF radiates from its joint's child link.

PARAMETER DESCRIPTION
entity_idx

Global entity index of the actuated robot. Required; motor noise needs an actuator.

TYPE: int

link_idx_local

Inherited from Genesis' link-attached sensor options and meaningless here: the sensor covers the whole entity and each DOF radiates from its own joint's child link. Anything but the default raises at build.

pos_offset

Inherited from Genesis' link-attached sensor options and meaningless here: the sensor covers the whole entity and each DOF radiates from its own joint's child link. Anything but the default raises at build.

euler_offset

Inherited from Genesis' link-attached sensor options and meaningless here: the sensor covers the whole entity and each DOF radiates from its own joint's child link. Anything but the default raises at build.

joints

Names of the joints to voice. None covers every joint of the entity that has DOFs. An unknown name raises at build rather than being ignored.

TYPE: tuple[str, ...] | None

default_properties

Timbre applied to every covered joint that properties does not override.

TYPE: ActuationAudioProperties

properties

Per-joint-name timbre overrides.

TYPE: dict[str, ActuationAudioProperties]

n_partials

Number of partials in the whine and hum oscillator banks.

TYPE: int

audio_substeps

Samples emitted per physics step. Must match every other audio participant in the scene.

TYPE: int

Examples:

Voice an arm's motors, with a louder whine on the shoulder:

>>> from eden.options import SensorOptions
>>> from eden.engine.sensors.actuation_audio import ActuationAudio, ActuationAudioProperties
>>> motors = SensorOptions(
...     sensor=ActuationAudio(
...         audio_substeps=20,
...         properties={"shoulder_pan_joint": ActuationAudioProperties(power_gain=0.4)},
...     ),
...     attach_entity_name="robot",
... )

Attributes

joints class-attribute instance-attribute
joints: tuple[str, ...] | None = None
default_properties class-attribute instance-attribute
properties class-attribute instance-attribute
properties: dict[str, ActuationAudioProperties] = Field(default_factory=dict)
n_partials class-attribute instance-attribute
n_partials: PositiveInt = 3
audio_substeps class-attribute instance-attribute
audio_substeps: PositiveInt = 20

Methods:

validate_scene
validate_scene(scene: Scene)

Require a real rigid entity, and refuse the link attachment this sensor would ignore.

Runs in scene.add_sensor before the sensor is constructed, which is after Eden has injected entity_idx / link_idx_local from attach_entity_name / attach_link_name.

PARAMETER DESCRIPTION
scene

The scene the sensor is being added to.

TYPE: Scene

Source code in eden/engine/sensors/actuation_audio.py
def validate_scene(self, scene: "Scene"):
    """Require a real rigid entity, and refuse the link attachment this sensor would ignore.

    Runs in ``scene.add_sensor`` before the sensor is constructed, which is after Eden has injected
    ``entity_idx`` / ``link_idx_local`` from ``attach_entity_name`` / ``attach_link_name``.

    Parameters
    ----------
    scene : Scene
        The scene the sensor is being added to.
    """
    if self.entity_idx < 0:
        gs.raise_exception(
            "ActuationAudio needs the entity whose motors it voices, but entity_idx is still -1. Set "
            "`attach_entity_name` on the enclosing eden SensorOptions (or entity_idx directly when adding the "
            "sensor through Genesis)."
        )
    super().validate_scene(scene)
    ignored = {name: getattr(self, name) for name in ("link_idx_local", "pos_offset", "euler_offset")}
    set_anyway = {name: value for name, value in ignored.items() if any(v != 0 for v in np.atleast_1d(value))}
    if set_anyway:
        gs.raise_exception(
            f"ActuationAudio covers a whole entity and ignores its link attachment, but got {set_anyway}. Each "
            "DOF radiates from its own joint's child link; drop `attach_link_name` from the enclosing eden "
            "SensorOptions and leave the offsets at their defaults. Use `joints` to voice a subset."
        )

ActuationVoice

ActuationVoice(options: ActuationAudio, solver: RigidSolver, dof_idx, emit_links, properties)

Bases: AudioSource

Synthesis for one :class:ActuationAudioSensor: one emission point per covered DOF.

Owns the oscillator phases and filter memory, which persist across steps, so it is built once and then driven by :meth:emit from the audio registry each step.

Allocate the synthesis state for the DOFs resolved by the owning sensor.

PARAMETER DESCRIPTION
options

The owning sensor's options.

TYPE: ActuationAudio

solver

Solver to read per-DOF torque and velocity from.

TYPE: RigidSolver

dof_idx

Solver-global DOF indices to voice, in read order.

TYPE: list[int]

emit_links

Solver-global index of the link each DOF radiates from, parallel to dof_idx.

TYPE: list[int]

properties

Timbre of each DOF, parallel to dof_idx.

TYPE: list[ActuationAudioProperties]

Source code in eden/engine/sensors/actuation_audio.py
def __init__(self, options: ActuationAudio, solver: "RigidSolver", dof_idx, emit_links, properties):
    """Allocate the synthesis state for the DOFs resolved by the owning sensor.

    Parameters
    ----------
    options : ActuationAudio
        The owning sensor's options.
    solver : RigidSolver
        Solver to read per-DOF torque and velocity from.
    dof_idx : list[int]
        Solver-global DOF indices to voice, in read order.
    emit_links : list[int]
        Solver-global index of the link each DOF radiates from, parallel to ``dof_idx``.
    properties : list[ActuationAudioProperties]
        Timbre of each DOF, parallel to ``dof_idx``.
    """
    self._options = options
    self._solver = solver
    self._sim = solver._sim

    n_emit = len(dof_idx)
    n_partials = options.n_partials
    B = self._sim._B
    K = options.audio_substeps
    self._K = K
    self._dt_sub = self._sim.dt / K
    # Keep every synthesized partial below Nyquist for the audio rate, with margin, so a fast joint
    # saturates into a scream instead of silently folding down into a low rumble.
    self._nyquist = 0.45 * 0.5 / self._dt_sub
    self._harm_mult = torch.arange(1, n_partials + 1, dtype=gs.tc_float, device=gs.device)

    self._dof_idx = torch.tensor(dof_idx, dtype=gs.tc_int, device=gs.device)
    self._emit_links_t = torch.tensor(emit_links, dtype=gs.tc_int, device=gs.device)
    self._emit_offset_t = torch.zeros((B, n_emit, 3), dtype=gs.tc_float, device=gs.device)

    def col(attr):
        return torch.tensor([getattr(p, attr) for p in properties], dtype=gs.tc_float, device=gs.device)

    def bank(attr):
        gains_t = torch.zeros((n_emit, n_partials), dtype=gs.tc_float, device=gs.device)
        for i, props in enumerate(properties):
            gains = getattr(props, attr)
            if len(gains) > n_partials:
                gs.raise_exception(
                    f"ActuationAudio: `{attr}` has {len(gains)} gains but n_partials={n_partials}, so the "
                    f"last {len(gains) - n_partials} would never sound. Raise `n_partials` or drop them."
                )
            gains_t[i, : len(gains)] = torch.tensor(gains, dtype=gs.tc_float, device=gs.device)
        return gains_t

    def max_fundamental(gains_t):
        # Partial p sits at p times its fundamental, so a bank's fundamental is bounded by the ceiling over
        # the multiple of its highest partial that actually sounds.
        top_multiple = ((gains_t != 0) * self._harm_mult).amax(dim=-1)
        return self._nyquist / top_multiple.clamp(min=1.0)

    def col_below(attr, ceiling, audible):
        values = col(attr)
        ceiling = torch.as_tensor(ceiling, dtype=gs.tc_float, device=gs.device).expand_as(values)
        # A band whose gain is zero never sounds, so clamping it changes nothing worth a warning.
        clamped_audibly = torch.nonzero((values > ceiling) & audible).flatten()
        if clamped_audibly.numel() > 0:
            i = int(clamped_audibly[0])
            en.logger.warning(
                f"ActuationAudio: `{attr}` of {float(values[i]):.0f} Hz exceeds what the "
                f"{1.0 / self._dt_sub:.0f} Hz audio rate can carry and is clamped to {float(ceiling[i]):.0f} Hz. "
                "Raise `audio_substeps` to keep it."
            )
        return torch.minimum(values, ceiling)

    self._harm = bank("harmonic_gains")  # (n_emit, n_partials)
    self._idle_harm = bank("idle_harmonic_gains")  # (n_emit, n_partials)
    self._max_whine_f0 = max_fundamental(self._harm)  # (n_emit,)

    self._pitch_slope = col("pitch_slope")
    self._idle_gain = col("idle_gain")
    self._idle_velocity_gain = col("idle_velocity_gain")
    self._idle_freq = col_below(
        "idle_freq", max_fundamental(self._idle_harm), (self._idle_gain != 0) | (self._idle_velocity_gain != 0)
    )
    self._friction_gain = col("friction_gain")
    self._friction_freq = col_below("friction_freq", self._nyquist, self._friction_gain != 0)
    self._friction_bw = col("friction_bandwidth").clamp(min=gs.EPS)
    self._load_gain = col("load_gain")
    self._power_gain = col("power_gain")
    self._reversal_click_gain = col("reversal_click_gain")
    self._click_freq = col_below("click_freq", self._nyquist, self._reversal_click_gain != 0)
    self._click_decay = col("click_decay").clamp(min=gs.EPS)
    self._slew = col("slew_coeff")

    # The friction band-pass and the click resonator are two-pole recurrences with constant coefficients,
    # so each block is filtered in closed form rather than sample by sample.
    self._friction_free, self._friction_forced = two_pole_block_kernels(
        *bandpass_coeffs(self._friction_freq, self._friction_bw, self._dt_sub), K
    )
    self._click_free, self._click_forced = two_pole_block_kernels(
        *resonator_coeffs(self._click_freq, self._click_decay, self._dt_sub), K
    )
    # Sample k of a block is k + 1 oscillator steps after the phase the previous block ended on.
    self._osc_steps = torch.arange(1, K + 1, dtype=gs.tc_float, device=gs.device)
    self._idle_dphase = 2.0 * torch.pi * self._idle_freq.unsqueeze(-1) * self._harm_mult * self._dt_sub

    # Persistent synthesis state: oscillator phases, band-pass and resonator memory, slew filters.
    self._phase = torch.zeros((B, n_emit, n_partials), dtype=gs.tc_float, device=gs.device)
    self._idle_phase = torch.zeros((B, n_emit, n_partials), dtype=gs.tc_float, device=gs.device)
    self._ty1 = torch.zeros((B, n_emit), dtype=gs.tc_float, device=gs.device)
    self._ty2 = torch.zeros((B, n_emit), dtype=gs.tc_float, device=gs.device)
    self._ay1 = torch.zeros((B, n_emit), dtype=gs.tc_float, device=gs.device)
    self._ay2 = torch.zeros((B, n_emit), dtype=gs.tc_float, device=gs.device)
    self._prev_omega = torch.zeros((B, n_emit), dtype=gs.tc_float, device=gs.device)
    self._slew_tau = torch.zeros((B, n_emit), dtype=gs.tc_float, device=gs.device)
    self._slew_omega = torch.zeros((B, n_emit), dtype=gs.tc_float, device=gs.device)
    self._block = torch.zeros((B, n_emit, K), dtype=gs.tc_float, device=gs.device)

Attributes

block property
block: Tensor
emit_links: Tensor
emit_offset property
emit_offset: Tensor
audio_substeps property
audio_substeps: int

Methods:

reset
reset(envs_idx) -> None

Zero the oscillator phases, filter memory, and last block for envs_idx.

PARAMETER DESCRIPTION
envs_idx

Environments to reset. None resets all of them.

TYPE: array - like | None

Source code in eden/engine/sensors/actuation_audio.py
def reset(self, envs_idx) -> None:
    """Zero the oscillator phases, filter memory, and last block for ``envs_idx``.

    Parameters
    ----------
    envs_idx : array-like | None
        Environments to reset. ``None`` resets all of them.
    """
    idx = slice(None) if envs_idx is None else envs_idx
    for tensor in (
        self._phase,
        self._idle_phase,
        self._ty1,
        self._ty2,
        self._ay1,
        self._ay2,
        self._prev_omega,
        self._slew_tau,
        self._slew_omega,
        self._block,
    ):
        tensor[idx] = 0.0
emit
emit() -> None

Synthesize this step's block from the current per-DOF torque and velocity.

Source code in eden/engine/sensors/actuation_audio.py
def emit(self) -> None:
    """Synthesize this step's block from the current per-DOF torque and velocity."""
    solver = self._solver
    if self._block.numel() == 0:
        return
    B, n_emit, K = self._block.shape
    dt_sub = self._dt_sub

    tau = solver.get_dofs_control_force(self._dof_idx)
    omega = solver.get_dofs_velocity(self._dof_idx)
    if solver.n_envs == 0:
        tau, omega = tau[None], omega[None]  # (B, n_emit)

    # One-pole slew limit denoises the controller's per-step torque and velocity ripple, which would
    # otherwise be audible as a buzz at the control rate rather than as motor noise.
    self._slew_tau += self._slew * (tau - self._slew_tau)
    self._slew_omega += self._slew * (omega - self._slew_omega)
    tau_f, omega_f = self._slew_tau, self._slew_omega

    load_amp = self._load_gain * tau_f.abs() + self._power_gain * (tau_f * omega_f).abs()  # (B, n_emit)
    f0 = torch.minimum(self._pitch_slope * omega_f.abs(), self._max_whine_f0)
    # A joint holding still under load has torque but no pitch: its oscillators freeze mid-cycle, and
    # `load_gain * |tau|` times a frozen sine is a DC offset that steps whenever the torque does — a thump, and
    # headroom lost to something inaudible. Gear mesh only sounds while the gears turn, so fade the whine out
    # as its pitch drops below hearing.
    load_amp = load_amp * (f0 / _AUDIBLE_FLOOR_HZ).clamp(max=1.0)
    # Static (torque-on) hum floor plus a motion-driven term, so the hum tracks joint speed instead
    # of droning constantly while the joint merely holds position.
    idle_amp = self._idle_gain * (tau_f.abs() > 1e-3).to(gs.tc_float) + self._idle_velocity_gain * omega_f.abs()
    friction_amp = self._friction_gain * omega_f.abs()
    # Reversal click: velocity changed sign since last step, i.e. the gear backlash took up.
    sign_flip = (torch.sign(omega_f) * torch.sign(self._prev_omega)) < 0.0
    click_kick = self._reversal_click_gain * omega_f.abs() * sign_flip.to(gs.tc_float)

    # Oscillator banks: every sample's phase at once, (B, n_emit, n_partials, K), summed over partials.
    dphase = 2.0 * torch.pi * f0.unsqueeze(-1) * self._harm_mult * dt_sub  # (B, n_emit, n_partials)
    whine_phase = self._phase.unsqueeze(-1) + dphase.unsqueeze(-1) * self._osc_steps
    idle_phase = self._idle_phase.unsqueeze(-1) + self._idle_dphase.unsqueeze(-1) * self._osc_steps
    whine = load_amp.unsqueeze(-1) * (self._harm.unsqueeze(-1) * torch.sin(whine_phase)).sum(dim=2)
    idle = idle_amp.unsqueeze(-1) * (self._idle_harm.unsqueeze(-1) * torch.sin(idle_phase)).sum(dim=2)

    # Two-pole filters in closed form (see `two_pole_block_kernels`): the memory ringing on, plus the
    # response to this block's drive. Friction is driven by noise on every sample, the click by one kick
    # on the first.
    noise = torch.randn((B, n_emit, K), dtype=gs.tc_float, device=gs.device)
    friction_memory = torch.stack((self._ty1, self._ty2), dim=-1)  # (B, n_emit, 2)
    friction = torch.einsum("ekm,bem->bek", self._friction_free, friction_memory) + torch.einsum(
        "ekj,bej->bek", self._friction_forced, friction_amp.unsqueeze(-1) * noise
    )
    click_memory = torch.stack((self._ay1, self._ay2), dim=-1)
    click = torch.einsum("ekm,bem->bek", self._click_free, click_memory) + (
        self._click_forced[:, :, 0] * click_kick.unsqueeze(-1)
    )

    self._phase.copy_(whine_phase[..., -1] % (2.0 * torch.pi))
    self._idle_phase.copy_(idle_phase[..., -1] % (2.0 * torch.pi))
    # The filters' memory is the block's last two outputs; a one-sample block keeps one sample of the old.
    friction_tail = torch.cat((self._ty1.unsqueeze(-1), friction), dim=-1)
    click_tail = torch.cat((self._ay1.unsqueeze(-1), click), dim=-1)
    self._ty1.copy_(friction_tail[..., -1])
    self._ty2.copy_(friction_tail[..., -2])
    self._ay1.copy_(click_tail[..., -1])
    self._ay2.copy_(click_tail[..., -2])
    self._prev_omega.copy_(omega_f)
    self._block.copy_(whine + idle + friction + click)

ActuationAudioMetadata dataclass

ActuationAudioMetadata(voices: list[ActuationVoice] = list(), cache_offsets: list[int] = list(), last_render: int = 0)

Bases: SimpleSensorMetadata

Per-class state of every :class:ActuationAudioSensor.

Each sensor contributes its voice and the offset of its rows in the class read cache, so the per-step classmethod hook can copy every voice's block without reaching back to instances.

Attributes

voices class-attribute instance-attribute
voices: list[ActuationVoice] = field(default_factory=list)
cache_offsets class-attribute instance-attribute
cache_offsets: list[int] = field(default_factory=list)
last_render class-attribute instance-attribute
last_render: int = 0

Methods:

destroy
destroy()

Drop the voices, releasing their synthesis buffers.

Source code in eden/engine/sensors/actuation_audio.py
def destroy(self):
    """Drop the voices, releasing their synthesis buffers."""
    self.voices.clear()
    self.cache_offsets.clear()

ActuationAudioSensor

ActuationAudioSensor(options: ActuationAudio, idx: int, shared_context: AudioContext, shared_metadata: ActuationAudioMetadata, manager: SensorManager)

Bases: SimpleSensor[ActuationAudio, AudioContext, ActuationAudioMetadata]

Registers an :class:ActuationVoice with the audio registry and reads back its block.

The voice does the synthesis, driven once per step by :meth:~eden.engine.sensors.audio.AudioContext.update. This sensor only copies the result into its read cache, which is what makes the block available as an observation without synthesizing twice.

Source code in eden/engine/sensors/actuation_audio.py
def __init__(
    self,
    options: ActuationAudio,
    idx: int,
    shared_context: AudioContext,
    shared_metadata: ActuationAudioMetadata,
    manager: "SensorManager",
):
    # Resolved here rather than in `build` because `_get_return_format` runs during construction and
    # the read-cache size depends on how many DOFs the options actually cover.
    self._dof_idx: list[int] = []
    self._emit_links: list[int] = []
    self._properties: list[ActuationAudioProperties] = []
    self._voice: ActuationVoice | None = None
    super().__init__(options, idx, shared_context, shared_metadata, manager)

Methods:

build
build()

Build this sensor's voice and register it with the scene's audio registry.

Source code in eden/engine/sensors/actuation_audio.py
def build(self):
    """Build this sensor's voice and register it with the scene's audio registry."""
    super().build()

    context = self._shared_context
    self._voice = ActuationVoice(
        self._options,
        self._manager._sim.rigid_solver,
        self._dof_idx,
        self._emit_links,
        self._properties,
    )
    context.register(self._voice, type(self._options).__name__)

    sm = self._shared_metadata
    sm.voices.append(self._voice)
    sm.cache_offsets.append(self._cache_offset)

Functions:

Contact sound

Contact microphone: the sound of a link touching, tapping and scraping the things around it.

A source-filter model. The filter is the struck object — a bank of damped modal oscillators whose frequencies and decay rates are its material signature (metal rings long and high, wood thuds). The source is the contact: a tap is an impulse into the bank, sliding is force- and speed-scaled noise shaped by the surface roughness, and a hard tap adds a short high click. The timbre is keyed by the material of the link being struck, not the link carrying the sensor: a fingertip sounds like whatever it touches.

Like :mod:~eden.engine.sensors.actuation_audio the synthesis lives in an :class:~eden.engine.sensors.audio.AudioSource, so :class:~eden.engine.sensors.audio.AudioContext drives it on every simulation step and any :class:~eden.engine.sensors.spatial_audio.SpatialAudio microphone hears it from the sensor's link. read() exposes the same block as a structure-borne vibration signal.

Provenance

Ported from neuroagents-lab/tactile-genesis @ 3023bcd (Genesis/genesis/engine/sensors/contact_audio.py and Genesis/genesis/options/sensors/audio.py). The fused block kernel and the contact-driver math are upstream's.

Deviations from upstream:

  • Synthesis runs from AudioContext.update rather than the sensor's own update hook. Eden skips sensor updates on intermediate decimation sub-steps, and a tap lasts one simulation step — synthesized from the sensor hook, three taps in four would never sound at decimation=4.
  • Materials are keyed by Eden scene names ("table", "robot/hand") instead of solver link indices, which a declarative config cannot know; an unknown name raises at build. Two sensors giving one name different materials raise instead of the later one winning.
  • Not ported: ContactAudioProperties.from_mesh and the strike-location mode-shape tables it fills (they need upstream's genesis.utils.modal_analysis, which stock Genesis does not have), and the active-acoustic ExcitationSignal emitter.
  • accel_noise_freq defaults to 800 Hz, not 5000 Hz, which the default 4 kHz audio rate cannot carry; a mode or click the rate cannot carry warns at build when it would have sounded.

Limitations: one dominant contact per sensor link drives the timbre; slip speed is taken between link origins, ignoring the lever arm of a rotating body; radiation is a point source at the sensor link with no directivity.

Classes

ContactAudioProperties

Bases: NamedTuple

How an object sounds when something touches it: its resonances, and the roughness of its surface.

Metal-like materials have high modes that decay slowly (a long ring); wood-like ones have lower modes that decay fast, and a stronger roughness source.

PARAMETER DESCRIPTION
modal_freqs

Resonant mode frequencies in Hz. Fewer than the sensor's n_modes is zero-padded; more raises. A mode the audio rate cannot carry is silenced, with a warning at build.

TYPE: tuple[float, ...]

modal_decays

Amplitude decay rate of each mode in 1/s. Large is a thud, small a long ring. Same length as modal_freqs.

TYPE: tuple[float, ...]

modal_gains

Output weight of each mode. Same length as modal_freqs.

TYPE: tuple[float, ...]

roughness_gain

Amplitude of the sliding-texture noise, scaled at runtime by normal force and slip speed. 0 is a smooth, silent slide.

TYPE: float

roughness_spatial_freq

Surface bumps per meter. The scrape's pitch is roughness_spatial_freq * slip_speed Hz, so sliding faster raises it.

TYPE: float

roughness_bandwidth

Spectral width in Hz of the scrape around that pitch. Several hundred Hz gives a noisy "shhh"; a narrow band an unrealistic tone.

TYPE: float

impact_gain

Scale of the impulse a tap injects into the modal bank.

TYPE: float

impact_threshold

Force jump in N per simulation step that counts as a tap. A tap fires only when the force crosses this from below, so the force ripple of a sustained slide does not re-ping the modes into a buzz.

TYPE: float

contact_damping

Extra modal decay in 1/s while in contact: a finger pressing on an object deadens it, so the free ring-down is only heard after release.

TYPE: float

contact_damping_per_force

Further in-contact decay per newton of normal force, in 1/(s·N): a firm press deadens the ring more than a light touch (Zheng & James 2011).

TYPE: float

accel_noise_gain

Amplitude of the short high click a tap adds on top of the ring-down: the Hertzian contact transient of small hard objects, which the modal bank misses. 0 disables it.

TYPE: float

accel_noise_freq

Center frequency in Hz of the click. Clamped to what the audio rate can carry, with a warning at build if the click is audible.

TYPE: float

accel_noise_decay

Decay rate in 1/s of the click. Large is short and snappy.

TYPE: float

Attributes

modal_freqs class-attribute instance-attribute
modal_freqs: tuple[float, ...] = (250.0,)
modal_decays class-attribute instance-attribute
modal_decays: tuple[float, ...] = (40.0,)
modal_gains class-attribute instance-attribute
modal_gains: tuple[float, ...] = (1.0,)
roughness_gain class-attribute instance-attribute
roughness_gain: float = 0.0
roughness_spatial_freq class-attribute instance-attribute
roughness_spatial_freq: float = 0.0
roughness_bandwidth class-attribute instance-attribute
roughness_bandwidth: float = 600.0
impact_gain class-attribute instance-attribute
impact_gain: float = 1.0
impact_threshold class-attribute instance-attribute
impact_threshold: float = 0.5
contact_damping class-attribute instance-attribute
contact_damping: float = 80.0
contact_damping_per_force class-attribute instance-attribute
contact_damping_per_force: float = 0.0
accel_noise_gain class-attribute instance-attribute
accel_noise_gain: float = 0.0
accel_noise_freq class-attribute instance-attribute
accel_noise_freq: float = 800.0
accel_noise_decay class-attribute instance-attribute
accel_noise_decay: float = 800.0

ContactAudio

Bases: RigidSensorOptionsMixin['ContactAudioSensor'], SimpleSensorOptions['ContactAudioSensor']

The sound of one link's contacts, keyed by the material of whatever it touches.

read() returns (B, audio_substeps): this step's block of the structure-borne vibration at the link. The same block is registered with the scene's audio registry, radiating from the link (plus pos_offset), so any :class:~eden.engine.sensors.spatial_audio.SpatialAudio microphone hears it.

PARAMETER DESCRIPTION
entity_idx

Global entity index of the body carrying the sensor. Required; Eden fills it from attach_entity_name.

TYPE: int

link_idx_local

Local index of the link whose contacts are voiced. Eden fills it from attach_link_name.

TYPE: int

pos_offset

Where on the link the sound radiates from, in the link's frame.

TYPE: array - like[float, float, float]

euler_offset

Accepted for parity with other link-attached sensors; a point source has no direction.

TYPE: array - like[float, float, float]

properties

Material of the things this link may touch, by Eden scene name: "table" covers every link of that entity, "robot/hand" one link. Merged across every ContactAudio sensor in the scene; an unknown name raises at build, and so does one name given two different materials.

TYPE: dict[str, ContactAudioProperties]

default_properties

Material of every link properties does not name. None leaves contacts with them silent.

TYPE: ContactAudioProperties | None

audio_substeps

Samples emitted per physics step. Must match every other audio participant in the scene; the sample rate is audio_substeps / sim_dt. Real materials ring at kHz, so expect to raise this well above the default.

TYPE: int

n_modes

Size of the modal bank. Must be the same on every ContactAudio sensor in the scene.

TYPE: int

velocity_gate_ref

If positive, scale the output by motion / (motion + velocity_gate_ref), where motion is the speed of the sensor's own link in m/s: a body at rest goes quiet, which suppresses the clicks of a resting contact re-settling. 0 disables the gate.

TYPE: float

velocity_gate_ang_weight

Meters per radian converting the link's angular speed into motion, roughly the body's radius.

TYPE: float

velocity_gate_smooth

One-pole smoothing in (0, 1] of the gate across simulation steps, so it cannot click by itself. 1 is no smoothing.

TYPE: float

Examples:

A fingertip that hears what it touches:

>>> from eden.options import SensorOptions
>>> from eden.engine.sensors.contact_audio import ContactAudio, ContactAudioProperties
>>> steel = ContactAudioProperties(modal_freqs=(1200.0, 2900.0), modal_decays=(6.0, 9.0), modal_gains=(1.0, 0.6))
>>> finger = SensorOptions(
...     sensor=ContactAudio(audio_substeps=80, properties={"bowl": steel}),
...     attach_entity_name="robot",
...     attach_link_name="left_finger",
... )

Attributes

properties class-attribute instance-attribute
properties: dict[str, ContactAudioProperties] = Field(default_factory=dict)
default_properties class-attribute instance-attribute
default_properties: ContactAudioProperties | None = None
audio_substeps class-attribute instance-attribute
audio_substeps: PositiveInt = 20
n_modes class-attribute instance-attribute
n_modes: PositiveInt = 8
velocity_gate_ref class-attribute instance-attribute
velocity_gate_ref: float = Field(default=0.0, ge=0.0)
velocity_gate_ang_weight class-attribute instance-attribute
velocity_gate_ang_weight: float = Field(default=0.0, ge=0.0)
velocity_gate_smooth class-attribute instance-attribute
velocity_gate_smooth: float = Field(default=1.0, gt=0.0, le=1.0)

Methods:

materials
materials() -> dict[str, ContactAudioProperties]

Every material this sensor declares, with the default under the key "".

Source code in eden/engine/sensors/contact_audio.py
def materials(self) -> dict[str, ContactAudioProperties]:
    """Every material this sensor declares, with the default under the key ``""``."""
    named = dict(self.properties)
    if self.default_properties is not None:
        named[""] = self.default_properties
    return named
validate_scene
validate_scene(scene: Scene)

Require a link to listen on, and that every named material exists in the scene.

Runs in scene.add_sensor, after Eden has injected entity_idx / link_idx_local. Entities are all added before any sensor, so a name that does not resolve here never will.

PARAMETER DESCRIPTION
scene

The scene the sensor is being added to.

TYPE: Scene

Source code in eden/engine/sensors/contact_audio.py
def validate_scene(self, scene: "Scene"):
    """Require a link to listen on, and that every named material exists in the scene.

    Runs in ``scene.add_sensor``, after Eden has injected ``entity_idx`` / ``link_idx_local``. Entities are
    all added before any sensor, so a name that does not resolve here never will.

    Parameters
    ----------
    scene : Scene
        The scene the sensor is being added to.
    """
    if self.entity_idx < 0:
        gs.raise_exception(
            "ContactAudio needs the link whose contacts it voices, but entity_idx is still -1. Set "
            "`attach_entity_name` (and usually `attach_link_name`) on the enclosing eden SensorOptions."
        )
    super().validate_scene(scene)
    for key in self.properties:
        _material_links(scene.entities, key)

ContactVoice

ContactVoice(solver: RigidSolver, metadata: ContactAudioSensorMetadata, n_modes: int, audio_substeps: int)

Bases: AudioSource

Synthesis for every :class:ContactAudioSensor in the scene: one emission point per sensor.

One voice for the whole class rather than one per sensor, so the contact list and link velocities are read once per step however many sensors exist. Sensors add themselves at build; the material table is rebuilt as their properties merge in.

Create an empty voice; sensors join through :meth:add_sensor.

PARAMETER DESCRIPTION
solver

Solver to read contacts and link velocities from.

TYPE: RigidSolver

metadata

The sensor class's shared metadata, read for each sensor's link and radiation offset.

TYPE: ContactAudioSensorMetadata

n_modes

Size of the modal bank.

TYPE: int

audio_substeps

Samples per simulation step.

TYPE: int

Source code in eden/engine/sensors/contact_audio.py
def __init__(
    self, solver: "RigidSolver", metadata: "ContactAudioSensorMetadata", n_modes: int, audio_substeps: int
):
    """Create an empty voice; sensors join through :meth:`add_sensor`.

    Parameters
    ----------
    solver : RigidSolver
        Solver to read contacts and link velocities from.
    metadata : ContactAudioSensorMetadata
        The sensor class's shared metadata, read for each sensor's link and radiation offset.
    n_modes : int
        Size of the modal bank.
    audio_substeps : int
        Samples per simulation step.
    """
    self._solver = solver
    self._metadata = metadata
    self._n_modes = n_modes
    self._K = audio_substeps
    self._dt_sub = solver._sim.dt / audio_substeps
    self._band_edge = _BAND_EDGE * 0.5 / self._dt_sub
    # One-pole DC-blocking high-pass: R = 1 - 2*pi*fc/fs.
    self._hp_r = max(0.0, 1.0 - 2.0 * torch.pi * _DC_BLOCK_HZ * self._dt_sub)

    self._materials: dict[str, ContactAudioProperties] = {}
    self._link_material = torch.full((solver.n_links,), -1, dtype=torch.long, device=gs.device)
    self._table: dict[str, torch.Tensor] = {}

    def state(*shape):
        return torch.zeros((solver._sim._B, 0, *shape), dtype=gs.tc_float, device=gs.device)

    # Persistent synthesis state, one column per sensor. `cur_*` latch the *free* coefficients of the last
    # material touched, so a released mode keeps ringing down with that material's own decay.
    self._modal_y = state(n_modes, 2)
    self._cur_a1, self._cur_a2, self._cur_gains = state(n_modes), state(n_modes), state(n_modes)
    self._tex_y, self._acc_y, self._hp_state = state(2), state(2), state(2)
    self._prev_force, self._gate_state = state(), state()
    self._block = state(audio_substeps)
    # Per-sensor velocity gate, (n_sensors,).
    self._gate_ref = torch.zeros((0,), dtype=gs.tc_float, device=gs.device)
    self._gate_ang_w = torch.zeros((0,), dtype=gs.tc_float, device=gs.device)
    self._gate_smooth = torch.zeros((0,), dtype=gs.tc_float, device=gs.device)
    self._has_gate = False

Attributes

block property
block: Tensor
emit_links: Tensor
emit_offset property
emit_offset: Tensor
audio_substeps property
audio_substeps: int

Methods:

add_sensor
add_sensor(options: ContactAudio, scene_entities) -> None

Grow every state tensor by one sensor column and merge in the sensor's materials.

PARAMETER DESCRIPTION
options

The joining sensor's options.

TYPE: ContactAudio

scene_entities

The scene's Genesis entities, for resolving material names to links.

TYPE: list

Source code in eden/engine/sensors/contact_audio.py
def add_sensor(self, options: ContactAudio, scene_entities) -> None:
    """Grow every state tensor by one sensor column and merge in the sensor's materials.

    Parameters
    ----------
    options : ContactAudio
        The joining sensor's options.
    scene_entities : list
        The scene's Genesis entities, for resolving material names to links.
    """
    for key, props in options.materials().items():
        if self._materials.setdefault(key, props) != props:
            name = f"material '{key}'" if key else "default_properties"
            gs.raise_exception(
                f"ContactAudio {name} is declared twice with different values. Materials are shared by every "
                "ContactAudio sensor in the scene; declare each one once, or identically."
            )
    self._rebuild_material_table(scene_entities)

    for attr in self._STATE:
        tensor = getattr(self, attr)
        setattr(self, attr, torch.cat((tensor, tensor.new_zeros((tensor.shape[0], 1, *tensor.shape[2:]))), dim=1))
    self._gate_ref = concat_with_tensor(self._gate_ref, options.velocity_gate_ref)
    self._gate_ang_w = concat_with_tensor(self._gate_ang_w, options.velocity_gate_ang_weight)
    self._gate_smooth = concat_with_tensor(self._gate_smooth, options.velocity_gate_smooth)
    self._has_gate = self._has_gate or options.velocity_gate_ref > 0.0
reset
reset(envs_idx) -> None

Silence the resonators and forget the last contact for envs_idx.

PARAMETER DESCRIPTION
envs_idx

Environments to reset. None resets all of them.

TYPE: array - like | None

Source code in eden/engine/sensors/contact_audio.py
def reset(self, envs_idx) -> None:
    """Silence the resonators and forget the last contact for ``envs_idx``.

    Parameters
    ----------
    envs_idx : array-like | None
        Environments to reset. ``None`` resets all of them.
    """
    idx = slice(None) if envs_idx is None else envs_idx
    for attr in self._STATE:
        getattr(self, attr)[idx] = 0.0
emit
emit() -> None

Synthesize this step's block from the contacts on each sensor link.

Source code in eden/engine/sensors/contact_audio.py
def emit(self) -> None:
    """Synthesize this step's block from the contacts on each sensor link."""
    solver, table = self._solver, self._table
    B, n_sensors, n_modes = self._modal_y.shape[:3]
    if n_sensors == 0:
        return
    if not self._materials:
        # Every sensor has joined by the first emit, which runs inside env.build(), so this still fails at build.
        gs.raise_exception(
            "No ContactAudio sensor in the scene declares a material, so none of them could ever sound. Set "
            "`properties` (by Eden scene name) or `default_properties` on at least one."
        )
    K, dt_sub = self._K, self._dt_sub
    sensor_links = self._metadata.links_idx.long()  # (S,)

    # --- 1) Contact drivers, constant over the block, aggregated per sensor link.
    contacts = solver.collider.get_contacts(as_tensor=True, to_torch=True, keep_batch_dim=True)
    force, normal = contacts["force"], contacts["normal"]  # (B, C, 3)
    link_a, link_b = contacts["link_a"].long(), contacts["link_b"].long()  # (B, C), -1 where padded
    n_contacts = link_a.shape[-1]

    zeros = torch.zeros((B, n_sensors), dtype=gs.tc_float, device=gs.device)
    if n_contacts == 0:
        # Nothing touching: no drive, and the modal bank rings down with its latched free coefficients.
        f_normal = zeros
        a1, a2, gains = self._cur_a1, self._cur_a2, self._cur_gains
        # Zero feedback lets the texture and click memory die at once; both are sub-millisecond anyway.
        impulse = modal_drive = tex_amp = accel_kick = at1 = at2 = aa1 = aa2 = zeros
    else:
        is_a = link_a[:, None, :] == sensor_links[None, :, None]  # (B, S, C)
        is_b = link_b[:, None, :] == sensor_links[None, :, None]
        involved = is_a | is_b
        any_contact = involved.any(dim=2)  # (B, S)

        # Total contact force on the sensor link (Newton's third law: +force when the sensor is link_b).
        orient = is_b.to(gs.tc_float) - is_a.to(gs.tc_float)
        f_normal = (orient[..., None] * force[:, None]).sum(dim=2).norm(dim=-1)  # (B, S)

        # The contact with the largest force speaks for the link: its counterpart is the struck material.
        primary = torch.where(involved, force.norm(dim=-1)[:, None], 0.0).argmax(dim=2)  # (B, S), into C
        other = torch.where(is_a, link_b[:, None, :], link_a[:, None, :])  # (B, S, C)
        struck = other.gather(2, primary[..., None]).squeeze(-1).clamp(min=0)  # (B, S)
        primary_normal = normal[:, None].expand(B, n_sensors, n_contacts, 3)
        primary_normal = primary_normal.gather(2, primary[..., None, None].expand(B, n_sensors, 1, 3)).squeeze(2)

        # Slip: the tangential part of the two link origins' relative velocity.
        links_vel = solver.get_links_vel()
        if solver.n_envs == 0:
            links_vel = links_vel[None]  # (B, n_links, 3)
        v_rel = links_vel[:, sensor_links] - links_vel.gather(1, struck[..., None].expand(B, n_sensors, 3))
        v_tan = v_rel - (v_rel * primary_normal).sum(dim=-1, keepdim=True) * primary_normal
        slip = v_tan.norm(dim=-1) * any_contact

        material = self._link_material[struck]  # (B, S), -1 where the struck link has no material
        valid = any_contact & (material >= 0)
        validf = valid.to(gs.tc_float)
        material = material.clamp(min=0)

        # Two coefficient sets per mode. While pressed, the finger damps the object (a floor plus a term per
        # newton), so a slide does not ring into a tone; the free decay alone is latched for after release.
        theta = 2.0 * torch.pi * table["freqs"][material] * dt_sub  # (B, S, M)
        decays = table["decays"][material]
        pressed = table["contact_damping"][material] + table["damp_per_force"][material] * f_normal
        free_r = torch.exp(-decays * dt_sub)
        pressed_r = torch.exp(-(decays + pressed[..., None]) * dt_sub)

        touching = valid[..., None]
        a1 = torch.where(touching, 2.0 * pressed_r * torch.cos(theta), self._cur_a1)
        a2 = torch.where(touching, pressed_r * pressed_r, self._cur_a2)
        gains = torch.where(touching, table["gains"][material], self._cur_gains)
        self._cur_a1.copy_(torch.where(touching, 2.0 * free_r * torch.cos(theta), self._cur_a1))
        self._cur_a2.copy_(torch.where(touching, free_r * free_r, self._cur_a2))
        self._cur_gains.copy_(gains)

        # A tap fires only when the force crosses the threshold from below. The solver's contact force ripples
        # during a slide; gating on the jump alone would inject an impulse every step, a buzz at the step rate.
        d_force = (f_normal - self._prev_force).clamp(min=0.0)
        threshold = table["impact_thresh"][material]
        tap = d_force * ((d_force > threshold) & (self._prev_force <= threshold)) * validf
        impulse = table["impact_gain"][material] * tap
        accel_kick = table["accel_gain"][material] * tap
        click_r = torch.exp(-table["accel_decay"][material] * dt_sub)
        aa1 = 2.0 * click_r * torch.cos(2.0 * torch.pi * table["accel_freq"][material] * dt_sub)
        aa2 = click_r * click_r

        # Sliding texture: a broadband scrape at the slip pitch, plus the same noise into the (damped) modal
        # bank, so the object's resonances color the scrape as formants instead of ringing as a tone.
        tex_amp = table["rough_gain"][material] * f_normal * slip * validf
        modal_drive = _MODAL_NOISE_FRAC * tex_amp
        tex_freq = (table["rough_sf"][material] * slip).clamp(max=self._band_edge)
        tex_r = torch.exp(-torch.pi * table["rough_bw"][material] * dt_sub)
        at1 = 2.0 * tex_r * torch.cos(2.0 * torch.pi * tex_freq * dt_sub)
        at2 = tex_r * tex_r

    # --- 2) The block itself, in one fused kernel over (env, sensor).
    tex_noise = torch.randn((B, n_sensors, K), dtype=gs.tc_float, device=gs.device)
    modal_noise = torch.randn((B, n_sensors, n_modes, K), dtype=gs.tc_float, device=gs.device)
    out = torch.empty((B, n_sensors, K), dtype=gs.tc_float, device=gs.device)
    _kernel_synthesize_block(
        a1.contiguous(),
        a2.contiguous(),
        gains.contiguous(),
        modal_drive.contiguous(),
        modal_noise,
        tex_noise,
        impulse.contiguous(),
        at1.contiguous(),
        at2.contiguous(),
        aa1.contiguous(),
        aa2.contiguous(),
        tex_amp.contiguous(),
        accel_kick.contiguous(),
        self._modal_y,
        self._tex_y,
        self._acc_y,
        self._hp_state,
        out,
        float(self._hp_r),
    )
    self._prev_force.copy_(f_normal)

    # --- 3) Optional gate on the sensor link's own motion, so a body at rest goes quiet.
    if self._has_gate:
        links_idx = self._metadata.links_idx
        links_vel, links_ang = solver.get_links_vel(links_idx), solver.get_links_ang(links_idx)
        if solver.n_envs == 0:
            links_vel, links_ang = links_vel[None], links_ang[None]
        motion = links_vel.norm(dim=-1) + self._gate_ang_w * links_ang.norm(dim=-1)  # (B, S)
        target = motion / (motion + self._gate_ref).clamp(min=gs.EPS)
        self._gate_state += self._gate_smooth * (target - self._gate_state)
        out = out * torch.where(self._gate_ref > 0.0, self._gate_state, 1.0)[..., None]

    self._block.copy_(out)

ContactAudioSensorMetadata dataclass

ContactAudioSensorMetadata(voice: ContactVoice | None = None, last_render: int = 0)

Bases: RigidSensorMetadataMixin, SimpleSensorMetadata

Per-class state of every :class:ContactAudioSensor: the link buckets, and the one voice they share.

Attributes

voice class-attribute instance-attribute
voice: ContactVoice | None = None
last_render class-attribute instance-attribute
last_render: int = 0

Methods:

destroy
destroy()

Drop the voice, releasing its synthesis buffers.

Source code in eden/engine/sensors/contact_audio.py
def destroy(self):
    """Drop the voice, releasing its synthesis buffers."""
    self.voice = None

ContactAudioSensor

ContactAudioSensor(options: ContactAudio, idx: int, shared_context: AudioContext, shared_metadata: ContactAudioSensorMetadata, manager: SensorManager)

Bases: RigidSensorMixin[ContactAudioSensorMetadata], SimpleSensor[ContactAudio, AudioContext, ContactAudioSensorMetadata]

Joins the scene's :class:ContactVoice and reads back its own column of the block.

The voice does the synthesis, driven once per step by :meth:~eden.engine.sensors.audio.AudioContext.update. This sensor only copies the result into its read cache.

Source code in eden/engine/sensors/contact_audio.py
def __init__(
    self,
    options: ContactAudio,
    idx: int,
    shared_context: AudioContext,
    shared_metadata: ContactAudioSensorMetadata,
    manager: "SensorManager",
):
    super().__init__(options, idx, shared_context, shared_metadata, manager)
    self.debug_object: "Mesh | None" = None

Attributes

debug_object instance-attribute
debug_object: Mesh | None = None

Methods:

build
build()

Create the class's voice if this is its first sensor, and add this sensor's column to it.

Source code in eden/engine/sensors/contact_audio.py
def build(self):
    """Create the class's voice if this is its first sensor, and add this sensor's column to it."""
    super().build()

    sm, context, options = self._shared_metadata, self._shared_context, self._options
    if sm.voice is None:
        sm.voice = ContactVoice(sm.solver, sm, options.n_modes, options.audio_substeps)
        context.register(sm.voice, type(options).__name__)
    else:
        context.require_audio_substeps(options.audio_substeps, type(options).__name__)
        if options.n_modes != sm.voice._n_modes:
            gs.raise_exception(
                f"ContactAudio declares n_modes={options.n_modes} but the scene's other ContactAudio sensors use "
                f"n_modes={sm.voice._n_modes}. They share one modal bank, so n_modes must agree."
            )
    sm.voice.add_sensor(options, self._manager._sim.entities)

Functions:

Piano keyboard

The sound of a piano keyboard: struck strings, voiced from the motion of the keys.

A keyboard entity (:class:~eden.options.objects.piano.Piano) is only hinges and springs. This sensor turns the motion of those hinges into notes and publishes them to the scene's audio registry, so any :class:~eden.engine.sensors.spatial_audio.SpatialAudio microphone hears the instrument, and read() returns the same signal directly.

The model is a piano action reduced to its three audible events:

  • Strike. When a key travels past strike_fraction of its stroke the hammer is thrown, and how loud the note is depends on how fast the key was moving at that moment, nothing else. A key pressed slowly enough makes no sound, as on the real instrument.
  • Ring. The string is a bank of partials at n * f0 * sqrt(1 + inharmonicity * n**2), each an exponentially decaying sinusoid; higher partials and higher notes die faster. While the key stays down the damper is off the string and the note rings on.
  • Damp. When the key comes back up past release_fraction the damper falls and the note is gone in a few tens of milliseconds, unless the sustain pedal (:meth:PianoAudioSensor.set_sustain) is down.

What a key is touched by plays no part, which is the difference from :class:~eden.engine.sensors.contact_audio.ContactAudio: there the contact force is the excitation and staying in contact damps the ring, the opposite of both on a piano.

Each partial is held as a rotating phasor rather than as a two-pole recurrence, so a block is linear in the phasors: one matrix product against precomputed tables of decay envelope times sine and cosine, exact to rounding at any pitch. The recurrence would do badly here: at 27.5 Hz and a one-second decay its two state samples differ in the fifth digit, and float32 would turn that into noise. It also makes the damper free: lifting or dropping it only swaps the envelope table.

RoboPianist, where the keyboard's dimensions come from, does not synthesize sound: it sends note-on/off events to a MIDI synthesizer with every strike at full velocity. Its "key is down" test, within half a degree of the joint limit, is about 87% of a white key's stroke, which is where strike_fraction defaults.

Limitation: parametric synthesis, not a sampled instrument. It is recognizably a struck string with the right pitch, dynamics and damping, not a grand piano; amplitudes are arbitrary units.

Classes

PianoAudio

Bases: RigidEntitySensorOptionsMixin['PianoAudioSensor'], SimpleSensorOptions['PianoAudioSensor']

The sound of a keyboard entity, from the motion of its key joints.

read() returns (B, audio_substeps): this step's block of the whole instrument. The same block is registered with the scene's audio registry and radiates from the keyboard's base link.

PARAMETER DESCRIPTION
entity_idx

Global entity index of the keyboard. Required.

TYPE: int

link_idx_local

Inherited from Genesis' link-attached sensor options. pos_offset moves the point the sound radiates from, in the base link's frame; the other two are meaningless here and anything but the default raises at build.

pos_offset

Inherited from Genesis' link-attached sensor options. pos_offset moves the point the sound radiates from, in the base link's frame; the other two are meaningless here and anything but the default raises at build.

euler_offset

Inherited from Genesis' link-attached sensor options. pos_offset moves the point the sound radiates from, in the base link's frame; the other two are meaningless here and anything but the default raises at build.

key_notes

The keys to voice and what each plays: key joint name to MIDI note number. Its order is the column order of :attr:PianoAudioSensor.keys_down. None voices every single-DOF joint of the entity, in order, as a chromatic run from lowest_midi_note, which is right for :class:~eden.options.objects.piano.Piano. To voice only some of its keys, name each with its own note, e.g. {key_joint_name(key): LOWEST_MIDI_NOTE + key for key in keys}. An unknown name raises at build.

TYPE: dict[str, int] | None

lowest_midi_note

MIDI note number of the entity's first key joint when key_notes is None; 21 is the A0 of an 88-key piano. Key i plays note lowest_midi_note + i, in equal temperament like every note. Changing it alongside key_notes raises, since the notes are then given there.

TYPE: int

tuning

Frequency of A4 (MIDI note 69), in Hz.

TYPE: float

strike_fraction

Fraction of a key's stroke at which the hammer is thrown.

TYPE: float

release_fraction

Fraction of the stroke below which a returning key drops its damper. Must be below strike_fraction; the gap between them is what stops a key hovering at the strike point from re-striking.

TYPE: float

velocity_ref

Key speed at the strike, in rad/s, that gives a full-scale note. Loudness is proportional to speed up to here and saturates above. A forte stroke takes a white key through its 10 mm in about 20 ms, 3 rad/s.

TYPE: float

n_partials

Partials per string.

TYPE: int

inharmonicity

String stiffness coefficient B: partial n sits at n * f0 * sqrt(1 + B * n**2), slightly sharp of harmonic. 0 is an ideal string.

TYPE: float

partial_rolloff

Partial n starts at amplitude n ** -partial_rolloff. Larger is mellower.

TYPE: float

ring_decay

Amplitude decay rate, in 1/s, of the fundamental of A4 while the damper is up.

TYPE: float

decay_pitch_exponent

Decay rates scale with (f0 / tuning) ** decay_pitch_exponent: a treble string dies sooner than a bass one.

TYPE: float

partial_decay_growth

Partial n decays 1 + partial_decay_growth * (n - 1) times faster than the fundamental, which is what makes a note mellow as it rings.

TYPE: float

damped_decay

Decay rate, in 1/s, once the damper is down. Partials that already decay faster keep their own rate.

TYPE: float

audio_substeps

Samples emitted per physics step. Must match every other audio participant in the scene. The default of 80 is a 16 kHz rate at Eden's default sim_dt=0.005, which carries every fundamental on the keyboard; the 20 of the other audio sensors' defaults would leave everything above A5 mute.

TYPE: int

Examples:

>>> from eden.options import SensorOptions
>>> from eden.engine.sensors.piano_audio import PianoAudio
>>> voice = SensorOptions(sensor=PianoAudio(), attach_entity_name="piano")

Attributes

key_notes class-attribute instance-attribute
key_notes: dict[str, Annotated[int, Field(ge=0, le=127)]] | None = None
lowest_midi_note class-attribute instance-attribute
lowest_midi_note: int = Field(default=21, ge=0, le=127)
tuning class-attribute instance-attribute
tuning: PositiveFloat = 440.0
strike_fraction class-attribute instance-attribute
strike_fraction: UnitInterval = 0.87
release_fraction class-attribute instance-attribute
release_fraction: UnitInterval = 0.5
velocity_ref class-attribute instance-attribute
velocity_ref: PositiveFloat = 6.0
n_partials class-attribute instance-attribute
n_partials: PositiveInt = 6
inharmonicity class-attribute instance-attribute
inharmonicity: float = Field(default=0.0004, ge=0.0)
partial_rolloff class-attribute instance-attribute
partial_rolloff: float = Field(default=1.3, ge=0.0)
ring_decay class-attribute instance-attribute
ring_decay: PositiveFloat = 1.2
decay_pitch_exponent class-attribute instance-attribute
decay_pitch_exponent: float = 0.6
partial_decay_growth class-attribute instance-attribute
partial_decay_growth: float = Field(default=0.6, ge=0.0)
damped_decay class-attribute instance-attribute
damped_decay: PositiveFloat = 40.0
audio_substeps class-attribute instance-attribute
audio_substeps: PositiveInt = 80

Methods:

validate_scene
validate_scene(scene: Scene)

Require a real rigid entity, and refuse the link attachment this sensor would ignore.

PARAMETER DESCRIPTION
scene

The scene the sensor is being added to.

TYPE: Scene

Source code in eden/engine/sensors/piano_audio.py
def validate_scene(self, scene: "Scene"):
    """Require a real rigid entity, and refuse the link attachment this sensor would ignore.

    Parameters
    ----------
    scene : Scene
        The scene the sensor is being added to.
    """
    if self.entity_idx < 0:
        gs.raise_exception(
            "PianoAudio needs the keyboard entity it voices, but entity_idx is still -1. Set "
            "`attach_entity_name` on the enclosing eden SensorOptions (or entity_idx directly when adding the "
            "sensor through Genesis)."
        )
    super().validate_scene(scene)
    ignored = {name: getattr(self, name) for name in ("link_idx_local", "euler_offset")}
    set_anyway = {name: value for name, value in ignored.items() if any(v != 0 for v in np.atleast_1d(value))}
    if set_anyway:
        gs.raise_exception(
            f"PianoAudio voices a whole keyboard and radiates from its base link, but got {set_anyway}. Drop "
            "`attach_link_name` from the enclosing eden SensorOptions; use `pos_offset` to move the point the "
            "sound comes from."
        )

PianoVoice

PianoVoice(options: PianoAudio, solver: RigidSolver, dof_idx: list[int], notes: list[int], emit_link: int)

Bases: AudioSource

Synthesis for one :class:PianoAudioSensor: every string of one keyboard, mixed to one emission point.

Owns the state of each partial, a phasor (c, s) whose signal over the next block is envelope[k] * (c * sin((k + 1) * w) + s * cos((k + 1) * w)), plus what it needs to tell a strike from a key that is merely down.

Allocate the strings of the keys resolved by the owning sensor.

PARAMETER DESCRIPTION
options

The owning sensor's options.

TYPE: PianoAudio

solver

Solver to read the key positions from.

TYPE: RigidSolver

dof_idx

Solver-global DOF index of each key.

TYPE: list[int]

notes

MIDI note number each key plays, parallel to dof_idx.

TYPE: list[int]

emit_link

Solver-global index of the link the instrument radiates from.

TYPE: int

Source code in eden/engine/sensors/piano_audio.py
def __init__(
    self, options: PianoAudio, solver: "RigidSolver", dof_idx: list[int], notes: list[int], emit_link: int
):
    """Allocate the strings of the keys resolved by the owning sensor.

    Parameters
    ----------
    options : PianoAudio
        The owning sensor's options.
    solver : RigidSolver
        Solver to read the key positions from.
    dof_idx : list[int]
        Solver-global DOF index of each key.
    notes : list[int]
        MIDI note number each key plays, parallel to ``dof_idx``.
    emit_link : int
        Solver-global index of the link the instrument radiates from.
    """
    self._solver = solver
    sim = solver._sim
    B = sim._B
    K = options.audio_substeps
    n_keys = len(dof_idx)
    n_partials = options.n_partials
    self._K = K
    self._dt = sim.dt
    dt_sub = sim.dt / K
    # A sampled sine is exact at any pitch below Nyquist, so the strings run up to 90% of it. The other sources
    # stop at 45%, which their two-pole filters need and these oscillators do not.
    band_edge = 0.9 * 0.5 / dt_sub

    self._dof_idx = torch.tensor(dof_idx, dtype=gs.tc_int, device=gs.device)
    self._emit_links_t = torch.tensor([emit_link], dtype=gs.tc_int, device=gs.device)
    offset = torch.tensor(options.pos_offset, dtype=gs.tc_float, device=gs.device)
    self._emit_offset_t = offset.expand(B, 1, 3).contiguous()
    self._velocity_ref = options.velocity_ref

    lower, upper = solver.get_dofs_limit(self._dof_idx)
    lower, upper = lower.reshape(-1, n_keys)[0], upper.reshape(-1, n_keys)[0]
    unbounded = torch.nonzero(~torch.isfinite(upper - lower) | (upper <= lower)).flatten()
    if unbounded.numel() > 0:
        gs.raise_exception(
            f"PianoAudio: key {int(unbounded[0])} (DOF {dof_idx[int(unbounded[0])]}) has no finite joint range, so "
            "there is no stroke to place the strike point on. Every key joint needs a lower and an upper limit."
        )
    self._strike_at = lower + options.strike_fraction * (upper - lower)  # (n_keys,)
    self._release_at = lower + options.release_fraction * (upper - lower)

    # Everything below is built in float64 and only the finished tables are cast down.
    f64 = torch.float64
    notes_t = torch.tensor(notes, dtype=f64, device=gs.device)
    f0 = options.tuning * 2.0 ** ((notes_t - 69.0) / 12.0)  # (n_keys,)
    n = torch.arange(1, n_partials + 1, dtype=f64, device=gs.device)
    freq = f0[:, None] * n * torch.sqrt(1.0 + options.inharmonicity * n * n)  # (n_keys, n_partials)
    ring = (
        options.ring_decay
        * (f0[:, None] / options.tuning) ** options.decay_pitch_exponent
        * (1.0 + options.partial_decay_growth * (n - 1.0))
    )
    damped = ring.clamp(min=options.damped_decay)

    carried = freq < band_edge
    mute = ~carried[:, 0]
    if mute.any():
        en.logger.warning(
            f"PianoAudio: the {1.0 / dt_sub:.0f} Hz audio rate carries nothing above {band_edge:.0f} Hz, so "
            f"{int(mute.sum())} of {n_keys} keys are mute, from MIDI note {int(notes_t[mute].min())} "
            f"({float(f0[mute].min()):.0f} Hz) up. Raise `audio_substeps` to hear them."
        )
    # A partial the rate cannot carry is dropped rather than folded down to a wrong pitch.
    self._gain = (n**-options.partial_rolloff * carried).to(gs.tc_float)  # (n_keys, n_partials)

    steps = torch.arange(1, K + 1, dtype=f64, device=gs.device)
    phase = 2.0 * torch.pi * freq[..., None] * dt_sub * steps  # (n_keys, n_partials, K)
    oscillation = torch.stack((torch.sin(phase), torch.cos(phase)), dim=2)  # (n_keys, n_partials, 2, K)
    ring_env = torch.exp(-ring[..., None] * dt_sub * steps)
    damped_env = torch.exp(-damped[..., None] * dt_sub * steps)
    # Row (damper, key, partial, sin | cos) is the block one unit of that phasor component rings out under that
    # damper, so a block is the phasors times these tables; see `emit`.
    tables = torch.stack((ring_env[:, :, None] * oscillation, damped_env[:, :, None] * oscillation))
    tables = tables.reshape(-1, K).to(gs.tc_float)  # (2 * n_keys * n_partials * 2, K)
    # A float32 matmul on CUDA may run in TF32, which Eden enables by default (`eden.init(allow_tf32=True)`): 10
    # mantissa bits, a -70 dB error on every sample. Splitting both sides into a TF32-exact part and a remainder
    # and multiplying out three of the four products recovers float32 accuracy, whatever the global flag says.
    # With `_tables_lo` set, `_tables` holds only the TF32-exact part.
    self._tables, self._tables_lo = tables, None
    if gs.tc_float == torch.float32 and gs.device.type == "cuda":
        self._tables, self._tables_lo = _split_tf32(tables)
    # Carrying a phasor across a block: decay it by the envelope's last sample, turn it by the block's phase.
    self._ring_end = ring_env[..., -1].to(gs.tc_float)  # (n_keys, n_partials)
    self._damped_end = damped_env[..., -1].to(gs.tc_float)
    self._end_sin = torch.sin(phase[..., -1]).to(gs.tc_float)
    self._end_cos = torch.cos(phase[..., -1]).to(gs.tc_float)

    self._c = torch.zeros((B, n_keys, n_partials), dtype=gs.tc_float, device=gs.device)
    self._s = torch.zeros((B, n_keys, n_partials), dtype=gs.tc_float, device=gs.device)
    self._prev_q = torch.zeros((B, n_keys), dtype=gs.tc_float, device=gs.device)
    self._prev_speed = torch.zeros((B, n_keys), dtype=gs.tc_float, device=gs.device)
    self._down = torch.zeros((B, n_keys), dtype=torch.bool, device=gs.device)
    # An environment's first step after a reset only takes stock: a key already down was not just struck.
    self._fresh = torch.ones((B,), dtype=torch.bool, device=gs.device)
    self._sustain = torch.zeros((B,), dtype=torch.bool, device=gs.device)
    self._block = torch.zeros((B, 1, K), dtype=gs.tc_float, device=gs.device)
    # Every block since `start_recording`, or None while not recording. `_has_recorded` separates the two
    # ways of being None — never started, and started then stopped — so neither is reported as the other.
    self._recorded: list[torch.Tensor] | None = None
    self._has_recorded = False

Attributes

block property
block: Tensor
emit_links: Tensor
emit_offset property
emit_offset: Tensor
audio_substeps property
audio_substeps: int
keys_down property
keys_down: Tensor

(B, n_keys) bool: keys past their strike point and not yet back up past their release point.

A copy, so it can be kept as the previous step's state: the voice overwrites its own every step.

Methods:

set_sustain
set_sustain(down, envs_idx=None) -> None

Press or lift the sustain pedal.

PARAMETER DESCRIPTION
down

Whether the pedal is down: one value, or one per selected environment.

TYPE: bool | Tensor

envs_idx

Environments to set. None sets all of them.

TYPE: array - like | None DEFAULT: None

Source code in eden/engine/sensors/piano_audio.py
def set_sustain(self, down, envs_idx=None) -> None:
    """Press or lift the sustain pedal.

    Parameters
    ----------
    down : bool | torch.Tensor
        Whether the pedal is down: one value, or one per selected environment.
    envs_idx : array-like | None
        Environments to set. ``None`` sets all of them.
    """
    idx = slice(None) if envs_idx is None else envs_idx
    self._sustain[idx] = torch.as_tensor(down, dtype=torch.bool, device=gs.device)
start_recording
start_recording() -> None

Start keeping every synthesized block.

Refuses a second start while one is open, as :meth:~eden.entities.camera.Camera.start_recording does: the only thing it could do instead is throw away what the first one kept, and there is no getting that back.

Source code in eden/engine/sensors/piano_audio.py
def start_recording(self) -> None:
    """Start keeping every synthesized block.

    Refuses a second start while one is open, as :meth:`~eden.entities.camera.Camera.start_recording` does:
    the only thing it could do instead is throw away what the first one kept, and there is no getting that
    back.
    """
    if self._recorded is not None:
        gs.raise_exception(
            f"PianoAudio: already recording, with {len(self._recorded)} block(s) kept. Starting again would "
            f"drop them. Call take_recording() to collect them and go on recording, or stop_recording() to "
            f"throw them away on purpose."
        )
    self._recorded = []
    self._has_recorded = True
take_recording
take_recording() -> Tensor

Return what was synthesized since the last call (or since recording started), and keep recording.

RETURNS DESCRIPTION
Tensor

(B, n_blocks * audio_substeps) samples, empty along the last axis if no step has run since.

Source code in eden/engine/sensors/piano_audio.py
def take_recording(self) -> torch.Tensor:
    """Return what was synthesized since the last call (or since recording started), and keep recording.

    Returns
    -------
    torch.Tensor
        ``(B, n_blocks * audio_substeps)`` samples, empty along the last axis if no step has run since.
    """
    if self._recorded is None:
        # Two different mistakes, and the fix differs: one call is missing, or one came too early.
        gs.raise_exception(
            "PianoAudio: take_recording() after stop_recording(), which dropped what had been kept. Take the "
            "samples before stopping, or leave the recording open."
            if self._has_recorded
            else "PianoAudio: take_recording() was called without start_recording()."
        )
    blocks, self._recorded = self._recorded, []
    if not blocks:
        return torch.zeros((self._block.shape[0], 0), dtype=gs.tc_float, device=gs.device)
    return torch.cat(blocks, dim=-1)
stop_recording
stop_recording() -> None

Stop keeping blocks and drop what was kept.

Refuses when no recording is open, as :meth:~eden.entities.camera.Camera.stop_recording does: a stop with nothing to stop is a mistake in the calling loop far more often than an intent.

Source code in eden/engine/sensors/piano_audio.py
def stop_recording(self) -> None:
    """Stop keeping blocks and drop what was kept.

    Refuses when no recording is open, as :meth:`~eden.entities.camera.Camera.stop_recording` does: a stop
    with nothing to stop is a mistake in the calling loop far more often than an intent.
    """
    if self._recorded is None:
        gs.raise_exception(
            "PianoAudio: not recording, so there is nothing to stop. start_recording() opens one; "
            "take_recording() collects what it holds without closing it."
        )
    self._recorded = None
reset
reset(envs_idx) -> None

Silence every string, lift the pedal and forget which keys were down, for envs_idx.

PARAMETER DESCRIPTION
envs_idx

Environments to reset. None resets all of them.

TYPE: array - like | None

Source code in eden/engine/sensors/piano_audio.py
def reset(self, envs_idx) -> None:
    """Silence every string, lift the pedal and forget which keys were down, for ``envs_idx``.

    Parameters
    ----------
    envs_idx : array-like | None
        Environments to reset. ``None`` resets all of them.
    """
    idx = slice(None) if envs_idx is None else envs_idx
    for tensor in (self._c, self._s, self._prev_speed, self._block):
        tensor[idx] = 0.0
    self._down[idx] = False
    self._sustain[idx] = False
    self._fresh[idx] = True
emit
emit() -> None

Throw the hammers of the keys that just passed their strike point, then ring every string one block.

Source code in eden/engine/sensors/piano_audio.py
def emit(self) -> None:
    """Throw the hammers of the keys that just passed their strike point, then ring every string one block."""
    q = self._solver.get_dofs_position(self._dof_idx)
    if self._solver.n_envs == 0:
        q = q[None]  # (B, n_keys)

    fresh = self._fresh[:, None]
    # The key's speed as it passed the strike point, from finite differences rather than the joint velocity,
    # which reads zero once the key has met its stop. A fast key meets it within the very step that carried it
    # past the strike point, cutting that step's travel short, so the step before counts too.
    step_speed = torch.where(fresh, 0.0, (q - self._prev_q) / self._dt)
    speed = torch.maximum(step_speed, self._prev_speed)
    self._prev_speed.copy_(step_speed)
    down = (q >= self._strike_at) | (self._down & (q > self._release_at))
    struck = down & ~self._down & ~fresh
    level = (speed / self._velocity_ref).clamp(min=0.0, max=1.0) * struck  # (B, n_keys)
    self._down.copy_(down)
    self._prev_q.copy_(q)
    self._fresh.fill_(False)

    # A strike starts each partial at a zero crossing, so the note begins without a click.
    self._c += level[..., None] * self._gain

    damped = ~(down | self._sustain[:, None])  # (B, n_keys)
    # One matrix product with the tables: each phasor goes into the rows of the envelope it rings under this
    # block, and zeros into the other's. Nothing the size of (B, n_keys, P, K) is ever built.
    phasor = torch.stack((self._c, self._s), dim=-1)  # (B, n_keys, P, 2)
    ringing = torch.where(damped[..., None, None], 0.0, phasor)
    coefficients = torch.stack((ringing, phasor - ringing), dim=1).flatten(1)  # (B, 2 * n_keys * P * 2)
    if self._tables_lo is None:
        self._block[:, 0] = coefficients @ self._tables
    else:
        hi, lo = _split_tf32(coefficients)
        self._block[:, 0] = (hi @ self._tables).addmm_(lo, self._tables).addmm_(hi, self._tables_lo)

    # Carry each phasor to the end of the block: decay it, and turn it by the phase the block covered.
    end_env = torch.where(damped[..., None], self._damped_end, self._ring_end)  # (B, n_keys, P)
    c, s = self._c, self._s
    self._c = end_env * (c * self._end_cos - s * self._end_sin)
    self._s = end_env * (c * self._end_sin + s * self._end_cos)
    if self._recorded is not None:
        self._recorded.append(self._block[:, 0].clone())

PianoAudioMetadata dataclass

PianoAudioMetadata(voices: list[PianoVoice] = list(), cache_offsets: list[int] = list(), last_render: int = 0)

Bases: SimpleSensorMetadata

Per-class state of every :class:PianoAudioSensor: each one's voice and its offset in the read cache.

Attributes

voices class-attribute instance-attribute
voices: list[PianoVoice] = field(default_factory=list)
cache_offsets class-attribute instance-attribute
cache_offsets: list[int] = field(default_factory=list)
last_render class-attribute instance-attribute
last_render: int = 0

Methods:

destroy
destroy()

Drop the voices, releasing their synthesis buffers.

Source code in eden/engine/sensors/piano_audio.py
def destroy(self):
    """Drop the voices, releasing their synthesis buffers."""
    self.voices.clear()
    self.cache_offsets.clear()

PianoAudioSensor

PianoAudioSensor(options: PianoAudio, idx: int, shared_context: AudioContext, shared_metadata: PianoAudioMetadata, manager: SensorManager)

Bases: SimpleSensor[PianoAudio, AudioContext, PianoAudioMetadata]

Registers a :class:PianoVoice with the audio registry and reads back its block.

Source code in eden/engine/sensors/piano_audio.py
def __init__(
    self,
    options: PianoAudio,
    idx: int,
    shared_context: AudioContext,
    shared_metadata: PianoAudioMetadata,
    manager: "SensorManager",
):
    self._voice: PianoVoice | None = None
    super().__init__(options, idx, shared_context, shared_metadata, manager)

Attributes

keys_down property
keys_down: Tensor

(B, n_keys) bool: which keys are down, by the same test that throws the hammers.

A copy, safe to keep across steps. Columns follow key_notes, or the entity's joint order without it.

Methods:

build
build()

Build this sensor's voice and register it with the scene's audio registry.

Source code in eden/engine/sensors/piano_audio.py
def build(self):
    """Build this sensor's voice and register it with the scene's audio registry."""
    super().build()

    dof_idx, notes, base_link = self._resolve_keys()
    self._voice = PianoVoice(self._options, self._manager._sim.rigid_solver, dof_idx, notes, base_link)
    self._shared_context.register(self._voice, type(self._options).__name__)

    sm = self._shared_metadata
    sm.voices.append(self._voice)
    sm.cache_offsets.append(self._cache_offset)
set_sustain
set_sustain(down, envs_idx=None) -> None

Press or lift the sustain pedal. While it is down, released keys keep ringing.

PARAMETER DESCRIPTION
down

Whether the pedal is down: one value, or one per selected environment.

TYPE: bool | Tensor

envs_idx

Environments to set. None sets all of them.

TYPE: array - like | None DEFAULT: None

Source code in eden/engine/sensors/piano_audio.py
def set_sustain(self, down, envs_idx=None) -> None:
    """Press or lift the sustain pedal. While it is down, released keys keep ringing.

    Parameters
    ----------
    down : bool | torch.Tensor
        Whether the pedal is down: one value, or one per selected environment.
    envs_idx : array-like | None
        Environments to set. ``None`` sets all of them.
    """
    self._voice.set_sustain(down, envs_idx)
start_recording
start_recording() -> None

Start keeping every block the strings synthesize, one per simulation step.

read() returns the block of the latest simulation step only, so a loop that steps the environment once per control step (decimation simulation steps) reads one block in decimation. A recording has them all: it is how to listen to a policy that acts at 20 Hz. The signal is the instrument's own, before any microphone's distance and delay.

Unlike :meth:~eden.entities.camera.Camera.start_recording, which streams to a file, this holds what it keeps in memory for every environment at once — num_envs * audio_substeps floats a simulation step, 65 MB/s at 1024 environments and the default 80 substeps. :meth:take_recording empties it, so a loop that drains each step holds one step's worth; one that never drains grows without bound. Starting a second recording while one is open raises rather than dropping what it holds, and a reset does not cut the recording: episodes are concatenated, at a different sample in each row.

Source code in eden/engine/sensors/piano_audio.py
def start_recording(self) -> None:
    """Start keeping every block the strings synthesize, one per *simulation* step.

    ``read()`` returns the block of the latest simulation step only, so a loop that steps the environment once
    per control step (``decimation`` simulation steps) reads one block in ``decimation``. A recording has them
    all: it is how to listen to a policy that acts at 20 Hz. The signal is the instrument's own, before any
    microphone's distance and delay.

    Unlike :meth:`~eden.entities.camera.Camera.start_recording`, which streams to a file, this holds what it
    keeps in memory for every environment at once — ``num_envs * audio_substeps`` floats a simulation step,
    65 MB/s at 1024 environments and the default 80 substeps. :meth:`take_recording` empties it, so a loop
    that drains each step holds one step's worth; one that never drains grows without bound. Starting a
    second recording while one is open raises rather than dropping what it holds, and a reset does not cut
    the recording: episodes are concatenated, at a different sample in each row.
    """
    self._voice.start_recording()
take_recording
take_recording() -> Tensor

Return the samples synthesized since the last call, and keep recording.

RETURNS DESCRIPTION
Tensor

(B, n_samples); calling it once per env.step() yields decimation * audio_substeps samples each time, ready for :class:~eden.engine.sensors.playback.LiveAudioPlayer.

Source code in eden/engine/sensors/piano_audio.py
def take_recording(self) -> torch.Tensor:
    """Return the samples synthesized since the last call, and keep recording.

    Returns
    -------
    torch.Tensor
        ``(B, n_samples)``; calling it once per ``env.step()`` yields ``decimation * audio_substeps`` samples
        each time, ready for :class:`~eden.engine.sensors.playback.LiveAudioPlayer`.
    """
    return self._voice.take_recording()
stop_recording
stop_recording() -> None

Stop keeping blocks and drop what was kept, raising if no recording is open.

Source code in eden/engine/sensors/piano_audio.py
def stop_recording(self) -> None:
    """Stop keeping blocks and drop what was kept, raising if no recording is open."""
    self._voice.stop_recording()

Functions:

Audio sources and propagation

The audio source registry and delay line every Eden audio sensor shares.

Eden's audio addon runs the usual three-stage pipeline: synthesis (a body turns physics into a block of samples), propagation (the samples travel, attenuating and arriving late), rendering (a microphone sums what reaches it). Sources publish into the registry defined here; microphones render what they find in it, with no knowledge of concrete source types.

Where this lives is the whole reason the addon needs no Genesis fork. Upstream owns the registry as a scene-level AudioManager, which takes three Genesis edits to exist: Simulator to construct it, Scene.step to pump it, and Scene.add_audio_source to fill it. Genesis already ships a mechanism with exactly those lifecycle hooks — SharedSensorContext, a resource shared across sensor types that SensorManager constructs on first use, refreshes once per step, resets, and destroys. Hosting the registry there gets the same behaviour as a pure addition.

The ordering that matters comes for free. SensorManager.step calls context.update() before the per-sensor-type update loop, so every source has synthesized the current step's block by the time any microphone renders — whatever order the sensors were declared in. A source that synthesized inside its own sensor's update hook would instead be at the mercy of declaration order, and a microphone declared first would render a block one step stale.

The delay line lives here for a related but sharper reason: it must advance on every simulation step, and a microphone does not. Eden skips the per-sensor-type update loop on intermediate decimation sub-steps (EnvOptions.update_sensors_every_substep) while context.update() still runs, so a history advanced from a microphone's render hook would drop every block but the last of each control step — silently shortening every propagation delay. Advancing it here keeps the recorded sound continuous no matter how often anything renders it.

Classes

AudioSource

Bases: ABC

One radiating body in the scene: the synthesis stage of the audio pipeline.

A source generates sound from physics and is never read by the user — a microphone (:class:~eden.engine.sensors.spatial_audio.SpatialAudioSensor) renders it. Subclasses own their sample buffers and expose the radiation interface below so receivers stay decoupled from concrete source types.

An emission point is one place sound leaves the body: a motor source has one per actuated DOF, a contact source one per contact patch. All three tensors are indexed by that axis.

Attributes

block abstractmethod property
block: Tensor

(B, n_emit, audio_substeps) most recently synthesized samples per emission point.

emit_links: Tensor

(n_emit,) solver-global rigid link index each emission point rides. -1 is static.

Solver-global indices (RigidLink.idx), the links_idx of RigidSolver.get_links_pos() — not entity-local idx_local. Read once, when the source registers.

emit_offset abstractmethod property
emit_offset: Tensor

(B, n_emit, 3) offset of each emission point in its link's frame, or world position if static.

Read every step, so a source whose emission points move on the body (a contact patch) may update it.

audio_substeps abstractmethod property
audio_substeps: int

Samples this source emits per simulation step. Must match every other participant.

Methods:

emit abstractmethod
emit() -> None

Synthesize this step's block into the source's own persistent state.

Called once per simulation step by :meth:AudioContext.update, before anything renders it.

Source code in eden/engine/sensors/audio.py
@abstractmethod
def emit(self) -> None:
    """Synthesize this step's block into the source's own persistent state.

    Called once per simulation step by :meth:`AudioContext.update`, before anything renders it.
    """
reset abstractmethod
reset(envs_idx) -> None

Clear persistent synthesis state (filter memory, oscillator phase) for envs_idx.

PARAMETER DESCRIPTION
envs_idx

Environments to reset. None resets all of them.

TYPE: array - like | None

Source code in eden/engine/sensors/audio.py
@abstractmethod
def reset(self, envs_idx) -> None:
    """Clear persistent synthesis state (filter memory, oscillator phase) for ``envs_idx``.

    Parameters
    ----------
    envs_idx : array-like | None
        Environments to reset. ``None`` resets all of them.
    """

AudioContext

AudioContext(sim)

Bases: SharedSensorContext

Registry of :class:AudioSource objects plus the delay line carrying their sound.

Held by SensorManager as the shared context of the audio sensor classes, so exactly one instance exists per scene however many audio sensors are declared. See the module docstring for why both the registry and the history live here rather than on the scene or on a microphone.

The delay line is a ring of whole blocks: :meth:update overwrites the oldest block in place, and a reader maps a sample's age to its slot through :attr:hist_newest. Nothing is allocated per step.

Source code in eden/engine/sensors/audio.py
def __init__(self, sim):
    super().__init__(sim)
    self._sources: list[AudioSource] = []
    # The scene-wide sample rate multiplier, adopted from the first participant to declare one.
    # 0 means "not yet claimed"; `require_audio_substeps` fixes it and rejects disagreement.
    self._audio_substeps = 0
    # Delay-line depth in samples requested by the microphones' propagation reach at build.
    self._hist_len = 0
    # (B, n_src, H) ring of samples. The source count is only final once every source has registered, so
    # this and the per-emission-point tensors below are allocated on the first update rather than at build.
    self._src_hist = torch.empty(0)
    self._hist_newest = -1
    self._src_offset = torch.empty(0)
    self._src_is_static = torch.empty(0, dtype=torch.bool)
    self._attached_cols = torch.empty(0, dtype=torch.long)
    self._attached_links = torch.empty(0, dtype=gs.tc_int)
    # (first emission-point column, source) for every source that radiates.
    self._src_slices: list[tuple[int, AudioSource]] = []
    # Blocks synthesized so far. A consumer remembers the value it last rendered at, which tells it how
    # many blocks it skipped; see `blocks_since`.
    self._n_updates = 0
    self._warned_gapped = False

Attributes

audio_substeps property
audio_substeps: int

The scene-wide samples-per-step every audio participant agrees on.

sources property
sources: tuple[AudioSource, ...]

Every registered source. Empty when microphones exist but nothing radiates.

src_hist property
src_hist: Tensor

(B, n_src, H) ring of samples of every emission point; see :attr:hist_newest for the order.

Empty until the first :meth:update with at least one radiating source.

hist_newest property
hist_newest: int

Slot of the newest sample in :attr:src_hist. A sample a samples old is at (hist_newest - a) % H.

n_updates property
n_updates: int

Blocks synthesized so far, for :meth:blocks_since.

Methods:

activate
activate() -> None

Mark the registry live. Idempotent; the delay line is allocated on first use.

Source code in eden/engine/sensors/audio.py
def activate(self) -> None:
    """Mark the registry live. Idempotent; the delay line is allocated on first use."""
    self._active = True
register
register(source: AudioSource, owner: str) -> None

Add source to the registry and activate it. Call from a producing sensor's build.

PARAMETER DESCRIPTION
source

The source to register. Its audio_substeps must agree with every other participant.

TYPE: AudioSource

owner

Name of the options class that declared the source, for the error message.

TYPE: str

Source code in eden/engine/sensors/audio.py
def register(self, source: AudioSource, owner: str) -> None:
    """Add ``source`` to the registry and activate it. Call from a producing sensor's ``build``.

    Parameters
    ----------
    source : AudioSource
        The source to register. Its ``audio_substeps`` must agree with every other participant.
    owner : str
        Name of the options class that declared the source, for the error message.
    """
    if self._src_hist.numel() > 0:
        gs.raise_exception(
            f"{owner} registered an audio source after the delay line was sized for {self._src_hist.shape[1]} "
            "emission points. Register every source at build, before the registry's first update."
        )
    self.require_audio_substeps(source.audio_substeps, owner)
    self._sources.append(source)
    self._active = True
require_audio_substeps
require_audio_substeps(audio_substeps: int, who: str) -> None

Claim the scene-wide audio_substeps, or fail if it disagrees with the claim on record.

Sources and microphones exchange fixed-length sample blocks, so one rate has to hold across the scene. Checking it here — at build, naming both values — beats discovering a ragged block inside the render loop.

PARAMETER DESCRIPTION
audio_substeps

The caller's samples-per-step.

TYPE: int

who

Name of the calling participant, for the error message.

TYPE: str

Source code in eden/engine/sensors/audio.py
def require_audio_substeps(self, audio_substeps: int, who: str) -> None:
    """Claim the scene-wide ``audio_substeps``, or fail if it disagrees with the claim on record.

    Sources and microphones exchange fixed-length sample blocks, so one rate has to hold across
    the scene. Checking it here — at build, naming both values — beats discovering a ragged block
    inside the render loop.

    Parameters
    ----------
    audio_substeps : int
        The caller's samples-per-step.
    who : str
        Name of the calling participant, for the error message.
    """
    if self._audio_substeps == 0:
        self._audio_substeps = audio_substeps
    elif self._audio_substeps != audio_substeps:
        gs.raise_exception(
            f"{who} declares audio_substeps={audio_substeps} but the scene's audio pipeline is already "
            f"running at audio_substeps={self._audio_substeps}. Every audio source and microphone in a "
            "scene must share one sample rate."
        )
require_history
require_history(samples: int) -> None

Ensure the delay line is at least samples deep. Call from a consuming sensor's build.

PARAMETER DESCRIPTION
samples

Depth this consumer needs: its longest propagation delay plus one block.

TYPE: int

Source code in eden/engine/sensors/audio.py
def require_history(self, samples: int) -> None:
    """Ensure the delay line is at least ``samples`` deep. Call from a consuming sensor's ``build``.

    Parameters
    ----------
    samples : int
        Depth this consumer needs: its longest propagation delay plus one block.
    """
    self._hist_len = max(self._hist_len, samples)
update
update() -> None

Synthesize every source's block and write it over the oldest block of the delay line.

Manager-driven, once per simulation step, before anything renders.

Source code in eden/engine/sensors/audio.py
def update(self) -> None:
    """Synthesize every source's block and write it over the oldest block of the delay line.

    Manager-driven, once per simulation step, before anything renders.
    """
    if not self._active:
        return
    for source in self._sources:
        source.emit()
    if self._src_hist.numel() == 0 and not self._allocate():
        return

    K = self._audio_substeps
    start = (self._hist_newest + 1) % self._src_hist.shape[2]
    for col, source in self._src_slices:
        n_emit = source.emit_links.shape[0]
        self._src_hist[:, col : col + n_emit, start : start + K] = source.block
        self._src_offset[:, col : col + n_emit] = source.emit_offset
    self._hist_newest = start + K - 1
    self._n_updates += 1
source_positions
source_positions() -> Tensor

(B, n_src, 3) world position of every emission point, parallel to :attr:src_hist.

An emission point rides its link with the offset rotated into the link's frame; a static one sits at its offset. Poses are the solver-frame ones (relative=False), matching eden.entities.sensor.Sensor.

Source code in eden/engine/sensors/audio.py
def source_positions(self) -> torch.Tensor:
    """``(B, n_src, 3)`` world position of every emission point, parallel to :attr:`src_hist`.

    An emission point rides its link with the offset rotated into the link's frame; a static one sits at its
    offset. Poses are the solver-frame ones (``relative=False``), matching ``eden.entities.sensor.Sensor``.
    """
    self._assert_active("source_positions")
    pos = self._src_offset.clone()
    if self._attached_cols.numel() > 0:
        solver = self._sim.rigid_solver
        links_pos = solver.get_links_pos(links_idx=self._attached_links)
        links_quat = solver.get_links_quat(links_idx=self._attached_links)
        if solver.n_envs == 0:
            links_pos, links_quat = links_pos[None], links_quat[None]
        pos[:, self._attached_cols] = links_pos + transform_by_quat(pos[:, self._attached_cols], links_quat)
    return pos
blocks_since
blocks_since(last_render: int, who: str) -> int

Blocks synthesized since a consumer last rendered at n_updates == last_render; warn once if over 1.

A consumer reads the newest block, so one render per :meth:update yields a continuous signal. Eden's default EnvOptions.update_sensors_every_substep=False skips sensor updates on intermediate decimation sub-steps, so with decimation=4 a sensor renders every fourth block and its read() is a gapped stream — fine as an observation, wrong as a recording. That distinction is invisible in the data, and the decimation is Eden's, which a Genesis sensor cannot see at build, so say so on the first gapped render.

PARAMETER DESCRIPTION
last_render

:attr:n_updates as it stood at the consumer's previous render.

TYPE: int

who

Name of the consuming sensor's options class, for the warning.

TYPE: str

Source code in eden/engine/sensors/audio.py
def blocks_since(self, last_render: int, who: str) -> int:
    """Blocks synthesized since a consumer last rendered at ``n_updates == last_render``; warn once if over 1.

    A consumer reads the newest block, so one render per :meth:`update` yields a continuous signal. Eden's
    default ``EnvOptions.update_sensors_every_substep=False`` skips sensor updates on intermediate decimation
    sub-steps, so with ``decimation=4`` a sensor renders every fourth block and its ``read()`` is a gapped
    stream — fine as an observation, wrong as a recording. That distinction is invisible in the data, and
    the decimation is Eden's, which a Genesis sensor cannot see at build, so say so on the first gapped render.

    Parameters
    ----------
    last_render : int
        :attr:`n_updates` as it stood at the consumer's previous render.
    who : str
        Name of the consuming sensor's options class, for the warning.
    """
    n_blocks = self._n_updates - last_render
    if n_blocks > 1 and not self._warned_gapped:
        self._warned_gapped = True
        en.logger.warning(
            f"{who} is rendered once per {n_blocks} synthesized audio blocks, so read() is a gapped stream "
            "rather than continuous sound. Propagation delay, attenuation and Doppler stay correct (the delay "
            "line advances every step), and this is harmless when the signal is an observation. Set "
            "EnvOptions.update_sensors_every_substep=True to render every block, e.g. when writing the signal "
            "to an audio file."
        )
    return n_blocks
reset
reset(envs_idx) -> None

Clear every source's synthesis state and the delay line for envs_idx.

PARAMETER DESCRIPTION
envs_idx

Environments being reset. None resets all of them.

TYPE: array - like | None

Source code in eden/engine/sensors/audio.py
def reset(self, envs_idx) -> None:
    """Clear every source's synthesis state and the delay line for ``envs_idx``.

    Parameters
    ----------
    envs_idx : array-like | None
        Environments being reset. ``None`` resets all of them.
    """
    if not self._active:
        return
    for source in self._sources:
        source.reset(envs_idx)
    if self._src_hist.numel() > 0:
        self._src_hist[slice(None) if envs_idx is None else envs_idx] = 0.0
destroy
destroy() -> None

Drop every registered source and the delay line, releasing their buffers.

Source code in eden/engine/sensors/audio.py
def destroy(self) -> None:
    """Drop every registered source and the delay line, releasing their buffers."""
    self._sources.clear()
    self._src_slices.clear()
    self._src_hist = torch.empty(0)
    self._src_offset = torch.empty(0)
    self._hist_newest = -1
    self._audio_substeps = 0
    self._hist_len = 0
    self._active = False

Functions:

Microphone

Airborne point microphone: the rendering stage of the audio pipeline.

Provenance

Ported from neuroagents-lab/tactile-genesis @ 3023bcd (Genesis/genesis/engine/sensors/spatial_audio.py), where it is a Genesis in-tree sensor reading a scene-level AudioManager. Here it renders :class:~eden.engine.sensors.audio.AudioContext instead, which needs no Genesis change; see :mod:eden.engine.sensors.audio.

Deviations from upstream: enable_occlusion is dropped (upstream declares it and never reads it); attenuation is a Literal rather than a free str whose typos fell back to inverse; the sample history lives on the context, not the microphone; and the link attachment goes through Genesis' KinematicSensorMixin, so pos_offset is link-local and set_pos_offset works as on any other sensor.

Limitation: free-field propagation only — no occlusion, no reflections, no room response. A microphone behind a wall hears the source as if the wall were not there.

Classes

SpatialAudio

Bases: KinematicSensorOptionsMixin['SpatialAudioSensor'], SimpleSensorOptions['SpatialAudioSensor']

A mono point microphone that hears every audio source registered with the scene.

read() returns (B, audio_substeps): this step's block of samples, one microphone signal per environment. Sources are summed with ref_distance-referenced distance attenuation and delayed by distance / speed_of_sound through a fractional delay line.

PARAMETER DESCRIPTION
entity_idx

Global entity index the microphone rides. -1 pins it in the world at pos_offset.

TYPE: int

link_idx_local

Local link index on that entity.

TYPE: int

pos_offset

Microphone position: in the link's frame when attached, world when static.

TYPE: array - like[float, float, float]

euler_offset

Accepted for parity with other link-attached sensors; a mono point microphone has no direction, so it does not change what is heard.

TYPE: array - like[float, float, float]

audio_substeps

Samples rendered per physics step. Must match every source in the scene; the effective sample rate is audio_substeps / sim_dt.

TYPE: int

speed_of_sound

Propagation speed in m/s, setting the delay per meter. Default 343.0 (air, ~20 °C).

TYPE: float

ref_distance

Distance in meters at which a source is heard at unit gain. Attenuation is clamped closer than this, so a source passing through the microphone stays finite. Default 0.1.

TYPE: float

attenuation

Falloff with distance. 'inverse' is ref/r (sound pressure, the physical free-field law and the usual choice); 'inverse_square' is (ref/r)**2, falling off faster.

TYPE: (inverse, inverse_square) DEFAULT: 'inverse'

enable_doppler

Ramp delay and attenuation across each block from last step's value to this step's, which pitch-shifts a source in relative motion. Costs nothing extra; disable for a static rig.

TYPE: bool

max_delay

Longest propagation delay to keep history for, in seconds. Sources farther than max_delay * speed_of_sound are heard at that clamped delay rather than their true one. Sizes the delay line, so raise it only as far as the scene needs. Default 0.03 (~10 m).

TYPE: float

Examples:

A microphone on a robot's head, hearing its own motors:

>>> from eden.options import SensorOptions
>>> from eden.engine.sensors.spatial_audio import SpatialAudio
>>> mic = SensorOptions(
...     sensor=SpatialAudio(audio_substeps=20, ref_distance=0.2),
...     attach_entity_name="robot",
...     attach_link_name="head_link",
... )

Attributes

audio_substeps class-attribute instance-attribute
audio_substeps: PositiveInt = 20
speed_of_sound class-attribute instance-attribute
speed_of_sound: PositiveFloat = 343.0
ref_distance class-attribute instance-attribute
ref_distance: PositiveFloat = 0.1
attenuation class-attribute instance-attribute
attenuation: Literal['inverse', 'inverse_square'] = 'inverse'
enable_doppler class-attribute instance-attribute
enable_doppler: StrictBool = True
max_delay class-attribute instance-attribute
max_delay: PositiveFloat = 0.03

SpatialAudioSensorMetadata dataclass

SpatialAudioSensorMetadata(speed_of_sound: Tensor = make_tensor_field((0,)), ref_distance: Tensor = make_tensor_field((0,)), inverse_square: Tensor = make_tensor_field((0,), dtype_factory=lambda: bool), doppler: Tensor = make_tensor_field((0,), dtype_factory=lambda: bool), ramp: Tensor = make_tensor_field((0,)), age: Tensor = make_tensor_field((0,)), prev_dist: Tensor = make_tensor_field((0, 0, 0)), last_render: int = 0)

Bases: KinematicSensorMetadataMixin, SimpleSensorMetadata

Per-class state of every :class:SpatialAudioSensor, batched so one pass renders them all.

The listener parameters are grown one column per sensor at build, alongside the link buckets and offsets of KinematicSensorMetadataMixin. prev_dist also depends on the source count, which is only final once every source has registered, so it is allocated on the first render instead. The sample history itself is not here: it belongs to :class:~eden.engine.sensors.audio.AudioContext, which advances it every simulation step whether or not a microphone renders.

Attributes

speed_of_sound class-attribute instance-attribute
speed_of_sound: Tensor = make_tensor_field((0,))
ref_distance class-attribute instance-attribute
ref_distance: Tensor = make_tensor_field((0,))
inverse_square class-attribute instance-attribute
inverse_square: Tensor = make_tensor_field((0,), dtype_factory=lambda: torch.bool)
doppler class-attribute instance-attribute
doppler: Tensor = make_tensor_field((0,), dtype_factory=lambda: torch.bool)
ramp class-attribute instance-attribute
ramp: Tensor = make_tensor_field((0,))
age class-attribute instance-attribute
age: Tensor = make_tensor_field((0,))
prev_dist class-attribute instance-attribute
prev_dist: Tensor = make_tensor_field((0, 0, 0))
last_render class-attribute instance-attribute
last_render: int = 0

Methods:

SpatialAudioSensor

SpatialAudioSensor(options: SpatialAudio, idx: int, shared_context: AudioContext, shared_metadata: SpatialAudioSensorMetadata, manager: SensorManager)

Bases: KinematicSensorMixin, SimpleSensor[SpatialAudio, AudioContext, SpatialAudioSensorMetadata]

Renders :class:~eden.engine.sensors.audio.AudioContext into one microphone signal per listener.

For each (listener, source) pair the current distance gives an attenuation and a delay in samples; the delay indexes the per-source sample history with linear interpolation between the two straddling samples, and the results are summed over sources. Interpolating the delay across the block from its value one simulation step ago to its current one is what produces Doppler: a shrinking delay reads the history faster than real time, raising the pitch.

Source code in eden/engine/sensors/spatial_audio.py
def __init__(
    self,
    options: SpatialAudio,
    idx: int,
    shared_context: AudioContext,
    shared_metadata: SpatialAudioSensorMetadata,
    manager: "SensorManager",
):
    super().__init__(options, idx, shared_context, shared_metadata, manager)
    self.debug_object: "Mesh | None" = None

Attributes

debug_object instance-attribute
debug_object: Mesh | None = None

Methods:

build
build()

Claim the scene's sample rate and append this listener's column.

Source code in eden/engine/sensors/spatial_audio.py
def build(self):
    """Claim the scene's sample rate and append this listener's column."""
    super().build()

    sm = self._shared_metadata
    context = self._shared_context
    # A microphone alone in the scene is valid (it renders silence), so activate rather than
    # register: activation is what a *consumer* of the registry declares.
    context.activate()
    context.require_audio_substeps(self._options.audio_substeps, type(self._options).__name__)

    sm.speed_of_sound = concat_with_tensor(sm.speed_of_sound, self._options.speed_of_sound)
    sm.ref_distance = concat_with_tensor(sm.ref_distance, self._options.ref_distance)
    sm.inverse_square = concat_with_tensor(sm.inverse_square, self._options.attenuation == "inverse_square")
    sm.doppler = concat_with_tensor(sm.doppler, self._options.enable_doppler)

    K = self._options.audio_substeps
    if sm.ramp.numel() == 0:
        sm.ramp = (torch.arange(1, K + 1, device=gs.device, dtype=gs.tc_float) / K).view(1, 1, 1, K)
        sm.age = torch.arange(K - 1, -1, -1, device=gs.device, dtype=gs.tc_float).view(1, 1, 1, K)

    # The delay line must span this listener's longest delay plus the block it renders.
    dt_sub = self._manager._sim.dt / K
    delay_samples = int(self._options.max_delay / dt_sub) + 2
    context.require_history(K + delay_samples)
reset classmethod
reset(shared_metadata: SpatialAudioSensorMetadata, shared_ground_truth_cache: Tensor, envs_idx)

Clear the Doppler history so a reset environment renders no spurious pitch sweep.

PARAMETER DESCRIPTION
shared_metadata

Per-class state to clear.

TYPE: SpatialAudioSensorMetadata

shared_ground_truth_cache

The class's ground-truth cache slice, handled by the base implementation.

TYPE: Tensor

envs_idx

Environments being reset.

TYPE: array - like | None

Source code in eden/engine/sensors/spatial_audio.py
@classmethod
def reset(cls, shared_metadata: SpatialAudioSensorMetadata, shared_ground_truth_cache: torch.Tensor, envs_idx):
    """Clear the Doppler history so a reset environment renders no spurious pitch sweep.

    Parameters
    ----------
    shared_metadata : SpatialAudioSensorMetadata
        Per-class state to clear.
    shared_ground_truth_cache : torch.Tensor
        The class's ground-truth cache slice, handled by the base implementation.
    envs_idx : array-like | None
        Environments being reset.
    """
    super().reset(shared_metadata, shared_ground_truth_cache, envs_idx)
    if shared_metadata.prev_dist.numel() > 0:
        shared_metadata.prev_dist[envs_idx] = 0.0

Functions:

Live playback

Listen to an audio sensor while the simulation runs.

:class:LiveAudioPlayer streams the blocks an audio sensor's read() returns to the sound card, one block per simulation step, so a scene can be heard as it is watched. It needs no Python audio package: the samples are piped as raw PCM into whichever command-line player the machine already has (aplay or ffplay).

Hearing the simulation in sync means running it in real time, and the player is what enforces that:

  • A simulation faster than real time is slowed down. :meth:LiveAudioPlayer.write sleeps until the sound card is about to need the block, keeping only lead seconds queued. Without that the pipe would swallow seconds of audio ahead of the picture.
  • A simulation slower than real time cannot be helped — the sound card runs dry. Each time it does, the player counts a dropout and rebuilds its cushion of silence, so one slow step costs one short gap instead of leaving the stream on the edge of crackling from then on. :meth:LiveAudioPlayer.close reports the count, so "did it keep up" is a number rather than an impression.

The cushion is the whole of the delay this module adds, because the players are told not to add their own. Left at their defaults they would: a stock aplay holds back 0.5 s of audio before it starts and stays that far behind (measured: 680 ms), ffplay 2 s. So aplay is given a buffer no deeper than lead and ffplay its low-delay flags, which leaves lead plus the sound server's own few tens of milliseconds. :meth:LiveAudioPlayer.close measures that figure — how long the player keeps sounding after the last block — and reports it. SoX's play is not tried: a play on PATH is not reliably SoX, and its latency was never measured here.

Eden's interactive viewer already paces env.step() to sim_dt, so a scene light enough to hold that rate plays cleanly with the viewer open. It also needs one read() per simulation step (decimation=1); see the engine addons guide on gapped reads.

This module is not imported by :mod:eden.engine.sensors and installs no sensor.

Classes

LiveAudioPlayer

LiveAudioPlayer(sample_rate: int, gain: float = 1.0, lead: float = 0.06, command: tuple[str, ...] | None = None)

Stream mono float blocks to the sound card in real time, through a command-line player.

Use as a context manager around the simulation loop::

with LiveAudioPlayer(sample_rate=16000, gain=0.2) as speaker:
    for _ in range(steps):
        env.step()
        speaker.write(env.sensors["mic"].read()[0])

A recording can be peak-normalized after the fact; a live stream cannot, since the peak is not known yet. The samples are scaled by a fixed gain instead and soft-limited with tanh, so a louder moment than expected saturates smoothly rather than wrapping around.

PARAMETER DESCRIPTION
sample_rate

Samples per second: audio_substeps / sim_dt of the sensor being played.

TYPE: int

gain

Scale from the sensor's arbitrary units to full scale [-1, 1], applied before the soft limiter.

TYPE: float DEFAULT: 1.0

lead

Seconds of audio kept queued ahead of the sound card, which is also how far the sound trails the simulation. Larger rides out slower steps without a dropout; smaller keeps the sound closer to the picture.

TYPE: float DEFAULT: 0.06

command

Player command reading raw mono S16_LE PCM from stdin, with {rate} standing for the sample rate and {buffer_us} for lead in microseconds. It must not buffer ahead by itself, or the sound will trail by that much more. None picks the first of aplay and ffplay found on PATH.

TYPE: tuple[str, ...] | None DEFAULT: None

ATTRIBUTE DESCRIPTION
dropouts

Times a block arrived after the queue had run dry, i.e. audible gaps so far.

TYPE: int

trailing

Seconds the player kept sounding after the last block was written, measured by :meth:close: how far the sound trailed the simulation. About lead plus the sound server's latency when the player is well behaved.

TYPE: float

Source code in eden/engine/sensors/playback.py
def __init__(self, sample_rate: int, gain: float = 1.0, lead: float = 0.06, command: tuple[str, ...] | None = None):
    if command is None:
        command = next((player for player in _PLAYERS if shutil.which(player[0])), None)
        if command is None:
            raise RuntimeError(
                f"No command-line audio player found; looked for {[player[0] for player in _PLAYERS]} on PATH. "
                "Install one (ALSA's aplay or ffmpeg's ffplay), or pass `command`."
            )
    self._command = [part.format(rate=sample_rate, buffer_us=round(lead * 1e6)) for part in command]
    self._sample_rate = sample_rate
    self._gain = gain
    self._lead = lead
    self._process = subprocess.Popen(self._command, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    self._started_at: float | None = None
    self._seconds_written = 0.0  # of the caller's audio; the silences this player inserts are not counted
    self.dropouts = 0
    self._last_write_at: float | None = None
    self.trailing = 0.0

Attributes

dropouts instance-attribute
dropouts = 0
trailing instance-attribute
trailing = 0.0

Methods:

write
write(block: Tensor | ndarray) -> None

Queue one block, first sleeping until the sound card is lead seconds from needing it.

PARAMETER DESCRIPTION
block

Mono float samples of one environment, any shape; flattened in order.

TYPE: Tensor | ndarray

Source code in eden/engine/sensors/playback.py
def write(self, block: "torch.Tensor | np.ndarray") -> None:
    """Queue one block, first sleeping until the sound card is ``lead`` seconds from needing it.

    Parameters
    ----------
    block : torch.Tensor | np.ndarray
        Mono float samples of one environment, any shape; flattened in order.
    """
    if isinstance(block, torch.Tensor):
        block = block.detach().cpu().numpy()
    samples = np.tanh(self._gain * np.asarray(block, dtype=np.float64).reshape(-1))

    # The queue holds `lead + due_in` seconds: the sound card has consumed `now - started_at` of the
    # `lead + seconds_written` handed over so far.
    block_seconds = samples.size / self._sample_rate
    now = time.perf_counter()
    ran_dry = self._started_at is not None and self._started_at + self._seconds_written - now < -self._lead
    if self._started_at is None or ran_dry:
        # Open with `lead` seconds of silence. That is the cushion: while the simulation keeps up the queue holds
        # `lead` seconds, and a slow step eats into it instead of into the sound. Once it is used up, start over.
        self.dropouts += ran_dry
        self._started_at = now - self._seconds_written
        samples = np.concatenate((np.zeros(round(self._lead * self._sample_rate)), samples))
    due_in = self._started_at + self._seconds_written - now
    if due_in > 0.0:
        time.sleep(due_in)

    try:
        self._process.stdin.write((samples * 32767.0).astype("<i2").tobytes())
        self._process.stdin.flush()
    except BrokenPipeError:
        stderr = self._process.stderr.read().decode(errors="replace").strip()
        raise RuntimeError(
            f"Audio player `{' '.join(self._command)}` exited early: {stderr or 'no output'}"
        ) from None
    self._seconds_written += block_seconds
    self._last_write_at = time.perf_counter()
close
close() -> None

Let the queued audio finish, stop the player, and report how the simulation kept up.

Source code in eden/engine/sensors/playback.py
def close(self) -> None:
    """Let the queued audio finish, stop the player, and report how the simulation kept up."""
    if self._process.stdin.closed:
        return
    try:
        self._process.stdin.close()
    except BrokenPipeError:
        pass
    self._process.wait()
    self._process.stderr.close()
    # The player exits once the last sample has sounded, so how long it outlives the last write is how far the
    # sound was trailing: the cushion plus whatever the player and the sound server buffer.
    self.trailing = time.perf_counter() - self._last_write_at if self._last_write_at is not None else 0.0

    en.logger.info(
        f"🔈 played {self._seconds_written:.2f} s of audio with {self.dropouts} dropouts, trailing the simulation "
        f"by ~{1e3 * self.trailing:.0f} ms"
    )
    if self.dropouts:
        en.logger.warning(
            f"The simulation fell behind real time {self.dropouts} times, each one a gap in the sound. Lighten "
            "the scene or raise `lead`, or write a WAV and listen afterwards."
        )

MochiSolver

A fully implicit multi-physics solver hosted by Eden on stock Genesis: each substep solves one nonlinear system over the rigid and articulated bodies and the vertex positions of deformable ones (tetrahedral solids, thin shells, discrete elastic rods), with a smooth signed-distance penalty contact model. Eden installs it in a scene on the scene's first Mochi entity. Every entity in a Mochi scene, the ground plane included, takes a Mochi*MaterialOptions (material options), and the default contact stiffness needs eden.init(precision="64").

The Eden-side entity wrappers are MochiRigidEntity and MochiSoftEntity on the entities page.

Options

Options configuring the MochiSolver.

Upstream Genesis declares a solver's options as a field of its SceneOptions; the MochiSolver is hosted by Eden, so its options class lives here and :func:eden.engine.solvers.mochi.hosting.install_mochi_solver hands it to the solver.

Classes

MochiOptions

Bases: GravityMixin, TimeBasedMixin

Options configuring the MochiSolver.

MochiSolver is a fully-implicit solver: at every substep it solves a single nonlinear system in which the inertia of every rigid body and a smooth signed-distance penalty contact model (regularized Coulomb friction, viscous normal damping) are assembled together. There is no separate collision response stage: contact is re-detected at every Newton iterate and enters the same residual and Hessian as the inertia, which is what keeps large time steps stable and is the prerequisite for coupling deformable bodies into the same system without a coupler.

Note

Double precision (gs.init(precision="64")) is recommended. The default contact stiffness of 1e9 Pa/m combined with a 1 mm activation threshold makes the Newton system ill-conditioned in single precision.

PARAMETER DESCRIPTION
integrator

Time integration scheme: "backward_euler" (first order, strongly damped) or "bdf2" (second order, closer to energy-conserving; needs the previous two steps so the first step of a fresh or reset scene falls back to backward Euler). Defaults to "backward_euler".

TYPE: str

use_newton_euler_inertia

Whether the rotational inertia enters as the Newton-Euler residual I dw/dt + w x I w instead of the variational merit of the rotation. The merit form derives from a potential and is what the line search monitors; the Newton-Euler form is exact for gyroscopic effects but its Hessian is approximate. Defaults to False.

TYPE: bool

n_newton_iterations

Maximum number of Newton iterations per substep. Defaults to 4.

TYPE: int

newton_abs_tol

Absolute tolerance on the mass-weighted residual norm (unit acceleration under gravity gives a norm of order one). Defaults to 1e-3.

TYPE: float

newton_rel_tol

Relative tolerance on the mass-weighted residual norm with respect to its value at the first iteration. Defaults to 1e-6.

TYPE: float

explosion_control

Whether a substep whose residual grows beyond explosion_rel_tol times its initial value or beyond explosion_abs_tol is flagged as diverged: the affected environment is reset to its previous pose with zero velocity and an error is raised at the next error check. A NaN residual is flagged either way. Defaults to True.

TYPE: bool

explosion_abs_tol

Absolute residual norm above which the solve is considered diverged. Defaults to 1e9.

TYPE: float

explosion_rel_tol

Residual growth factor above which the solve is considered diverged. Defaults to 1e4.

TYPE: float

linesearch_type

Step acceptance rule: "residual_norm" accepts the first trial whose residual norm does not exceed the current one, "armijo" requires a sufficient decrease of the incremental potential (costs an extra energy assembly per trial), "none" always takes the full Newton step. Defaults to "residual_norm".

TYPE: str

n_linesearch_iterations

Maximum number of step reductions (by linesearch_alpha) per Newton iteration. The last trial is kept even if it did not improve. Defaults to 4.

TYPE: int

linesearch_alpha

Step size reduction factor between two line search trials. Defaults to 0.5.

TYPE: float

linesearch_wolfe1

Sufficient decrease parameter of the Armijo rule. Defaults to 1e-4.

TYPE: float

linear_solver

Linear solver for the Newton system: "ldlt" (dense Cholesky of every simulation island, exact, cubic in the number of degrees of freedom of the island), "pcg" (block-Jacobi preconditioned conjugate gradient, linear per iteration), or "auto" (dense when the largest island of the environment has at most dense_solver_max_dofs degrees of freedom, PCG otherwise). Bodies coupled by the contact candidates of the step form an island. Defaults to "auto".

TYPE: str

dense_solver_max_dofs

Largest island solved with the dense solver under "auto". Defaults to 50.

TYPE: int

dense_matrix_max_dofs

Largest total number of degrees of freedom for which the dense matrix of the system is allocated (memory quadratic in this number per environment); beyond it every environment is solved with PCG. Defaults to 256.

TYPE: int

n_pcg_iterations

Maximum number of conjugate gradient iterations. If None, the number of degrees of freedom capped at 500. Defaults to None.

TYPE: int

pcg_rel_tol

Relative tolerance of the conjugate gradient solve. Ignored under the "adaptive" tolerance strategy. Defaults to 1e-5.

TYPE: float

pcg_abs_tol

Absolute floor of the conjugate gradient stopping test, on the norm of the preconditioned residual (the residual scaled by the inverse of the Hessian diagonal, i.e. a displacement). A solve stops as soon as that norm drops below the floor, whatever the relative tolerance asks for; mochi's default. Set to 0 to disable. Defaults to 1e-9.

TYPE: float

linear_tolerance_strategy

How tightly each Newton step solves its linear system: "constant" always solves to pcg_rel_tol, so the accuracy of a substep is set by pcg_rel_tol and n_newton_iterations alone; "adaptive" starts each substep at a loose tolerance and tightens it as the nonlinear residual drops, spending far fewer conjugate gradient iterations per Newton step but leaving more truncation error in the step it takes, which can cost an extra Newton iteration on scenes that would otherwise converge in one. Prefer "adaptive" (mochi's default policy) when the conjugate gradient dominates the substep and a relative accuracy of order 1e-5 is enough, "constant" when accuracy per substep matters more than the cost of reaching it. Defaults to "adaptive".

TYPE: str

friction_model

Regularization of the Coulomb friction force around zero sliding velocity: "c1" has compact support (exact Coulomb beyond friction_falloff_vel), "cinf" is smooth everywhere (never exactly Coulomb, better conditioned). Defaults to "c1".

TYPE: str

use_fitted_friction_hessian

Whether the friction Hessian uses a quadratic fit that is the same in every tangential direction. The exact Hessian converges faster close to the solution but can stall the Newton iterations at the stick-slip transition. Defaults to True.

TYPE: bool

friction_with_collider_normal

Whether the friction plane is defined by the collider's distance gradient (True) or by the colliding surface normal (False). Defaults to True.

TYPE: bool

fade_friction

Whether friction fades out as the colliding surface normal and the collider gradient become aligned, i.e. as a sample point passes through the far side of a thin collider. Defaults to True.

TYPE: bool

implicit_normal_force_for_dissipation

Whether friction and damping scale with the normal force evaluated at the current iterate instead of the one recovered at the start of the step. The implicit form is required for an accurate coefficient of restitution through normal damping; the explicit form is cheaper and smoother. Defaults to False.

TYPE: bool

boundary_element_type

Quadrature rule placing contact sample points on the collision triangles: "P1Q1" (centroid), "P1Q3" (3 points per triangle, degree 2), "P1Q6" (6 points, degree 4). More points resolve contact patches better at a proportional cost. Defaults to "P1Q3".

TYPE: str

equality_stiffness

Stiffness of the penalty enforcing the equality constraints of the articulations (connect, weld and joint couplings; loop closures): the constraint violation is penalized by 0.5 * k * |c|^2. Defaults to 1e6.

TYPE: float

equality_damping

Damping of the equality constraint penalty, 0.5 * (d / dt) * |c - c_prev|^2. Defaults to 0.

TYPE: float

max_contact_pairs_per_env

Capacity of the list of (link, collider geom) pairs whose bounding boxes overlap within a substep. If None, the number of possible pairs. Defaults to None.

TYPE: int

broadphase_margin

Absolute padding of the per-step conservative bounding boxes in meters. Defaults to 0.01.

TYPE: float

spatial_hash_bins_per_item

Bins of the spatial hash that locates the collider spheres of shells and rods per inserted sphere, rounded up to a power of two; more bins shorten the chains a query walks at the cost of memory (4 bytes per bin per environment). The tetrahedra of solids use a bounding-box hierarchy instead. Defaults to 2.

TYPE: int

max_soft_hits_per_sample

Capacity of the list of contacts between deformable boundary samples and rigid colliders, per sample. Exceeding it halts the simulation with an error. Defaults to 2.

TYPE: int

max_deformable_collider_hits_per_query

Capacity of the list of contacts between sample points (rigid or deformable) and the tetrahedra of the deformable solid colliders, per sample point. Defaults to 2.

TYPE: int

max_point_cloud_hits_per_query

Capacity of the list of contacts between deformable samples and the collider spheres of shells and rods, per deformable sample (rigid samples get max_soft_hits_per_sample slots each). If None, 4 when a shell or rod has self-contact (a sample then sees the spheres of the opposing layer of its own body) and 2 otherwise; the capacity is a per-sample average over a shared list, and exceeding it halts with an error naming this option. Defaults to None.

TYPE: int

record_contacts

Whether individual contact points can be read back through entity.get_contacts(); their buffers are allocated at the first readback. Defaults to True.

TYPE: bool

use_sleeping

Whether bodies at rest are put to sleep: a sleeping body keeps its pose exactly and costs neither contact nor element work until an awake body comes near it or a setter changes its state, its controls, the gravity or the contact settings. It pays off for scenes with many settled objects. The cost is physical: slow motion stops for good, such as creep on a slope or the slow settling of a soft body, and the trajectory differs from a run without sleeping. Bodies under an active controller and rods never sleep. Defaults to False.

TYPE: bool

sleep_threshold

A step counts as at rest for a body when the solve reduced its residual by less than this factor from the warm start, which an unforced body at equilibrium does not improve on. Higher values sleep sooner. Only steps whose solve meets the Newton tolerances count: in single precision, a soft body at rest can stall just above the default newton_abs_tol and then never sleeps. A looser tolerance such as 5e-3 lets it sleep, at the cost of accuracy. Defaults to 0.5.

TYPE: float

sleep_min_steps

Consecutive steps at rest after which every body of an island at rest goes to sleep. Fewer steps sleep sooner but can freeze a body at the turning point of a slow swing. Defaults to 20.

TYPE: int

sleep_max_speed

Speed in m/s above which a body never counts as at rest, whatever its residual: it keeps a body coasting without forces (whose motion the BDF2 warm start predicts exactly) awake. Defaults to 1e-2.

TYPE: float

step_kernel

How a step is executed: "monolith" runs the whole step of every environment in one kernel (one thread per environment, one launch per step, no host round trips), "pipeline" runs each stage as its own kernel with the host driving the loops, "graph" runs the step as one graph-launched kernel whose Newton, line-search and conjugate-gradient loops run on the device with the stages parallel over items and environments (one launch per step). "auto" picks the monolith on the CPU (the pipeline for deformable scenes of at least 3000 vertices when several CPU threads run) and, on the GPU, for rigid scenes of at most 64 degrees of freedom, the pipeline otherwise. The graph kernel is opt-in: on GPUs without device-side graph conditionals (before compute capability 9.0) the runtime replays its loop bodies from the host with one flag readback per round, which runs at the pipeline's speed, and the single module compiles several times slower than the pipeline's kernels (minutes for a cloth with self-contact and an arm). Defaults to "auto".

TYPE: str

graph_pcg_unroll

Conjugate-gradient iterations per round of the graph step kernel's inner loop (each round costs one flag readback on GPUs without device-side graph conditionals; each unrolled iteration is compiled once more). Defaults to 1.

TYPE: int

joint_limit_stiffness

Stiffness in N/m (N*m/rad) of the penalty holding revolute and prismatic joints inside their range. Joint limits are soft: the violation at rest is the limit torque divided by this stiffness. Higher values reduce the violation but stiffen the Newton system. Defaults to 1e4 (the original engine defaults to 100).

TYPE: float

joint_limit_damping

Damping in Ns/m (Nm*s/rad) of the joint limit penalty, resisting the velocity of the violation. Defaults to 0.

TYPE: float

batch_links_info

Whether to batch link info. Defaults to False.

TYPE: bool

batch_joints_info

Whether to batch joint info. Defaults to False.

TYPE: bool

batch_dofs_info

Whether to batch DOF info. Defaults to False.

TYPE: bool

IK_max_targets

Maximum number of simultaneous target links of an inverse-kinematics solve (the scratch buffers are quadratic in it and allocated at the first solve). Defaults to 6.

TYPE: int

Attributes

IK_max_targets class-attribute instance-attribute
IK_max_targets: PositiveInt = 6
integrator class-attribute instance-attribute
integrator: Literal['backward_euler', 'bdf2'] = 'backward_euler'
use_newton_euler_inertia class-attribute instance-attribute
use_newton_euler_inertia: StrictBool = False
n_newton_iterations class-attribute instance-attribute
n_newton_iterations: PositiveInt = 4
newton_abs_tol class-attribute instance-attribute
newton_abs_tol: PositiveFloat = 0.001
newton_rel_tol class-attribute instance-attribute
newton_rel_tol: PositiveFloat = 1e-06
explosion_control class-attribute instance-attribute
explosion_control: StrictBool = True
explosion_abs_tol class-attribute instance-attribute
explosion_abs_tol: PositiveFloat = 1000000000.0
explosion_rel_tol class-attribute instance-attribute
explosion_rel_tol: PositiveFloat = 10000.0
linesearch_type class-attribute instance-attribute
linesearch_type: Literal['none', 'residual_norm', 'armijo'] = 'residual_norm'
n_linesearch_iterations class-attribute instance-attribute
n_linesearch_iterations: NonNegativeInt = 4
linesearch_alpha class-attribute instance-attribute
linesearch_alpha: PositiveFloat = 0.5
linesearch_wolfe1 class-attribute instance-attribute
linesearch_wolfe1: PositiveFloat = 0.0001
linear_solver class-attribute instance-attribute
linear_solver: Literal['auto', 'ldlt', 'pcg'] = 'auto'
dense_solver_max_dofs class-attribute instance-attribute
dense_solver_max_dofs: PositiveInt = 50
dense_matrix_max_dofs class-attribute instance-attribute
dense_matrix_max_dofs: PositiveInt = 256
n_pcg_iterations class-attribute instance-attribute
n_pcg_iterations: PositiveInt | None = None
pcg_rel_tol class-attribute instance-attribute
pcg_rel_tol: PositiveFloat = 1e-05
pcg_abs_tol class-attribute instance-attribute
pcg_abs_tol: NonNegativeFloat = 1e-09
linear_tolerance_strategy class-attribute instance-attribute
linear_tolerance_strategy: Literal['constant', 'adaptive'] = 'adaptive'
friction_model class-attribute instance-attribute
friction_model: Literal['c1', 'cinf'] = 'c1'
use_fitted_friction_hessian class-attribute instance-attribute
use_fitted_friction_hessian: StrictBool = True
friction_with_collider_normal class-attribute instance-attribute
friction_with_collider_normal: StrictBool = True
fade_friction class-attribute instance-attribute
fade_friction: StrictBool = True
implicit_normal_force_for_dissipation class-attribute instance-attribute
implicit_normal_force_for_dissipation: StrictBool = False
boundary_element_type class-attribute instance-attribute
boundary_element_type: Literal['P1Q1', 'P1Q3', 'P1Q6'] = 'P1Q3'
equality_stiffness class-attribute instance-attribute
equality_stiffness: PositiveFloat = 1000000.0
equality_damping class-attribute instance-attribute
equality_damping: NonNegativeFloat = 0.0
max_contact_pairs_per_env class-attribute instance-attribute
max_contact_pairs_per_env: PositiveInt | None = None
spatial_hash_bins_per_item class-attribute instance-attribute
spatial_hash_bins_per_item: PositiveInt = 2
max_soft_hits_per_sample class-attribute instance-attribute
max_soft_hits_per_sample: PositiveInt = 2
max_deformable_collider_hits_per_query class-attribute instance-attribute
max_deformable_collider_hits_per_query: PositiveInt = 2
max_point_cloud_hits_per_query class-attribute instance-attribute
max_point_cloud_hits_per_query: PositiveInt | None = None
broadphase_margin class-attribute instance-attribute
broadphase_margin: NonNegativeFloat = 0.01
record_contacts class-attribute instance-attribute
record_contacts: StrictBool = True
use_sleeping class-attribute instance-attribute
use_sleeping: StrictBool = False
sleep_threshold class-attribute instance-attribute
sleep_threshold: UnitInterval = 0.5
sleep_min_steps class-attribute instance-attribute
sleep_min_steps: PositiveInt = 20
sleep_max_speed class-attribute instance-attribute
sleep_max_speed: NonNegativeFloat = 0.01
step_kernel class-attribute instance-attribute
step_kernel: Literal['auto', 'monolith', 'pipeline', 'graph'] = 'auto'
graph_pcg_unroll class-attribute instance-attribute
graph_pcg_unroll: PositiveInt = 1
joint_limit_stiffness class-attribute instance-attribute
joint_limit_stiffness: PositiveFloat = 10000.0
joint_limit_damping class-attribute instance-attribute
joint_limit_damping: NonNegativeFloat = 0.0
batch_links_info: StrictBool = False
batch_joints_info class-attribute instance-attribute
batch_joints_info: StrictBool = False
batch_dofs_info class-attribute instance-attribute
batch_dofs_info: StrictBool = False

Materials

Classes

Base

Bases: Material[EntityT]

Base class of the materials simulated by the MochiSolver, holding the contact parameters they share.

Contact between two bodies is a smooth penalty on the signed distance of sample points placed on one body's collision surface to the other body's signed distance field (SDF). Every pair parameter is combined from the two bodies' values by geometric mean, except the smoothing distance, the activation threshold and the alignment threshold, which are read from the collider body alone.

PARAMETER DESCRIPTION
friction

Coulomb friction coefficient. Default is 0.5.

TYPE: float

penalty_coefficient

Contact stiffness in Pa/m: pressure per unit of penetration once the penalty ramp is fully active. Higher values reduce penetration at the cost of a stiffer, more ill-conditioned Newton system. Default is 1e9.

TYPE: float

penalty_smoothing_half_distance

Half-width in meters of the smooth ramp between zero contact pressure and the linear regime. The pressure ramps up over twice this distance below the activation threshold, so a wider ramp gives softer, better conditioned contact and a narrower one sharper contact onset. Default is 0.005.

TYPE: float

penalty_threshold

Signed distance in meters at which contact pressure starts to build up. Positive values start pushing bodies apart before they touch (a contact skin), negative values allow some interpenetration before responding. Default is 0.001.

TYPE: float

friction_falloff_vel

Sliding speed in m/s below which the Coulomb friction force is regularized towards zero. Larger values give smoother, more robust stick-slip transitions with more creep under static load; smaller values approach exact Coulomb friction with a stiffer system. Default is 0.01.

TYPE: float

viscous_friction

Tangential viscous friction coefficient in s/m, multiplying the normal force and the sliding velocity. Default is 0.

TYPE: float

normal_viscous_damping

Normal viscous damping coefficient in s/m, multiplying the normal force and the approach velocity. This is the mechanism controlling the coefficient of restitution: 0 gives fully elastic penalty contact, larger values dissipate more impact energy. Default is 0.

TYPE: float

max_alignment_normals

Cosine in [-1, 1] between the colliding surface normal and the collider gradient above which the contact is disabled, so that a body embedded past the far side of a thin collider can escape rather than being trapped. Default is 0.

TYPE: float

has_gravity

Whether gravity acts on this body. Default is True.

TYPE: bool

contact_layer

Name of the contact layer of this body. Contact between two layers can be disabled at the solver level. Default is "default".

TYPE: str

Note

This class should not be instantiated directly.

Attributes

friction class-attribute instance-attribute
friction: NonNegativeFloat = 0.5
penalty_coefficient class-attribute instance-attribute
penalty_coefficient: PositiveFloat = 1000000000.0
penalty_smoothing_half_distance class-attribute instance-attribute
penalty_smoothing_half_distance: NonNegativeFloat = 0.005
penalty_threshold class-attribute instance-attribute
penalty_threshold: ValidFloat = 0.001
friction_falloff_vel class-attribute instance-attribute
friction_falloff_vel: NonNegativeFloat = 0.01
viscous_friction class-attribute instance-attribute
viscous_friction: NonNegativeFloat = 0.0
normal_viscous_damping class-attribute instance-attribute
normal_viscous_damping: NonNegativeFloat = 0.0
max_alignment_normals class-attribute instance-attribute
max_alignment_normals: Annotated[ValidFloat, Field(ge=-1.0, le=1.0)] = 0.0
has_gravity class-attribute instance-attribute
has_gravity: StrictBool = True
contact_layer class-attribute instance-attribute
contact_layer: str = 'default'

Attributes

ColliderType module-attribute

ColliderType = Literal['auto', 'plane', 'sphere', 'box', 'sdf', 'none']

Rigid module-attribute

Rigid = MochiRigid

Classes

MochiRigid

Bases: Base['MochiEntity'], Rigid

Rigid body material simulated by the MochiSolver.

PARAMETER DESCRIPTION
use_visual_raycasting

See Kinematic. Default is False.

TYPE: bool

rho

Density in kg/m^3 used to derive the mass and inertia from the collision geometry when the asset specifies none. Overrides per-geometry densities found in the asset. Default is 1000.

TYPE: float

friction

penalty_coefficient

penalty_smoothing_half_distance

penalty_threshold

friction_falloff_vel

viscous_friction

Contact parameters, see Base.

TYPE: optional

normal_viscous_damping

Contact parameters, see Base.

TYPE: optional

max_alignment_normals

Contact parameters, see Base.

TYPE: optional

has_gravity

Contact parameters, see Base.

TYPE: optional

contact_layer

Contact parameters, see Base.

TYPE: optional

collider_type

Representation of this body used when other bodies' sample points collide against it: "plane", "sphere" and "box" are exact analytic distance fields, "sdf" is the precomputed grid of the collision mesh, "none" makes the body collide only through its own sample points (it never acts as a collider), and "auto" selects the analytic field for plane, sphere and box primitives and the grid otherwise. An analytic field requires the matching primitive. Default is "auto".

TYPE: str

sdf_cell_size

Cell size in SDF grid in meters. Contact resolves the penalty ramp against this grid, so the cell should stay well below the ramp width. Default is 0.0025.

TYPE: float

sdf_min_res

Minimum resolution of the SDF grid. Must be at least 16. Default is 32.

TYPE: int

sdf_max_res

Maximum resolution of the SDF grid. Must be >= sdf_min_res. Default is 128.

TYPE: int

Attributes

rho class-attribute instance-attribute
rho: PositiveFloat = 1000.0
collider_type class-attribute instance-attribute
collider_type: ColliderType = 'auto'
needs_coup class-attribute instance-attribute
needs_coup: StrictBool = False
sdf_cell_size class-attribute instance-attribute
sdf_cell_size: PositiveFloat = 0.0025

Attributes

ElasticModel module-attribute

ElasticModel = Literal['stable_neohookean', 'stvk', 'linear']

SoftColliderType module-attribute

SoftColliderType = Literal['auto', 'sdf', 'none']

Classes

Elastic

Bases: Base['MochiSoftEntity']

Deformable (tetrahedral finite element) material simulated by the MochiSolver.

The body is discretized into linear tetrahedra whose vertex positions are unknowns of the same implicit Newton solve as the rigid bodies. Contact acts on quadrature samples of the boundary triangles against the rigid bodies' signed distance fields, with the same smooth penalty and friction model (see Base).

PARAMETER DESCRIPTION
E

Young's modulus in Pa. Default is 1e5.

TYPE: float

nu

Poisson's ratio. Default is 0.45.

TYPE: float

rho

Density in kg/m^3. Default is 1000.

TYPE: float

model

Constitutive model: "stable_neohookean" (Smith et al. 2018, robust to inversion), "stvk" (Saint Venant-Kirchhoff) or "linear" (small strain). Default is "stable_neohookean".

TYPE: str

mass_damping

Mass-proportional (Rayleigh) damping coefficient in 1/s. Default is 0.

TYPE: float

stiffness_damping

Stiffness-proportional (Kelvin-Voigt) damping coefficient in s, acting through the rest-state elastic tangent on the rate of the Green strain. Default is 0.

TYPE: float

friction

penalty_coefficient

penalty_smoothing_half_distance

penalty_threshold

friction_falloff_vel

viscous_friction

Contact parameters, see Base.

TYPE: optional

normal_viscous_damping

Contact parameters, see Base.

TYPE: optional

max_alignment_normals

Contact parameters, see Base.

TYPE: optional

has_gravity

Contact parameters, see Base.

TYPE: optional

contact_layer

Contact parameters, see Base.

TYPE: optional

collider_type

Whether other bodies' sample points collide against this body: "sdf" (and "auto") builds a signed distance field of the rest shape that is queried through the deformed tetrahedra (contact only registers for points inside the body, the activation threshold is zero), "none" makes the body collide only through its own samples. Default is "auto".

TYPE: str

Attributes

E class-attribute instance-attribute
E: PositiveFloat = 100000.0
nu class-attribute instance-attribute
nu: Annotated[ValidFloat, Field(gt=-1.0, lt=0.5)] = 0.45
rho class-attribute instance-attribute
rho: PositiveFloat = 1000.0
model class-attribute instance-attribute
model: ElasticModel = 'stable_neohookean'
mass_damping class-attribute instance-attribute
mass_damping: NonNegativeFloat = 0.0
stiffness_damping class-attribute instance-attribute
stiffness_damping: NonNegativeFloat = 0.0
collider_type class-attribute instance-attribute
collider_type: SoftColliderType = 'auto'
mu class-attribute instance-attribute
mu: float = Field(default=0.0, init=False, repr=False)
lam class-attribute instance-attribute
lam: float = Field(default=0.0, init=False, repr=False)

Attributes

ShellColliderType module-attribute

ShellColliderType = Literal['auto', 'point_cloud', 'none']

Classes

Shell

Bases: Base['MochiSoftEntity']

Thin shell (cloth-like) material simulated by the MochiSolver.

The surface triangles of the morph are the finite elements: a Saint Venant-Kirchhoff membrane on the metric of the mid-surface and a Koiter-type bending term on its discrete curvature, both thickness-integrated. The stiffnesses derive from the 3D isotropic parameters and the thickness (plane stress): membrane shear mu t, membrane lambda_ps t with lambda_ps = E nu / (1 - nu^2), bending D nu and D (1 - nu) with the plate rigidity D = E t^3 / (12 (1 - nu^2)), areal density rho t; each can be overridden.

PARAMETER DESCRIPTION
E

Young's modulus in Pa. Default is 1e4.

TYPE: float

nu

Poisson's ratio. Default is 0.3.

TYPE: float

rho

Density in kg/m^3. Default is 200.

TYPE: float

thickness

Shell thickness in m. Default is 0.001.

TYPE: float

membrane_mu

Thickness-integrated membrane stiffnesses in N/m, overriding the values derived from E, nu and the thickness.

TYPE: float

membrane_lambda

Thickness-integrated membrane stiffnesses in N/m, overriding the values derived from E, nu and the thickness.

TYPE: float

bending_alpha

Bending stiffnesses in N m, overriding the values derived from E, nu and the thickness.

TYPE: float

bending_beta

Bending stiffnesses in N m, overriding the values derived from E, nu and the thickness.

TYPE: float

mass_damping

Mass-proportional damping coefficient in 1/s. Default is 0.

TYPE: float

stiffness_damping

Stiffness-proportional (Rayleigh) damping coefficient in s. Default is 0.

TYPE: float

friction

penalty_coefficient

penalty_smoothing_half_distance

penalty_threshold

friction_falloff_vel

viscous_friction

Contact parameters, see Base.

TYPE: optional

normal_viscous_damping

Contact parameters, see Base.

TYPE: optional

max_alignment_normals

Contact parameters, see Base.

TYPE: optional

has_gravity

Contact parameters, see Base.

TYPE: optional

contact_layer

Contact parameters, see Base.

TYPE: optional

collider_type

Whether other bodies collide against this shell: "point_cloud" (and "auto") places a sphere of radius collider_radius at every vertex, "none" makes the shell collide only through its own samples. Default is "auto".

TYPE: str

collider_radius

Radius in m of the collider spheres at the vertices. Default is 0.01.

TYPE: float

self_contact

Whether the samples of this shell collide against its own collider spheres (point-cloud collider only). Default is False.

TYPE: bool

self_contact_exclusion_ratio

Samples whose rest position lies within collider_radius * self_contact_exclusion_ratio plus the penalty threshold of a vertex do not collide with the sphere of that vertex (neighbors along the shell); the rest distance to the second-nearest vertices must exceed this range plus the contact band. Must be larger than 1. Default is 1.5.

TYPE: float

Attributes

E class-attribute instance-attribute
E: PositiveFloat = 10000.0
nu class-attribute instance-attribute
nu: Annotated[ValidFloat, Field(gt=-1.0, lt=0.5)] = 0.3
rho class-attribute instance-attribute
rho: PositiveFloat = 200.0
thickness class-attribute instance-attribute
thickness: PositiveFloat = 0.001
membrane_mu class-attribute instance-attribute
membrane_mu: PositiveFloat | None = None
membrane_lambda class-attribute instance-attribute
membrane_lambda: ValidFloat | None = None
bending_alpha class-attribute instance-attribute
bending_alpha: ValidFloat | None = None
bending_beta class-attribute instance-attribute
bending_beta: PositiveFloat | None = None
mass_damping class-attribute instance-attribute
mass_damping: NonNegativeFloat = 0.0
stiffness_damping class-attribute instance-attribute
stiffness_damping: NonNegativeFloat = 0.0
collider_type class-attribute instance-attribute
collider_type: ShellColliderType = 'auto'
collider_radius class-attribute instance-attribute
collider_radius: PositiveFloat = 0.01
self_contact class-attribute instance-attribute
self_contact: StrictBool = False
self_contact_exclusion_ratio class-attribute instance-attribute
self_contact_exclusion_ratio: Annotated[ValidFloat, Field(gt=1.0)] = 1.5
areal_density class-attribute instance-attribute
areal_density: float = Field(default=0.0, init=False, repr=False)

Attributes

RodColliderType module-attribute

RodColliderType = Literal['auto', 'point_cloud', 'none']

Classes

Rod

Bases: Base['MochiSoftEntity']

Rod (cable, rope, wire) material simulated by the MochiSolver as a discrete elastic rod: stretching of the segments, bending and twisting at the interior nodes.

The stiffnesses derive from the 3D isotropic parameters and the radius of the circular cross-section: axial E A, flexural E I (both principal directions), torsional G J with G = E / (2 (1 + nu)), linear density rho A and linear rotational inertia rho J, with A = pi r^2, I = pi r^4 / 4, J = pi r^4 / 2; each can be overridden.

PARAMETER DESCRIPTION
E

Young's modulus in Pa. Default is 1e7.

TYPE: float

nu

Poisson's ratio. Default is 0.3.

TYPE: float

rho

Density in kg/m^3. Default is 1000.

TYPE: float

axial_stiffness

Stretching stiffness E A in N, overriding the derived value.

TYPE: float

flexural_stiffness

Bending stiffness E I in N m^2 (both directions), overriding the derived value.

TYPE: float

torsional_stiffness

Twisting stiffness G J in N m^2, overriding the derived value.

TYPE: float

linear_density

Mass per unit length in kg/m, overriding the derived value.

TYPE: float

linear_rotational_inertia

Rotational inertia per unit length about the centerline in kg m, overriding the derived value.

TYPE: float

mass_damping

Mass-proportional damping coefficient in 1/s. Default is 0.

TYPE: float

stiffness_damping

Stiffness-proportional (Rayleigh) damping coefficient in s. Default is 0.

TYPE: float

friction

penalty_coefficient

penalty_smoothing_half_distance

penalty_threshold

friction_falloff_vel

viscous_friction

Contact parameters, see Base.

TYPE: optional

normal_viscous_damping

Contact parameters, see Base.

TYPE: optional

max_alignment_normals

Contact parameters, see Base.

TYPE: optional

has_gravity

Contact parameters, see Base.

TYPE: optional

contact_layer

Contact parameters, see Base.

TYPE: optional

collider_type

Whether other bodies collide against this rod: "point_cloud" (and "auto") places a sphere of the rod radius at every node, "none" makes the rod collide only through its own samples. Default is "auto".

TYPE: str

self_contact

Whether the samples of this rod collide against its own collider spheres (point-cloud collider only). Default is False.

TYPE: bool

self_contact_exclusion_ratio

Samples whose rest position lies within the rod radius times self_contact_exclusion_ratio plus the penalty threshold of a vertex do not collide with the sphere of that vertex (neighbors along the rod); the rest distance to the second-nearest vertices must exceed this range plus the contact band. Must be larger than 1. Default is 1.5.

TYPE: float

Attributes

E class-attribute instance-attribute
E: PositiveFloat = 10000000.0
nu class-attribute instance-attribute
nu: Annotated[ValidFloat, Field(gt=-1.0, lt=0.5)] = 0.3
rho class-attribute instance-attribute
rho: PositiveFloat = 1000.0
axial_stiffness class-attribute instance-attribute
axial_stiffness: PositiveFloat | None = None
flexural_stiffness class-attribute instance-attribute
flexural_stiffness: PositiveFloat | None = None
torsional_stiffness class-attribute instance-attribute
torsional_stiffness: PositiveFloat | None = None
linear_density class-attribute instance-attribute
linear_density: PositiveFloat | None = None
linear_rotational_inertia class-attribute instance-attribute
linear_rotational_inertia: NonNegativeFloat | None = None
mass_damping class-attribute instance-attribute
mass_damping: NonNegativeFloat = 0.0
stiffness_damping class-attribute instance-attribute
stiffness_damping: NonNegativeFloat = 0.0
collider_type class-attribute instance-attribute
collider_type: RodColliderType = 'auto'
self_contact class-attribute instance-attribute
self_contact: StrictBool = False
self_contact_exclusion_ratio class-attribute instance-attribute
self_contact_exclusion_ratio: Annotated[ValidFloat, Field(gt=1.0)] = 1.5

Methods:

resolve
resolve(radius)

Stiffnesses and inertia per unit length of a circular cross-section of the given radius; overrides win.

Source code in eden/engine/solvers/mochi/materials/rod.py
def resolve(self, radius):
    """Stiffnesses and inertia per unit length of a circular cross-section of the given radius; overrides win."""
    area = np.pi * radius**2
    second_moment = np.pi * radius**4 / 4.0
    shear_modulus = self.E / (2.0 * (1.0 + self.nu))
    return {
        "axial_stiffness": self.axial_stiffness if self.axial_stiffness is not None else self.E * area,
        "flexural_stiffness": (
            self.flexural_stiffness if self.flexural_stiffness is not None else self.E * second_moment
        ),
        "torsional_stiffness": (
            self.torsional_stiffness
            if self.torsional_stiffness is not None
            else shear_modulus * 2.0 * second_moment
        ),
        "linear_density": self.linear_density if self.linear_density is not None else self.rho * area,
        "linear_rotational_inertia": (
            self.linear_rotational_inertia
            if self.linear_rotational_inertia is not None
            else self.rho * 2.0 * second_moment
        ),
    }

Rod morph

The rod morph: a polyline the MochiSolver simulates as a discrete elastic rod.

Classes

Rod

Bases: Morph

Morph for a rod (cable, rope, wire): a polyline of nodes with a circular cross-section, simulated as a discrete elastic rod by the MochiSolver.

PARAMETER DESCRIPTION
points

Positions of the rod nodes in the morph frame, in order along the rod (at least 2).

TYPE: (array - like, shape(n_nodes, 3))

radius

Radius of the circular cross-section in meters. Default is 0.01.

TYPE: float

n_cross_section_segments

Number of segments of the tube drawn around the rod. Default is 12.

TYPE: int

is_closed_loop

Whether the last node connects back to the first one. Default is False.

TYPE: bool

pos

Position of the morph frame in meters. Default is (0.0, 0.0, 0.0).

TYPE: (tuple, shape(3))

euler

Orientation of the morph frame in degrees (scipy extrinsic x-y-z). Default is (0.0, 0.0, 0.0).

TYPE: (tuple, shape(3))

quat

Orientation of the morph frame as a w-x-y-z quaternion; takes precedence over euler. Default is None.

TYPE: (tuple, shape(4))

Attributes

points class-attribute instance-attribute
points: Any = None
radius class-attribute instance-attribute
radius: float = 0.01
n_cross_section_segments class-attribute instance-attribute
n_cross_section_segments: StrictInt = Field(default=12, ge=3)
is_closed_loop class-attribute instance-attribute
is_closed_loop: StrictBool = False

Hosting on stock Genesis

Host the MochiSolver in a scene of stock Genesis.

Upstream Genesis has no seam for a solver it does not ship: Simulator.__init__ builds a fixed list of solvers, Scene.add_entity decides a surface's defaults from a closed chain of isinstance checks on the material, and the visualizer names each solver it draws. This module stands in for the edits a Genesis fork would make, one scene at a time and never on a class, so a scene without a Mochi entity runs exactly the upstream code:

  • :func:install_mochi_solver builds the solver and appends it to the simulator's solvers. From then on the scene takes Mochi materials in scene.add_entity, holds the solver as scene.mochi_solver, draws its bodies with the rasterizer, restores them on a reset and frees the solver's arrays with the scene, as a scene of the fork would. It refuses scene.export, which the fork allows: gs.Scene.load rebuilds a scene on stock Genesis, without the solver;
  • :func:create_mochi_scene is gs.Scene(..., mochi_options=...) of the fork: a new scene, installed;
  • :func:add_mochi_entity is what scene.add_entity does for a Mochi material;
  • :func:find_mochi_solver returns the solver of a scene, or None.

What the solver itself needs beyond this (registering its error flags, clearing them when an environment is reset) lives in :class:~eden.engine.solvers.mochi.mochi_solver.MochiSolver.

Classes

Functions:

find_mochi_solver

find_mochi_solver(scene: Scene) -> MochiSolver | None

The MochiSolver installed in scene, or None if no Mochi entity was ever added to it.

Source code in eden/engine/solvers/mochi/hosting.py
def find_mochi_solver(scene: Scene) -> MochiSolver | None:
    """The MochiSolver installed in ``scene``, or None if no Mochi entity was ever added to it."""
    for solver in scene.sim.solvers:
        if isinstance(solver, MochiSolver):
            return solver
    return None

install_mochi_solver

install_mochi_solver(scene: Scene, options: MochiOptions | None = None) -> MochiSolver

Build a MochiSolver for scene and register it with its simulator, its visualizer and its reset.

Call it once per scene, before the scene is built and before any Mochi entity is added.

PARAMETER DESCRIPTION
scene

The unbuilt scene. Its renderer must be the rasterizer, the only one the solver is bound to.

TYPE: Scene

options

The solver's options. Their unset dt and gravity come from the scene's SimOptions.

TYPE: MochiOptions | None DEFAULT: None

RETURNS DESCRIPTION
MochiSolver

The installed solver, also reachable as scene.mochi_solver.

Source code in eden/engine/solvers/mochi/hosting.py
def install_mochi_solver(scene: Scene, options: MochiOptions | None = None) -> MochiSolver:
    """Build a MochiSolver for ``scene`` and register it with its simulator, its visualizer and its reset.

    Call it once per scene, before the scene is built and before any Mochi entity is added.

    Parameters
    ----------
    scene : Scene
        The unbuilt scene. Its renderer must be the rasterizer, the only one the solver is bound to.
    options : MochiOptions | None
        The solver's options. Their unset ``dt`` and ``gravity`` come from the scene's ``SimOptions``.

    Returns
    -------
    MochiSolver
        The installed solver, also reachable as ``scene.mochi_solver``.
    """
    if scene.is_built:
        gs.raise_exception("The MochiSolver must be installed before the scene is built.")
    if find_mochi_solver(scene) is not None:
        gs.raise_exception("This scene already has a MochiSolver: install it once, then add every Mochi entity to it.")
    renderer = scene.visualizer.raytracer or scene.visualizer.batch_renderer
    if renderer is not None:
        gs.raise_exception(
            f"The MochiSolver cannot be drawn by {type(renderer).__name__}: Eden binds it to the rasterizer only, "
            "which every camera of this scene would then skip. Pass `renderer=gs.renderers.Rasterizer()`, or render a "
            "Mochi scene with a Nyx camera (examples/recipes/mochi/_nyx.py)."
        )

    sim = scene.sim
    # SceneOptions fills the values a solver's options leave unset (dt, gravity) from SimOptions, for the solvers it
    # declares only.
    options = (options or MochiOptions()).model_copy_from(scene.options.sim)
    solver = MochiSolver(scene, sim, options)
    sim._solvers.append(solver)
    # The name a scene of the fork holds the solver under, which the code written for the fork reads.
    scene.mochi_solver = solver
    _bind_add_entity(scene)
    _bind_reset(sim, solver)
    _bind_destroy(sim, solver)
    _bind_rasterizer(scene, solver)
    _bind_export(scene)
    return solver

create_mochi_scene

create_mochi_scene(mochi_options: MochiOptions | None = None, **scene_kwargs) -> Scene

A new gs.Scene(**scene_kwargs) with a MochiSolver installed (see :func:install_mochi_solver).

PARAMETER DESCRIPTION
mochi_options

The solver's options, which a scene of the fork took as gs.Scene(mochi_options=...).

TYPE: MochiOptions | None DEFAULT: None

**scene_kwargs

Everything else gs.Scene takes.

DEFAULT: {}

RETURNS DESCRIPTION
Scene

The unbuilt scene, holding the solver as scene.mochi_solver.

Source code in eden/engine/solvers/mochi/hosting.py
def create_mochi_scene(mochi_options: MochiOptions | None = None, **scene_kwargs) -> Scene:
    """A new ``gs.Scene(**scene_kwargs)`` with a MochiSolver installed (see :func:`install_mochi_solver`).

    Parameters
    ----------
    mochi_options : MochiOptions | None
        The solver's options, which a scene of the fork took as ``gs.Scene(mochi_options=...)``.
    **scene_kwargs
        Everything else ``gs.Scene`` takes.

    Returns
    -------
    Scene
        The unbuilt scene, holding the solver as ``scene.mochi_solver``.
    """
    scene = gs.Scene(**scene_kwargs)
    install_mochi_solver(scene, mochi_options)
    return scene

add_mochi_entity

add_mochi_entity(scene: Scene, morph: Morph, material: Material, surface: Surface | None = None, visualize_contact: bool = False, vis_mode: str | None = None, name: str | None = None) -> Entity

Scene.add_entity for a Mochi material, on a scene :func:install_mochi_solver was called on.

The upstream Scene.add_entity cannot take one: its surface defaults end in a bare exception for the deformable materials, and Simulator._add_entity hands a Rigid to the RigidSolver, which declares the most derived class of the two that claim it. An installed scene's add_entity routes Mochi materials here.

Source code in eden/engine/solvers/mochi/hosting.py
def add_mochi_entity(
    scene: Scene,
    morph: Morph,
    material: "Material",
    surface: "Surface | None" = None,
    visualize_contact: bool = False,
    vis_mode: str | None = None,
    name: str | None = None,
) -> "Entity":
    """``Scene.add_entity`` for a Mochi material, on a scene :func:`install_mochi_solver` was called on.

    The upstream ``Scene.add_entity`` cannot take one: its surface defaults end in a bare exception for the deformable
    materials, and ``Simulator._add_entity`` hands a ``Rigid`` to the RigidSolver, which declares the most derived
    class of the two that claim it. An installed scene's ``add_entity`` routes Mochi materials here.
    """
    solver = find_mochi_solver(scene)
    if solver is None:
        gs.raise_exception("No MochiSolver in this scene: call `install_mochi_solver(scene, options)` first.")
    if scene.is_built:
        gs.raise_exception("Entities can be added only before the scene is built.")
    if not isinstance(material, mochi_materials.Base):
        gs.raise_exception(f"`add_mochi_entity` takes a Mochi material, got {type(material).__name__}.")
    if visualize_contact:
        gs.raise_exception(
            "`visualize_contact` draws the RigidSolver's contacts only; a Mochi entity has none to draw."
        )

    if surface is None:
        surface = gs.surfaces.Default()
    if isinstance(morph, collections.abc.Iterable) and not isinstance(morph, Morph):
        morph = tuple(morph)
    if isinstance(morph[0] if isinstance(morph, tuple) else morph, (gs.morphs.Box, gs.morphs.Cylinder)):
        surface.smooth = False
    if surface.double_sided is None:
        surface.double_sided = isinstance(material, mochi_materials.Shell)
    if vis_mode is not None:
        surface.vis_mode = vis_mode
    if surface.vis_mode is None:
        surface.vis_mode = "visual"
    allowed = ("visual",) if isinstance(material, _SOFT_MATERIALS) else ("visual", "collision", "sdf")
    if surface.vis_mode not in allowed:
        gs.raise_exception(
            f"Unsupported `surface.vis_mode` for material {type(material).__name__}: '{surface.vis_mode}'. Expected "
            f"one of: {list(allowed)}."
        )
    # Mochi contact samples the authored collision triangles, so a file mesh is kept as it is unless asked otherwise.
    # The rigid-entity description would otherwise convexify it, since a `Mochi.Rigid` is a `Rigid`.
    for each_morph in morph if isinstance(morph, tuple) else (morph,):
        if isinstance(each_morph, gs.morphs.FileMorph) and each_morph.convexify is None:
            each_morph.convexify = False

    sim = scene.sim
    entity = solver.add_entity(sim.n_entities, material, morph, surface, visualize_contact, name=name, desc=None)
    sim._entities.append(entity)
    if entity.desc is not None:
        scene._desc.entities.append(entity.desc)
    return entity

Solver, entities and states

MochiSolver

MochiSolver(scene: Scene, sim: Simulator, options: MochiOptions)

Bases: GravityMixin, TimeBasedMixin, KinematicSolver

Fully-implicit multi-physics solver with smooth penalty contact.

Every substep solves one nonlinear system over the generalized coordinates of all rigid and articulated bodies and the vertex positions of all deformable bodies, whose residual is the gradient of an incremental potential (inertia, gravity, damping, elasticity and the contact penalty with its friction and damping terms) and whose Hessian is assembled from per-link, per-contact-pair and per-tetrahedron blocks. The solve is a damped Newton iteration with a line search; contact is re-detected at every iterate.

Source code in eden/engine/solvers/mochi/mochi_solver.py
def __init__(self, scene: "Scene", sim: "Simulator", options: MochiOptions) -> None:
    super().__init__(scene, sim, options)
    self._options: MochiOptions = options
    self.sdf_info: array_class.SDFInfo | None = None
    self._is_contacts_recorded = False
    # Environments whose kinematic state was changed from outside the solver; their multistep history is rebuilt
    # at the start of the next step. Allocated at build, once the batch size is known.
    self._external_state_dirty_mask = None
    self._external_state_subscriber = Subscriber(
        frozenset({StateChange.GEOMETRY, StateChange.DYNAMICS}), callback=self._on_external_state_change
    )
    # Island node of every degree of freedom and the index of every node, for the setters to wake what they change.
    # Allocated at build when sleeping is enabled.
    self._dofs_node = None
    self._nodes_idx = None
    # Deformable bodies live outside the kinematic tree.
    self._soft_entities = gs.List()
    self.soft_info = None
    self.soft_state = None
    self._soft_vverts_render = None
    self._soft_vverts_vert_idx = None

Attributes

material_cls class-attribute instance-attribute

material_cls = mochi_materials.Base

sdf_info instance-attribute

sdf_info: SDFInfo | None = None

soft_info instance-attribute

soft_info = None

soft_state instance-attribute

soft_state = None

n_attachments property

n_attachments

Number of rigid-deformable vertex attachments registered on the deformable entities.

data class-attribute instance-attribute

data = Solver.data
is_links_info_batched: bool

Whether the inertial properties of a link are stored per environment rather than shared by the batch.

n_geoms property

n_geoms

n_verts property

n_verts

n_free_verts property

n_free_verts

n_fixed_verts property

n_fixed_verts

n_faces property

n_faces

n_edges property

n_edges

n_equalities property

n_equalities

equalities property

equalities

n_samples property

n_samples

is_active property

is_active

has_soft property

has_soft

soft_entities property

soft_entities

n_soft_entities property

n_soft_entities

n_soft_verts property

n_soft_verts

n_soft_elems property

n_soft_elems

n_shell_elems property

n_shell_elems

n_rod_elems property

n_rod_elems

n_rod_stencils property

n_rod_stencils

n_soft_vverts property

n_soft_vverts

n_soft_vfaces property

n_soft_vfaces

n_dofs_total property

n_dofs_total

Degrees of freedom of the Newton system: rigid, then 3 per deformable vertex, then one twist per rod segment.

Methods:

add_entity

add_entity(idx, material, morph, surface, visualize_contact=False, name=None, desc=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def add_entity(self, idx, material, morph, surface, visualize_contact=False, name=None, desc=None):
    morphs = desc.morphs if desc is not None else ((morph,) if isinstance(morph, Morph) else tuple(morph))
    if len(morphs) > 1:
        gs.raise_exception("Heterogeneous morphs are not supported by the MochiSolver.")
    if isinstance(morphs[0], (gs.morphs.Terrain, gs.morphs.USD, gs.morphs.Drone)):
        gs.raise_exception(f"Morph {type(morphs[0]).__name__} is not supported by the MochiSolver.")

    if isinstance(material, (mochi_materials.Elastic, mochi_materials.Shell, mochi_materials.Rod)):
        is_rod = isinstance(material, mochi_materials.Rod)
        if not is_rod and not isinstance(
            morph, (gs.morphs.Box, gs.morphs.Sphere, gs.morphs.Cylinder, gs.morphs.Mesh)
        ):
            gs.raise_exception(
                f"Morph {type(morph).__name__} is not supported for deformable Mochi bodies (Box, Sphere, "
                "Cylinder or Mesh expected)."
            )
        if is_rod and not isinstance(morph, Rod):
            gs.raise_exception(
                "A `Rod` material (eden.engine.solvers.mochi.materials) requires a `Rod` morph "
                "(eden.engine.solvers.mochi.morphs)."
            )
        entity = MochiSoftEntity(
            scene=self._scene,
            solver=self,
            material=material,
            morph=morph,
            surface=surface,
            idx=idx,
            idx_in_solver=self.n_soft_entities,
            v_start=self.n_soft_verts,
            vvert_start=self.n_soft_vverts,
            vface_start=self.n_soft_vfaces,
            name=name,
        )
        self._soft_entities.append(entity)
        return entity

    return super().add_entity(idx, material, morph, surface, visualize_contact, name, desc)

build

build()
Source code in eden/engine/solvers/mochi/mochi_solver.py
def build(self):
    if self.is_active:
        other_solvers = [
            solver
            for solver in self.sim.solvers
            if solver is not self and solver.is_active and type(solver) is not KinematicSolver
        ]
        if other_solvers:
            gs.raise_exception(
                "MochiSolver cannot run alongside other physics solvers: "
                f"{[type(solver).__name__ for solver in other_solvers]}."
            )
        if self.sim.requires_grad:
            gs.raise_exception("MochiSolver does not support differentiable simulation.")

    self._n_geoms = self.n_geoms
    self._n_verts = self.n_verts
    self._n_free_verts = self.n_free_verts
    self._n_fixed_verts = self.n_fixed_verts
    self._n_faces = self.n_faces
    self._n_edges = self.n_edges
    self._geoms = self.geoms

    self.n_geoms_ = max(1, self.n_geoms)
    self.n_verts_ = max(1, self.n_verts)
    self.n_faces_ = max(1, self.n_faces)
    self.n_edges_ = max(1, self.n_edges)
    self.n_free_verts_ = max(1, self.n_free_verts)
    self.n_fixed_verts_ = max(1, self.n_fixed_verts)

    super().build()
    self._init_default_armature()
    self._external_state_dirty_mask = np.zeros((self._B,), dtype=bool)
    # Which of the two linear arms have environments to solve; refreshed at every substep by _select_linear_arms.
    self._has_dense_envs = False
    self._has_pcg_envs = False

    if not self.is_active:
        return

    self.n_soft_entities_ = max(1, self.n_soft_entities)
    self.n_soft_verts_ = max(1, self.n_soft_verts)
    self.n_soft_elems_ = max(1, self.n_soft_elems)
    self.n_shell_elems_ = max(1, self.n_shell_elems)
    self.n_rod_elems_ = max(1, self.n_rod_elems)
    self.n_rod_stencils_ = max(1, self.n_rod_stencils)
    self.n_attachments_ = max(1, self.n_attachments)
    self.n_dofs_total_ = max(1, self.n_dofs_total)

    if gs.qd_float == qd.f32 and any(
        entity.material.penalty_coefficient >= 1e8 for entity in (*self._entities, *self._soft_entities)
    ):
        gs.logger.warning(
            "MochiSolver runs in single precision with a contact stiffness of 1e8 Pa/m or more: the Newton system "
            "is ill-conditioned and contact accuracy degrades. Consider `gs.init(precision='64')`."
        )

    self._init_vert_fields()
    self._init_geom_fields()
    kernel_update_geoms(
        self._scene._envs_idx, self.dyn_state, self.dyn_info, self.rigid_info, self.rigid_config, True
    )
    self._init_mochi()
    # The base solver latches these flags at the end of each step and raises them at the start of the next, and a
    # setter of the state of some envs clears theirs (see Solver.register_errors)
    self.register_errors(
        self.data_manager.errno,
        {
            MochiErrorCode.OVERFLOW_MOCHI_CONTACT_PAIRS: (
                f"Exceeding max number of contact pairs ({self._max_pairs}). Please increase the value of "
                "MochiSolver's option 'max_contact_pairs_per_env'."
            ),
            MochiErrorCode.OVERFLOW_MOCHI_CONTACTS: (
                "Exceeding the capacity of a contact list: increase MochiSolver's option "
                "'max_soft_hits_per_sample', 'max_deformable_collider_hits_per_query' or "
                "'max_point_cloud_hits_per_query' (see `get_contact_capacity_usage()`). With "
                "record_contacts=True the rigid contact record (2 slots per sample, not configurable) can fill too."
            ),
            MochiErrorCode.MOCHI_DIVERGED: (
                "MochiSolver diverged: the residual of the implicit solve blew up. Reduce the time step or the "
                "contact stiffness, or increase 'n_newton_iterations'."
            ),
        },
    )
    self._init_soft()
    self._step_kernel = self._resolve_step_kernel()
    # loop counters of the graph step kernel (the same physical arrays every launch)
    self._newton_counter = qd.ndarray(qd.i32, shape=())
    self._round_counter = qd.ndarray(qd.i32, shape=())
    self._pcg_counter = qd.ndarray(qd.i32, shape=())
    # The contact points recorded for readback are allocated at the first readback (see `_record_contacts`).
    self.hit_readback = get_mochi_hit_readback(self, 1, 1, 1, 1)
    self._is_hit_readback_allocated = False
    self._links_entity_idx = np.array([link.entity.idx for link in self.links] or [-1], dtype=gs.np_int)
    self._geoms_link_idx = np.array([geom.link.idx for geom in self.geoms] or [-1], dtype=gs.np_int)
    self._soft_entities_idx = np.array([entity.idx for entity in self._soft_entities] or [-1], dtype=gs.np_int)
    links_node, dofs_node = self._island_nodes()
    self.island_state = get_mochi_island_state(self, links_node, dofs_node)
    if self._options.use_sleeping:
        self._dofs_node = torch.as_tensor(dofs_node, device=gs.device)
        n_nodes = len(self._entities) + len(self._soft_entities)
        self._nodes_idx = torch.arange(n_nodes, dtype=gs.tc_int, device=gs.device)
    self._init_sdf()
    self.subscribe(self._external_state_subscriber)
    kernel_reset_history(
        self._scene._envs_idx, self.dyn_state, self.dyn_info, self.mochi_info, self.mochi_state, self.rigid_config
    )
    self._forward_kinematics()

substep_pre_coupling

substep_pre_coupling(f)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def substep_pre_coupling(self, f):
    if not self.is_active:
        return
    self._sync_external_state()
    if self._step_kernel == "graph":
        options = self._options
        n_linesearch = (
            0 if self.mochi_config.linesearch_type == LINESEARCH.NONE else options.n_linesearch_iterations
        )
        pcg_unroll = options.graph_pcg_unroll
        kernel_step_graph(
            self._newton_counter,
            self._round_counter,
            self._pcg_counter,
            self.dyn_state,
            self.dyn_info,
            self.rigid_info,
            self.sdf_info,
            self.geoms_init_AABB,
            self.mochi_info,
            self.mochi_state,
            self.contact_state,
            self.hit_readback,
            self.island_state,
            self.eq_info,
            self.eq_state,
            self.soft_info,
            self.soft_state,
            self.rigid_config,
            self.mochi_config,
            self.n_shell_elems > 0,
            self.n_rod_elems > 0,
            pcg_unroll,
            max(1, n_linesearch),
            self._dense_max_dofs,
            len(self._entities),
            options.n_newton_iterations,
            self._n_pcg_iterations,
            -(-self._n_pcg_iterations // pcg_unroll),
            self._max_samples_per_soft_entity,
            self._errno,
        )
        self._is_forward_pos_updated = True
        self._is_forward_vel_updated = True
        self._is_contacts_recorded = False
        return
    if self._step_kernel == "monolith":
        options = self._options
        n_linesearch = (
            0 if self.mochi_config.linesearch_type == LINESEARCH.NONE else options.n_linesearch_iterations
        )
        kernel_step_monolith(
            self.dyn_state,
            self.dyn_info,
            self.rigid_info,
            self.sdf_info,
            self.geoms_init_AABB,
            self.mochi_info,
            self.mochi_state,
            self.contact_state,
            self.hit_readback,
            self.island_state,
            self.eq_info,
            self.eq_state,
            self.soft_info,
            self.soft_state,
            self.rigid_config,
            self.mochi_config,
            self.n_shell_elems > 0,
            self.n_rod_elems > 0,
            max(1, n_linesearch),
            self._dense_max_dofs,
            len(self._entities),
            options.n_newton_iterations,
            self._n_pcg_iterations,
            self._max_samples_per_soft_entity,
            self._errno,
        )
        self._is_forward_pos_updated = True
        self._is_forward_vel_updated = True
        self._is_contacts_recorded = False
        return

    kernel_step_start(
        self.dyn_state,
        self.dyn_info,
        self.rigid_info,
        self.mochi_info,
        self.mochi_state,
        self.island_state,
        self.rigid_config,
        self.mochi_config,
    )
    if self.has_soft:
        kernel_soft_step_start(
            self.mochi_state, self.soft_info, self.soft_state, self.rigid_config, self.mochi_config
        )
        if self.n_shell_elems > 0:
            kernel_shell_stage_start(self.mochi_state, self.soft_info, self.soft_state, self.rigid_config)
        if self.n_rod_elems > 0:
            kernel_rod_step_start(
                self.mochi_state, self.soft_info, self.soft_state, self.rigid_config, self.mochi_config
            )
    self._forward_kinematics(with_velocity=False)
    kernel_store_stage_start_poses(self.dyn_state, self.mochi_state, self.rigid_config)
    if self.mochi_config.has_equalities:
        kernel_equalities_stage_start(
            self.dyn_info,
            self.rigid_info,
            self.mochi_info,
            self.mochi_state,
            self.eq_info,
            self.eq_state,
            self.rigid_config,
        )
    if self.mochi_config.has_attachments:
        kernel_attachments_stage_start(self.mochi_state, self.soft_info, self.soft_state, self.rigid_config)
    kernel_reset_newton(self.mochi_state, self.rigid_config)
    kernel_conservative_bounds(
        self.dyn_state, self.dyn_info, self.mochi_info, self.mochi_state, self.contact_state, self.rigid_config
    )
    kernel_broadphase_pairs(
        self.dyn_state,
        self.dyn_info,
        self.mochi_info,
        self.mochi_state,
        self.contact_state,
        self.rigid_config,
        self._errno,
    )
    if self.has_soft:
        kernel_soft_conservative_bounds(
            self.mochi_state, self.mochi_info, self.soft_info, self.soft_state, self.rigid_config
        )
        kernel_soft_broadphase(
            self.dyn_state,
            self.dyn_info,
            self.mochi_info,
            self.mochi_state,
            self.contact_state,
            self.soft_info,
            self.soft_state,
            self.rigid_config,
            self._errno,
        )
    kernel_build_islands(
        self.dyn_info,
        self.mochi_info,
        self.mochi_state,
        self.contact_state,
        self.soft_info,
        self.soft_state,
        self.island_state,
        self.eq_info,
        self._dense_max_dofs,
        len(self._entities),
        self.rigid_config,
        self.has_soft,
        self.mochi_config.has_dense,
        self.mochi_config.has_equalities,
        self.mochi_config.use_sleeping,
    )
    self._select_linear_arms()
    self._newton_solve()
    kernel_post_stage(
        self.dyn_state, self.dyn_info, self.rigid_info, self.mochi_info, self.mochi_state, self.rigid_config
    )
    if self.has_soft:
        kernel_soft_post_stage(self.mochi_state, self.soft_info, self.soft_state, self.rigid_config)
        if self.n_rod_elems > 0:
            kernel_rod_post_stage(self.mochi_state, self.soft_info, self.soft_state, self.rigid_config)
    if self.mochi_config.use_sleeping:
        kernel_update_sleep(
            len(self._entities),
            self.dyn_state,
            self.dyn_info,
            self.mochi_info,
            self.mochi_state,
            self.soft_info,
            self.soft_state,
            self.island_state,
            self.rigid_config,
            self.has_soft,
        )
    self._forward_kinematics()
    self._is_forward_pos_updated = True
    self._is_forward_vel_updated = True
    self._is_contacts_recorded = False

substep_post_coupling

substep_post_coupling(f)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def substep_post_coupling(self, f):
    if self.is_active and self.n_dofs > 0:
        kernel_record_dofs_control_force(self.dyn_state, self.dyn_info, self.rigid_info, self.rigid_config)

get_soft_state_render

get_soft_state_render(f)

Environment-offset render vertex positions of the deformable surfaces, shape (n_vverts, B), as (positions, None, None) (UVs and faces are read from the visual geoms).

Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_soft_state_render(self, f):
    """Environment-offset render vertex positions of the deformable surfaces, shape (n_vverts, B), as (positions,
    None, None) (UVs and faces are read from the visual geoms).
    """
    if not self.has_soft or self.n_soft_vverts == 0:
        return None, None, None
    kernel_soft_get_state_render(
        self._soft_vverts_render,
        self._soft_vverts_vert_idx,
        self._soft_vverts_elem,
        self._soft_vverts_bary,
        self._envs_offset,
        self.soft_info,
        self.soft_state,
        self.rigid_config,
    )
    if self._n_rod_vverts > 0:
        kernel_rod_get_state_render(
            self._soft_vverts_render,
            self._rod_vverts_vvert,
            self._rod_vverts_node,
            self._rod_vverts_elem,
            self._rod_vverts_offset,
            self._envs_offset,
            self.soft_info,
            self.soft_state,
            self.rigid_config,
        )
    return self._soft_vverts_render, None, None

get_state

get_state(f=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_state(self, f=None):
    if not self.is_active:
        return None
    state = MochiSolverState(self, self.sim.cur_step_global)
    kernel_get_kinematic_state(
        state.qpos,
        state.dofs_vel,
        state.links_pos,
        state.links_quat,
        self.dyn_state,
        self.rigid_info,
        self.rigid_config,
    )
    state.qpos_prev = qd_to_torch(self.mochi_state.qpos_prev, copy=True).permute((2, 0, 1)).contiguous()
    state.dofs_vel_prev = qd_to_torch(self.mochi_state.dofs_vel_prev, copy=True).permute((2, 0, 1)).contiguous()
    state.links_vel = qd_to_torch(self.mochi_state.links_vel, copy=True).permute((1, 0, 2)).contiguous()
    state.links_ang = qd_to_torch(self.mochi_state.links_ang, copy=True).permute((1, 0, 2)).contiguous()
    state.links_vsym = qd_to_torch(self.mochi_state.links_vsym, copy=True).permute((1, 0, 2, 3)).contiguous()
    state.links_vel_prev = (
        qd_to_torch(self.mochi_state.links_vel_prev, copy=True).permute((2, 0, 1, 3)).contiguous()
    )
    state.links_ang_prev = (
        qd_to_torch(self.mochi_state.links_ang_prev, copy=True).permute((2, 0, 1, 3)).contiguous()
    )
    state.links_vsym_prev = (
        qd_to_torch(self.mochi_state.links_vsym_prev, copy=True).permute((2, 0, 1, 3, 4)).contiguous()
    )
    state.n_hist = qd_to_torch(self.mochi_state.n_hist, copy=True)
    if self.has_soft:
        kernel_soft_get_state(
            state.soft_pos,
            state.soft_vel,
            state.soft_pos_prev,
            state.soft_vel_prev,
            self.soft_state,
            self.rigid_config,
        )
    return state

set_state

set_state(f, state, envs_idx=None, *, partial: bool = False) -> None
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_state(self, f, state, envs_idx=None, *, partial: bool = False) -> None:
    if not self.is_active:
        return
    # The kinematic state is restored (and subscribers notified) by the base class; the integration history is
    # restored on top, leaving nothing for the external-state sync of the next step to rebuild.
    super().set_state(f, state, envs_idx, partial=False)
    envs_idx = self._scene._sanitize_envs_idx(envs_idx)
    _kernel_set_history(
        envs_idx,
        state.qpos_prev,
        state.dofs_vel_prev,
        state.links_vel,
        state.links_ang,
        state.links_vsym,
        state.links_vel_prev,
        state.links_ang_prev,
        state.links_vsym_prev,
        state.n_hist,
        self.mochi_state,
        self.rigid_config,
    )
    kernel_update_geoms(
        self._scene._envs_idx, self.dyn_state, self.dyn_info, self.rigid_info, self.rigid_config, False
    )
    kernel_update_geom_aabbs(self.geoms_init_AABB, self.dyn_state, self.dyn_info, self.rigid_config)
    if self.has_soft:
        kernel_soft_set_state(
            envs_idx,
            state.soft_pos,
            state.soft_vel,
            state.soft_pos_prev,
            state.soft_vel_prev,
            self.soft_state,
            self.rigid_config,
        )
    self._external_state_dirty_mask[tensor_to_array(envs_idx)] = False
    self._is_contacts_recorded = False
    # A restored environment starts over: whatever error it raised (a diverged solve) is cleared, as the rigid
    # solver clears its own, so that resetting the environments `get_error_envs_mask` names lets the others go on.
    self.clear_errno(envs_idx)

set_qpos

set_qpos(qpos, qs_idx=None, envs_idx=None, *, skip_forward=False)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_qpos(self, qpos, qs_idx=None, envs_idx=None, *, skip_forward=False):
    super().set_qpos(qpos, qs_idx, envs_idx, skip_forward=skip_forward)
    # Setting the configuration of an environment clears its error, as restoring its state does.
    self.clear_errno(envs_idx)

set_dofs_position

set_dofs_position(position, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_dofs_position(self, position, dofs_idx=None, envs_idx=None):
    super().set_dofs_position(position, dofs_idx, envs_idx)
    self.clear_errno(envs_idx)

destroy_arrays

destroy_arrays() -> None

Free the ndarrays of the solver right away, as Simulator.destroy frees those of the other solvers.

Simulator.destroy gathers the arrays to free from data, which this solver refuses, so the destroy of its scene calls this instead, after it (see eden.engine.solvers.mochi.hosting). They are those its attributes hold, alone or in the structs (dataclasses) and the containers they hold. One already freed — by an earlier call, or by Simulator.destroy as the array of another solver — is left alone; a field is freed by qd.reset.

Source code in eden/engine/solvers/mochi/mochi_solver.py
def destroy_arrays(self) -> None:
    """Free the ndarrays of the solver right away, as `Simulator.destroy` frees those of the other solvers.

    `Simulator.destroy` gathers the arrays to free from `data`, which this solver refuses, so the destroy of its
    scene calls this instead, after it (see `eden.engine.solvers.mochi.hosting`). They are those its attributes
    hold, alone or in the structs (dataclasses) and the containers they hold. One already freed — by an earlier
    call, or by `Simulator.destroy` as the array of another solver — is left alone; a field is freed by `qd.reset`.
    """
    for array in _iter_ndarrays(vars(self).values(), set()):
        if array.arr is not None:
            qd_destroy_data(array)

get_contacts

get_contacts(as_tensor: bool = True, to_torch: bool = True, is_padded: bool = False)

Contact points of the current state, as a dict of arrays laid out (n_envs, n_contacts, ...) (see MochiEntity.get_contacts and MochiSoftEntity.get_contacts). The contact axis holds the largest per-environment count; shorter environments carry -1 indices and zeros in their unused slots.

Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_contacts(self, as_tensor: bool = True, to_torch: bool = True, is_padded: bool = False):
    """
    Contact points of the current state, as a dict of arrays laid out (n_envs, n_contacts, ...) (see
    `MochiEntity.get_contacts` and `MochiSoftEntity.get_contacts`). The contact axis holds the largest
    per-environment count; shorter environments carry -1 indices and zeros in their unused slots.
    """
    self._record_contacts()
    n_contacts = torch.zeros((self._B,), dtype=gs.tc_int, device=gs.device)
    kernel_count_contact_records(self.hit_readback, self.soft_state, n_contacts, self.rigid_config, self.has_soft)
    n_max = max(1, int(n_contacts.max()))

    def ints(*shape):
        return torch.full((self._B, n_max, *shape), -1, dtype=gs.tc_int, device=gs.device)

    def floats(*shape):
        return torch.zeros((self._B, n_max, *shape), dtype=gs.tc_float, device=gs.device)

    contact_data = {
        "entity_a": ints(),
        "entity_b": ints(),
        "link_a": ints(),
        "link_b": ints(),
        "geom_a": ints(),
        "geom_b": ints(),
        "verts_a": ints(4),
        "bary_a": floats(4),
        "verts_b": ints(4),
        "bary_b": floats(4),
        "position": floats(3),
        "normal": floats(3),
        "distance": floats(),
        "force_a": floats(3),
        "weight": floats(),
    }
    kernel_gather_contact_records(
        self._links_entity_idx,
        self._geoms_link_idx,
        self._soft_entities_idx,
        self.mochi_info.samples,
        self.hit_readback,
        self.soft_info,
        self.soft_state,
        self.rigid_config,
        self.has_soft,
        contact_data["entity_a"],
        contact_data["entity_b"],
        contact_data["link_a"],
        contact_data["link_b"],
        contact_data["geom_a"],
        contact_data["geom_b"],
        contact_data["verts_a"],
        contact_data["bary_a"],
        contact_data["verts_b"],
        contact_data["bary_b"],
        contact_data["position"],
        contact_data["normal"],
        contact_data["force_a"],
        contact_data["distance"],
        contact_data["weight"],
    )
    contact_data["force_b"] = -contact_data["force_a"]
    if is_padded:
        contact_data["n_contacts"] = n_contacts if self.n_envs > 0 else n_contacts[0]
    if self.n_envs == 0:
        contact_data = {key: value[0] for key, value in contact_data.items()}
    if not to_torch:
        contact_data = {key: tensor_to_array(value) for key, value in contact_data.items()}
    return contact_data

get_collider_distances

get_collider_distances(geom_idx, points_world, envs_idx=None)

Signed distance and distance gradient of world points to a collider geom, in the world frame, as seen by the contact model (a grid collider only answers inside its grid; other points are flagged invalid).

RETURNS DESCRIPTION
(distances, gradients, is_valid) : tuple of arrays of shape (n_points,), (n_points, 3), (n_points,)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_collider_distances(self, geom_idx, points_world, envs_idx=None):
    """
    Signed distance and distance gradient of world points to a collider geom, in the world frame, as seen by the
    contact model (a grid collider only answers inside its grid; other points are flagged invalid).

    Returns
    -------
    (distances, gradients, is_valid) : tuple of arrays of shape (n_points,), (n_points, 3), (n_points,)
    """
    envs_idx = self._scene._sanitize_envs_idx(envs_idx)
    points = torch.as_tensor(points_world, dtype=gs.tc_float, device=gs.device).reshape((-1, 3)).contiguous()
    distances = torch.zeros((points.shape[0],), dtype=gs.tc_float, device=gs.device)
    gradients = torch.zeros((points.shape[0], 3), dtype=gs.tc_float, device=gs.device)
    is_valid = torch.zeros((points.shape[0],), dtype=torch.bool, device=gs.device)
    _kernel_get_collider_distances(
        int(geom_idx),
        int(envs_idx[0]),
        points,
        distances,
        gradients,
        is_valid,
        self.dyn_state,
        self.dyn_info,
        self.sdf_info,
        self.mochi_info,
        self.mochi_config,
    )
    return distances, gradients, is_valid

get_convergence_info

get_convergence_info()

Per-environment outcome of the last solve: number of Newton iterations, number of conjugate-gradient iterations summed over the Newton iterations, status (0 running, 1 converged, 2 stopped at the iteration budget, 3 diverged), and the plain residual norm before and after the solve.

Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_convergence_info(self):
    """Per-environment outcome of the last solve: number of Newton iterations, number of conjugate-gradient
    iterations summed over the Newton iterations, status (0 running, 1 converged, 2 stopped at the iteration budget,
    3 diverged), and the plain residual norm before and after the solve.
    """
    return {
        "n_iter": qd_to_torch(self.mochi_state.n_iter, copy=True),
        "n_pcg_iter": qd_to_torch(self.mochi_state.n_pcg_iter, copy=True),
        "status": qd_to_torch(self.mochi_state.status, copy=True),
        "res_norm0": qd_to_torch(self.mochi_state.res_norm0, copy=True),
        "res_norm": qd_to_torch(self.mochi_state.res_norm_sq, copy=True).sqrt(),
    }

get_contact_capacity_usage

get_contact_capacity_usage()

Per bounded contact list, the largest per-environment count of the last full assembly (the one the last linear solve used) and the capacity of the list (what the max_* options size), to tune the capacities of a scene.

Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_contact_capacity_usage(self):
    """Per bounded contact list, the largest per-environment count of the last full assembly (the one the last
    linear solve used) and the capacity of the list (what the `max_*` options size), to tune the capacities of a
    scene.
    """
    usage = {"contact_pairs": (int(qd_to_numpy(self.contact_state.n_pairs).max()), self._max_pairs)}
    if self.has_soft:
        usage["soft_hits"] = (int(qd_to_numpy(self.soft_state.n_soft_hits_max)), self._max_soft_hits)
        usage["soft_collider_hits"] = (int(qd_to_numpy(self.soft_state.n_sc_hits_max)), self._max_sc_hits)
        usage["point_cloud_hits"] = (int(qd_to_numpy(self.soft_state.n_pc_hits_max)), self._max_pc_hits)
    return usage

set_dofs_kp

set_dofs_kp(kp, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_dofs_kp(self, kp, dofs_idx=None, envs_idx=None):
    self._set_dofs_info([kp], dofs_idx, "kp", envs_idx)

set_dofs_kv

set_dofs_kv(kv, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_dofs_kv(self, kv, dofs_idx=None, envs_idx=None):
    self._set_dofs_info([kv], dofs_idx, "kv", envs_idx)

set_dofs_stiffness

set_dofs_stiffness(stiffness, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_dofs_stiffness(self, stiffness, dofs_idx=None, envs_idx=None):
    self._set_dofs_info([stiffness], dofs_idx, "stiffness", envs_idx)

set_dofs_armature

set_dofs_armature(armature, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_dofs_armature(self, armature, dofs_idx=None, envs_idx=None):
    self._set_dofs_info([armature], dofs_idx, "armature", envs_idx)

set_dofs_damping

set_dofs_damping(damping, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_dofs_damping(self, damping, dofs_idx=None, envs_idx=None):
    self._set_dofs_info([damping], dofs_idx, "damping", envs_idx)

set_dofs_limit

set_dofs_limit(lower, upper, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_dofs_limit(self, lower, upper, dofs_idx=None, envs_idx=None):
    self._set_dofs_info([lower, upper], dofs_idx, "limit", envs_idx)
get_links_pos(links_idx=None, envs_idx=None, *, ref: link_ref_frame = link_origin, relative=False)

Link positions in the requested reference frame, as RigidSolver.get_links_pos.

KinematicSolver's own getter knows only the link origin; the inertial frames the other references name are maintained by this solver as well, so the whole ref vocabulary is answered here.

Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_links_pos(
    self,
    links_idx=None,
    envs_idx=None,
    *,
    ref: link_ref_frame = link_ref_frame.link_origin,
    relative=False,
):
    """Link positions in the requested reference frame, as ``RigidSolver.get_links_pos``.

    ``KinematicSolver``'s own getter knows only the link origin; the inertial frames the other references name
    are maintained by this solver as well, so the whole ``ref`` vocabulary is answered here.
    """
    ref_frame = self._sanitize_ref_frame(ref)
    if ref_frame == link_ref_frame.root_COM:
        tensor = qd_to_torch(self.dyn_state.links.root_COM, envs_idx, links_idx, transpose=True, copy=True)
    elif ref_frame == link_ref_frame.link_COM:
        i_pos = qd_to_torch(self.dyn_state.links.i_pos, envs_idx, links_idx, transpose=True)
        root_COM = qd_to_torch(self.dyn_state.links.root_COM, envs_idx, links_idx, transpose=True)
        tensor = i_pos + root_COM
    else:
        return super().get_links_pos(links_idx, envs_idx, relative=relative)
    return tensor[0] if self.n_envs == 0 else tensor
get_links_vel(links_idx=None, envs_idx=None, *, ref: link_ref_frame = link_origin, relative=False)

Link velocities in the requested reference frame, as RigidSolver.get_links_vel.

Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_links_vel(
    self, links_idx=None, envs_idx=None, *, ref: link_ref_frame = link_ref_frame.link_origin, relative=False
):
    """Link velocities in the requested reference frame, as ``RigidSolver.get_links_vel``."""
    ref_frame = self._sanitize_ref_frame(ref, has_root_COM=False)
    if ref_frame == link_ref_frame.link_origin:
        return super().get_links_vel(links_idx, envs_idx, relative=relative)
    cd_vel = qd_to_torch(self.dyn_state.links.cd_vel, envs_idx, links_idx, transpose=True, copy=True)
    cd_ang = qd_to_torch(self.dyn_state.links.cd_ang, envs_idx, links_idx, transpose=True)
    i_pos = qd_to_torch(self.dyn_state.links.i_pos, envs_idx, links_idx, transpose=True)
    tensor = cd_vel + cd_ang.cross(i_pos, dim=-1)
    return tensor[0] if self.n_envs == 0 else tensor

get_dofs_kp

get_dofs_kp(dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_dofs_kp(self, dofs_idx=None, envs_idx=None):
    return self._get_dofs_pd_gains(dofs_idx, envs_idx)[0]

get_dofs_kv

get_dofs_kv(dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_dofs_kv(self, dofs_idx=None, envs_idx=None):
    return self._get_dofs_pd_gains(dofs_idx, envs_idx)[1]

get_dofs_act_gain

get_dofs_act_gain(dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_dofs_act_gain(self, dofs_idx=None, envs_idx=None):
    return self._get_dofs_info(self.dyn_info.dofs.act_gain, dofs_idx, envs_idx)

get_dofs_act_bias

get_dofs_act_bias(dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_dofs_act_bias(self, dofs_idx=None, envs_idx=None):
    tensor = self._get_dofs_info(self.dyn_info.dofs.act_bias, dofs_idx, envs_idx)
    return tensor[..., 0], tensor[..., 1], tensor[..., 2]

get_dofs_force_range

get_dofs_force_range(dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_dofs_force_range(self, dofs_idx=None, envs_idx=None):
    tensor = self._get_dofs_info(self.dyn_info.dofs.force_range, dofs_idx, envs_idx)
    return tensor[..., 0], tensor[..., 1]

get_dofs_stiffness

get_dofs_stiffness(dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_dofs_stiffness(self, dofs_idx=None, envs_idx=None):
    return self._get_dofs_info(self.dyn_info.dofs.stiffness, dofs_idx, envs_idx)

get_dofs_invweight

get_dofs_invweight(dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_dofs_invweight(self, dofs_idx=None, envs_idx=None):
    return self._get_dofs_info(self.dyn_info.dofs.invweight, dofs_idx, envs_idx)

get_dofs_armature

get_dofs_armature(dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_dofs_armature(self, dofs_idx=None, envs_idx=None):
    return self._get_dofs_info(self.dyn_info.dofs.armature, dofs_idx, envs_idx)

get_dofs_damping

get_dofs_damping(dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_dofs_damping(self, dofs_idx=None, envs_idx=None):
    return self._get_dofs_info(self.dyn_info.dofs.damping, dofs_idx, envs_idx)

get_dofs_frictionloss

get_dofs_frictionloss(dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_dofs_frictionloss(self, dofs_idx=None, envs_idx=None):
    return self._get_dofs_info(self.dyn_info.dofs.frictionloss, dofs_idx, envs_idx)

set_dofs_force_range

set_dofs_force_range(lower, upper, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_dofs_force_range(self, lower, upper, dofs_idx=None, envs_idx=None):
    gs.raise_exception(
        "`set_dofs_force_range` is not supported by the MochiSolver: drive forces are not clamped to a force "
        "range, so a new range would silently have no effect."
    )

set_dofs_frictionloss

set_dofs_frictionloss(frictionloss, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_dofs_frictionloss(self, frictionloss, dofs_idx=None, envs_idx=None):
    gs.raise_exception(
        "`set_dofs_frictionloss` is not supported by the MochiSolver: joint friction loss is not modeled, so a "
        "new value would silently have no effect. Use joint damping instead."
    )

set_dofs_act_gain

set_dofs_act_gain(act_gain, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_dofs_act_gain(self, act_gain, dofs_idx=None, envs_idx=None):
    gs.raise_exception("`set_dofs_act_gain` is not supported by the MochiSolver: use `set_dofs_kp`.")

set_dofs_act_bias

set_dofs_act_bias(act_bias, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_dofs_act_bias(self, act_bias, dofs_idx=None, envs_idx=None):
    gs.raise_exception(
        "`set_dofs_act_bias` is not supported by the MochiSolver: use `set_dofs_kp` / `set_dofs_kv`."
    )

control_dofs_force

control_dofs_force(force, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def control_dofs_force(self, force, dofs_idx=None, envs_idx=None):
    force, dofs_idx, envs_idx = self._sanitize_control(force, dofs_idx, envs_idx)
    kernel_control_dofs_force(dofs_idx, envs_idx, force, self.dyn_state, self.rigid_config)
    self._wake_nodes(dofs_idx, self._dofs_node, envs_idx)

control_dofs_velocity

control_dofs_velocity(velocity, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def control_dofs_velocity(self, velocity, dofs_idx=None, envs_idx=None):
    velocity, dofs_idx, envs_idx = self._sanitize_control(velocity, dofs_idx, envs_idx)
    kernel_control_dofs_velocity(dofs_idx, envs_idx, velocity, self.dyn_state, self.rigid_config)
    self._wake_nodes(dofs_idx, self._dofs_node, envs_idx)

control_dofs_position

control_dofs_position(position, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def control_dofs_position(self, position, dofs_idx=None, envs_idx=None):
    position, dofs_idx, envs_idx = self._sanitize_control(position, dofs_idx, envs_idx)
    kernel_control_dofs_position(dofs_idx, envs_idx, position, self.dyn_state, self.rigid_config)
    self._wake_nodes(dofs_idx, self._dofs_node, envs_idx)

control_dofs_position_velocity

control_dofs_position_velocity(position, velocity, dofs_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def control_dofs_position_velocity(self, position, velocity, dofs_idx=None, envs_idx=None):
    position, dofs_idx, envs_idx_ = self._sanitize_control(position, dofs_idx, envs_idx)
    velocity, _, _ = self._sanitize_control(velocity, dofs_idx, envs_idx)
    kernel_control_dofs_position_velocity(
        dofs_idx, envs_idx_, position, velocity, self.dyn_state, self.rigid_config
    )
    self._wake_nodes(dofs_idx, self._dofs_node, envs_idx_)

get_dofs_control_force

get_dofs_control_force(dofs_idx=None, envs_idx=None)

The control force the last step applied, as the rigid solver reports it (see substep_post_coupling).

Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_dofs_control_force(self, dofs_idx=None, envs_idx=None):
    """The control force the last step applied, as the rigid solver reports it (see `substep_post_coupling`)."""
    tensor = self._read_data(self.dyn_state.dofs.qf_applied, envs_idx, dofs_idx, copy=True)
    return tensor[0] if self.n_envs == 0 else tensor
set_links_contact_params(links_idx, **params)
Source code in eden/engine/solvers/mochi/mochi_solver.py
@mutates(StateChange.MODEL)
def set_links_contact_params(self, links_idx, **params):
    links_idx = tensor_to_array(links_idx).reshape((-1,))
    geoms_idx = np.concatenate(
        [np.arange(self.links[i_l].geom_start, self.links[i_l].geom_end) for i_l in links_idx], dtype=gs.np_int
    )
    for name, value in params.items():
        if value is None:
            continue
        field = self.mochi_info.geoms.__getattribute__(name)
        values = qd_to_numpy(field)
        values[geoms_idx] = value
        field.from_numpy(values)
    self._wake_nodes(self._nodes_idx, self._nodes_idx, self._scene._envs_idx)
get_links_mass(links_idx=None, envs_idx=None)

The mass of each link, as the solver currently uses it.

Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_links_mass(self, links_idx=None, envs_idx=None):
    """The mass of each link, as the solver currently uses it."""
    if not self._options.batch_links_info and envs_idx is not None:
        gs.raise_exception("`envs_idx` cannot be specified for non-batched links info.")
    tensor = qd_to_torch(self.dyn_info.links.inertial_mass, envs_idx, links_idx, transpose=True, copy=True)
    return tensor[0] if self.n_envs == 0 and self._options.batch_links_info else tensor
get_links_inertia(links_idx=None, envs_idx=None)

The inertia matrix of each link, in its inertial frame, as the solver currently uses it.

Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_links_inertia(self, links_idx=None, envs_idx=None):
    """The inertia matrix of each link, in its inertial frame, as the solver currently uses it."""
    if not self._options.batch_links_info and envs_idx is not None:
        gs.raise_exception("`envs_idx` cannot be specified for non-batched links info.")
    tensor = qd_to_torch(self.dyn_info.links.inertial_i, envs_idx, links_idx, transpose=True, copy=True)
    return tensor[0] if self.n_envs == 0 and self._options.batch_links_info else tensor
get_links_COM(links_idx=None, envs_idx=None)

The center of mass (COM) of each link, as an offset from the origin of its local frame.

Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_links_COM(self, links_idx=None, envs_idx=None):
    """The center of mass (COM) of each link, as an offset from the origin of its local frame."""
    if not self._options.batch_links_info and envs_idx is not None:
        gs.raise_exception("`envs_idx` cannot be specified for non-batched links info.")
    tensor = qd_to_torch(self.dyn_info.links.inertial_pos, envs_idx, links_idx, transpose=True, copy=True)
    return tensor[0] if self.n_envs == 0 and self._options.batch_links_info else tensor
get_links_invweight(links_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_links_invweight(self, links_idx=None, envs_idx=None):
    if not self._options.batch_links_info and envs_idx is not None:
        gs.raise_exception("`envs_idx` cannot be specified for non-batched links info.")
    tensor = qd_to_torch(self.dyn_info.links.invweight, envs_idx, links_idx, transpose=True, copy=True)
    return tensor[0] if self.n_envs == 0 and self._options.batch_links_info else tensor
set_links_mass(mass, links_idx=None, envs_idx=None, scale_inertia=False)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_links_mass(self, mass, links_idx=None, envs_idx=None, scale_inertia=False):
    gs.raise_exception("`set_links_mass` is not supported by the MochiSolver: link masses are fixed at build.")
set_links_inertia(inertia, links_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_links_inertia(self, inertia, links_idx=None, envs_idx=None):
    gs.raise_exception("`set_links_inertia` is not supported by the MochiSolver: link inertias are fixed at build.")
set_links_COM(com, links_idx=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_links_COM(self, com, links_idx=None, envs_idx=None):
    gs.raise_exception("`set_links_COM` is not supported by the MochiSolver: link inertials are fixed at build.")
set_links_has_gravity(links_idx, has_gravity)
Source code in eden/engine/solvers/mochi/mochi_solver.py
@mutates(StateChange.MODEL)
def set_links_has_gravity(self, links_idx, has_gravity):
    links_idx = tensor_to_array(links_idx).reshape((-1,))
    values = qd_to_numpy(self.mochi_info.links.has_gravity)
    values[links_idx] = has_gravity
    self.mochi_info.links.has_gravity.from_numpy(values)
    self._wake_nodes(self._nodes_idx, self._nodes_idx, self._scene._envs_idx)

set_gravity

set_gravity(gravity, envs_idx=None)

Set the gravity acting on the bodies of this solver, in m/s^2, and wake the environments it reaches.

Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_gravity(self, gravity, envs_idx=None):
    """Set the gravity acting on the bodies of this solver, in m/s^2, and wake the environments it reaches."""
    super().set_gravity(gravity, envs_idx)
    if self.mochi_config.use_sleeping:
        self._wake_nodes(self._nodes_idx, self._nodes_idx, self._scene._sanitize_envs_idx(envs_idx))

enable_layer_contact

enable_layer_contact(layer_a: str, layer_b: str, is_enabled: bool = True)

Enable or disable contact between two contact layers (symmetric).

Source code in eden/engine/solvers/mochi/mochi_solver.py
@mutates(StateChange.MODEL)
def enable_layer_contact(self, layer_a: str, layer_b: str, is_enabled: bool = True):
    """Enable or disable contact between two contact layers (symmetric)."""
    i_a, i_b = self._layers.index(layer_a), self._layers.index(layer_b)
    self._layers_pair_enabled[i_a, i_b] = self._layers_pair_enabled[i_b, i_a] = is_enabled
    kernel_set_links_pair_enabled(self._compute_links_pair_enabled(), self.mochi_info, self.rigid_config)
    if self.has_soft:
        kernel_soft_set_links_pair_enabled(
            self._compute_soft_links_pair_enabled(), self.soft_info, self.rigid_config
        )
        kernel_soft_set_pair_enabled(self._compute_soft_pair_enabled(), self.soft_info, self.rigid_config)
    self._wake_nodes(self._nodes_idx, self._nodes_idx, self._scene._envs_idx)

enable_entity_contact

enable_entity_contact(entity_a, entity_b, is_enabled: bool = True)

Enable or disable contact between two entities (symmetric).

Source code in eden/engine/solvers/mochi/mochi_solver.py
@mutates(StateChange.MODEL)
def enable_entity_contact(self, entity_a, entity_b, is_enabled: bool = True):
    """Enable or disable contact between two entities (symmetric)."""
    is_soft_a = isinstance(entity_a, MochiSoftEntity)
    is_soft_b = isinstance(entity_b, MochiSoftEntity)
    i_a, i_b = entity_a._idx_in_solver, entity_b._idx_in_solver
    if is_soft_a and is_soft_b:
        self._soft_entities_pair_enabled[i_a, i_b] = self._soft_entities_pair_enabled[i_b, i_a] = is_enabled
    elif is_soft_a:
        self._soft_rigid_entities_pair_enabled[i_a, i_b] = is_enabled
    elif is_soft_b:
        self._soft_rigid_entities_pair_enabled[i_b, i_a] = is_enabled
    else:
        self._entities_pair_enabled[i_a, i_b] = self._entities_pair_enabled[i_b, i_a] = is_enabled
    kernel_set_links_pair_enabled(self._compute_links_pair_enabled(), self.mochi_info, self.rigid_config)
    if self.has_soft:
        kernel_soft_set_links_pair_enabled(
            self._compute_soft_links_pair_enabled(), self.soft_info, self.rigid_config
        )
        kernel_soft_set_pair_enabled(self._compute_soft_pair_enabled(), self.soft_info, self.rigid_config)
    self._wake_nodes(self._nodes_idx, self._nodes_idx, self._scene._envs_idx)

get_soft_entity_state

get_soft_entity_state(entity, pos, vel)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_soft_entity_state(self, entity, pos, vel):
    kernel_soft_get_entity_state(entity.v_start, pos, vel, self.soft_state, self.rigid_config)

get_soft_vertices_position

get_soft_vertices_position(entity, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_soft_vertices_position(self, entity, envs_idx=None):
    return self._soft_field_of_entity(entity, self.soft_state.verts_pos, envs_idx)

get_soft_vertices_velocity

get_soft_vertices_velocity(entity, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_soft_vertices_velocity(self, entity, envs_idx=None):
    return self._soft_field_of_entity(entity, self.soft_state.verts_vel, envs_idx)

get_soft_vertices_contact_force

get_soft_vertices_contact_force(entity, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def get_soft_vertices_contact_force(self, entity, envs_idx=None):
    self._record_contacts()
    return self._soft_field_of_entity(entity, self.soft_state.verts_contact_force, envs_idx)

set_soft_vertices_position

set_soft_vertices_position(entity, pos, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_soft_vertices_position(self, entity, pos, envs_idx=None):
    envs_idx, pos = self._soft_values_of_entity(entity, pos, envs_idx)
    kernel_soft_set_vertices_positions(envs_idx, entity.v_start, pos, self.soft_state, self.rigid_config)
    self._mark_external_state_dirty(envs_idx)
    # Setting the configuration of a deformable body clears the error of its environments, as `set_qpos` does.
    self.clear_errno(envs_idx)
    self._is_contacts_recorded = False

set_soft_vertices_velocity

set_soft_vertices_velocity(entity, vel, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_soft_vertices_velocity(self, entity, vel, envs_idx=None):
    envs_idx, vel = self._soft_values_of_entity(entity, vel, envs_idx)
    kernel_soft_set_vertices_velocities(envs_idx, entity.v_start, vel, self.soft_state, self.rigid_config)
    self._mark_external_state_dirty(envs_idx)

set_soft_vertices_fixed

set_soft_vertices_fixed(entity, verts_idx, is_fixed, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_soft_vertices_fixed(self, entity, verts_idx, is_fixed, envs_idx=None):
    envs_idx = self._scene._sanitize_envs_idx(envs_idx)
    verts_idx = self._per_env_vertices(verts_idx, envs_idx)
    kernel_soft_set_vertices_fixed(
        envs_idx, verts_idx, entity.v_start, int(bool(is_fixed)), self.soft_state, self.rigid_config
    )
    self._mark_external_state_dirty(envs_idx)

set_soft_vertices_target

set_soft_vertices_target(entity, verts_idx, pos, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_solver.py
def set_soft_vertices_target(self, entity, verts_idx, pos, envs_idx=None):
    envs_idx = self._scene._sanitize_envs_idx(envs_idx)
    is_shared = verts_idx.ndim == 1
    verts_idx = self._per_env_vertices(verts_idx, envs_idx)
    n_verts = verts_idx.shape[1]
    # A tensor already on the device goes straight to the kernel: this is called every control step.
    pos = torch.as_tensor(pos, dtype=gs.tc_float, device=gs.device)
    if is_shared and pos.shape == (n_verts, 3):
        pos = pos[None].expand(len(envs_idx), -1, -1)
    if pos.shape != (len(envs_idx), n_verts, 3):
        gs.raise_exception(
            f"Expected an array of shape ({len(envs_idx)}, {n_verts}, 3)"
            + (f" or ({n_verts}, 3)" if is_shared else "")
            + f", got {tuple(pos.shape)}."
        )
    kernel_soft_set_vertices_target(
        envs_idx, verts_idx, entity.v_start, pos.contiguous(), self.soft_state, self.rigid_config
    )
    if self.mochi_config.use_sleeping:
        i_n = len(self._entities) + entity.idx_in_solver
        self._wake_nodes(self._nodes_idx[i_n : i_n + 1], self._nodes_idx, envs_idx)

set_soft_entity_contact_params

set_soft_entity_contact_params(entity, **params)
Source code in eden/engine/solvers/mochi/mochi_solver.py
@mutates(StateChange.MODEL)
def set_soft_entity_contact_params(self, entity, **params):
    material = entity.material
    keys = (
        "penalty_coefficient",
        "friction",
        "penalty_smoothing_half_distance",
        "penalty_threshold",
        "friction_falloff_vel",
        "viscous_friction",
        "normal_viscous_damping",
    )
    values = []
    for key in keys:
        value = params.get(key)
        if value is None:
            value = getattr(material, key)
        else:
            setattr(material, key, value)
        values.append(float(value))
    kernel_soft_set_entity_contact_params(
        entity.idx_in_solver, self._pc_hash_cell, np.array(values, dtype=gs.np_float), self.soft_info
    )
    self._wake_nodes(self._nodes_idx, self._nodes_idx, self._scene._envs_idx)
    self._is_contacts_recorded = False

MochiEntity

Bases: RigidEntity

Rigid entity simulated by the MochiSolver.

Loading, kinematics, pose/velocity access and joint control are shared with RigidEntity. Contact is the solver's smooth penalty model, so the contact readback exposes the sample-based contact points; the mass-matrix and constraint-solver specific accessors of the rigid solver are unavailable.

Methods:

get_contacts

get_contacts(with_entity=None, exclude_self_contact=False, is_padded=False)

Returns the contact points computed at the end of the most recent scene.step(), filtered to those involving this entity (and with_entity if given).

A contact point is a sample of the collision surface of side A pressed into the distance field of side B (a rigid geom, or a deformable body). The returned dict has the keys 'entity_a', 'entity_b' (scene entity indices), 'link_a', 'link_b', 'geom_a', 'geom_b' (-1 for a deformable side), 'verts_a', 'bary_a', 'verts_b', 'bary_b' (entity-local vertices and weights a deformable side is spread over, -1 padded), 'position', 'normal' (unit, pointing away from B), 'distance' (signed, negative when penetrating), 'force_a' (force on A), 'force_b' (= -force_a), 'weight' (surface measure the sample stands for) and, for a parallelized scene or with is_padded, 'valid_mask'. Shapes are (n_envs, n_contacts, ...) for a parallelized scene and (n_contacts, ...) otherwise; an unparallelized scene without is_padded keeps only the matching contacts.

Source code in eden/engine/solvers/mochi/mochi_entity.py
def get_contacts(self, with_entity=None, exclude_self_contact=False, is_padded=False):
    """
    Returns the contact points computed at the end of the most recent `scene.step()`, filtered to those involving
    this entity (and `with_entity` if given).

    A contact point is a sample of the collision surface of side A pressed into the distance field of side B
    (a rigid geom, or a deformable body). The returned dict has the keys 'entity_a', 'entity_b' (scene entity
    indices), 'link_a', 'link_b', 'geom_a', 'geom_b' (-1 for a deformable side), 'verts_a', 'bary_a', 'verts_b',
    'bary_b' (entity-local vertices and weights a deformable side is spread over, -1 padded), 'position', 'normal'
    (unit, pointing away from B), 'distance' (signed, negative when penetrating), 'force_a' (force on A),
    'force_b' (= -force_a), 'weight' (surface measure the sample stands for) and, for a parallelized scene or
    with `is_padded`, 'valid_mask'. Shapes are (n_envs, n_contacts, ...) for a parallelized scene and
    (n_contacts, ...) otherwise; an unparallelized scene without `is_padded` keeps only the matching contacts.
    """
    return filter_entity_contacts(self._solver, self, with_entity, exclude_self_contact, is_padded)
get_links_net_contact_force(envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def get_links_net_contact_force(self, envs_idx=None):
    self._solver._record_contacts()
    return super().get_links_net_contact_force(envs_idx)

set_contact_params

set_contact_params(*, penalty_coefficient=None, friction=None, penalty_smoothing_half_distance=None, penalty_threshold=None, friction_falloff_vel=None, viscous_friction=None, normal_viscous_damping=None, links_idx_local=None)

Update the contact parameters of the collision geoms of the given links (all links by default). See eden.engine.solvers.mochi.materials.Rigid for the meaning of each parameter.

Source code in eden/engine/solvers/mochi/mochi_entity.py
def set_contact_params(
    self,
    *,
    penalty_coefficient=None,
    friction=None,
    penalty_smoothing_half_distance=None,
    penalty_threshold=None,
    friction_falloff_vel=None,
    viscous_friction=None,
    normal_viscous_damping=None,
    links_idx_local=None,
):
    """
    Update the contact parameters of the collision geoms of the given links (all links by default). See
    `eden.engine.solvers.mochi.materials.Rigid` for the meaning of each parameter.
    """
    links_idx = self._get_global_idx(links_idx_local, self.n_links, self._link_start, unsafe=True)
    self._solver.set_links_contact_params(
        links_idx,
        penalty_coefficient=penalty_coefficient,
        friction=friction,
        penalty_smoothing_half_distance=penalty_smoothing_half_distance,
        penalty_threshold=penalty_threshold,
        friction_falloff_vel=friction_falloff_vel,
        viscous_friction=viscous_friction,
        normal_viscous_damping=normal_viscous_damping,
    )

set_friction

set_friction(friction)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def set_friction(self, friction):
    self.set_contact_params(friction=friction)

set_has_gravity

set_has_gravity(has_gravity)

Enable or disable gravity on every link of the entity.

Source code in eden/engine/solvers/mochi/mochi_entity.py
def set_has_gravity(self, has_gravity):
    """Enable or disable gravity on every link of the entity."""
    links_idx = self._get_global_idx(None, self.n_links, self._link_start, unsafe=True)
    self._solver.set_links_has_gravity(links_idx, has_gravity)

get_dofs_force

get_dofs_force(dofs_idx_local=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def get_dofs_force(self, dofs_idx_local=None, envs_idx=None):
    self._raise_unsupported("get_dofs_force")

get_mass_mat

get_mass_mat(envs_idx=None, decompose=False)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def get_mass_mat(self, envs_idx=None, decompose=False):
    self._raise_unsupported("get_mass_mat")

set_friction_ratio

set_friction_ratio(friction_ratio, links_idx_local=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def set_friction_ratio(self, friction_ratio, links_idx_local=None, envs_idx=None):
    self._raise_unsupported("set_friction_ratio")

set_friction_torsional

set_friction_torsional(friction_torsional)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def set_friction_torsional(self, friction_torsional):
    self._raise_unsupported("set_friction_torsional")

set_friction_rolling

set_friction_rolling(friction_rolling)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def set_friction_rolling(self, friction_rolling):
    self._raise_unsupported("set_friction_rolling")

set_mass_shift

set_mass_shift(mass_shift, links_idx_local=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def set_mass_shift(self, mass_shift, links_idx_local=None, envs_idx=None):
    self._raise_unsupported("set_mass_shift")

set_COM_shift

set_COM_shift(com_shift, links_idx_local=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def set_COM_shift(self, com_shift, links_idx_local=None, envs_idx=None):
    self._raise_unsupported("set_COM_shift")
apply_links_external_wrench(*args, **kwargs)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def apply_links_external_wrench(self, *args, **kwargs):
    self._raise_unsupported("apply_links_external_wrench")

detect_collision

detect_collision(env_idx=0)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def detect_collision(self, env_idx=0):
    self._raise_unsupported("detect_collision")

get_kinetic_energy

get_kinetic_energy(envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def get_kinetic_energy(self, envs_idx=None):
    self._raise_unsupported("get_kinetic_energy")

get_potential_energy

get_potential_energy(envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def get_potential_energy(self, envs_idx=None):
    self._raise_unsupported("get_potential_energy")

get_total_energy

get_total_energy(envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def get_total_energy(self, envs_idx=None):
    self._raise_unsupported("get_total_energy")
get_links_acc(links_idx_local=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def get_links_acc(self, links_idx_local=None, envs_idx=None):
    self._raise_unsupported("get_links_acc")
get_links_acc_ang(links_idx_local=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def get_links_acc_ang(self, links_idx_local=None, envs_idx=None):
    self._raise_unsupported("get_links_acc_ang")

get_AABB

get_AABB(envs_idx=None, *, allow_fast_approx: bool = False)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def get_AABB(self, envs_idx=None, *, allow_fast_approx: bool = False):
    self._raise_unsupported("get_AABB")

get_verts

get_verts()
Source code in eden/engine/solvers/mochi/mochi_entity.py
def get_verts(self):
    self._raise_unsupported("get_verts")

set_geoms_friction_ratio

set_geoms_friction_ratio(friction_ratio, geoms_idx_local=None, envs_idx=None)
Source code in eden/engine/solvers/mochi/mochi_entity.py
def set_geoms_friction_ratio(self, friction_ratio, geoms_idx_local=None, envs_idx=None):
    self._raise_unsupported("set_geoms_friction_ratio")

MochiSoftEntity

MochiSoftEntity(scene, solver, material, morph, surface, idx, idx_in_solver, v_start, vvert_start, vface_start, name=None)

Bases: Entity

Deformable body simulated by the MochiSolver: a tetrahedral mesh (materials.Elastic), a thin shell made of the surface triangles of the morph (materials.Shell) or a discrete elastic rod along the polyline of a Rod morph (materials.Rod), whose vertex positions are unknowns of the implicit solve.

The solid simulation mesh is tetrahedralized from the morph surface (tetgen options on the morph), or read directly from a tetgen '.node' / '.ele' file pair or a legacy ASCII '.vtk' file given as a gs.morphs.Mesh. The boundary triangles (all triangles of a shell) carry the contact samples and the render mesh; a rod carries its samples on the centerline and renders as a tube.

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def __init__(
    self,
    scene,
    solver,
    material,
    morph,
    surface,
    idx,
    idx_in_solver,
    v_start,
    vvert_start,
    vface_start,
    name=None,
):
    self._is_shell = isinstance(material, mochi_materials.Shell)
    self._is_rod = isinstance(material, mochi_materials.Rod)
    if self._is_rod != isinstance(morph, Rod):
        gs.raise_exception(
            "A `Rod` morph (eden.engine.solvers.mochi.morphs) requires a `Rod` material "
            "(eden.engine.solvers.mochi.materials), and conversely."
        )
    super().__init__(idx, scene, morph, solver, material, surface, name=name)
    self._idx_in_solver = idx_in_solver
    self._v_start = v_start
    self._vvert_start = vvert_start
    self._vface_start = vface_start
    self._attachments = []
    self._contact_skin = None

    self.sample()

    # Boundary triangles (outward winding), all triangles of a shell, none for a rod (its contact samples lie on
    # the centerline).
    if self._is_rod:
        self._surface_tri_np = np.zeros((0, 3), dtype=gs.np_int)
    elif self._is_shell:
        self._surface_tri_np = self.elems
    else:
        self._surface_tri_np = self._boundary_triangles(self.elems)

Attributes

attachments property

attachments

Rigid-link attachment registrations of this entity.

idx_in_solver property

idx_in_solver

is_shell property

is_shell

is_rod property

is_rod

n_rod_stencils property

n_rod_stencils

Interior nodes of a rod (all nodes of a closed loop).

n_vertices property

n_vertices

n_elements property

n_elements

n_surfaces property

n_surfaces

surface_triangles property

surface_triangles

contact_skin property

contact_skin

Rest vertices and triangles of the contact skin, or None when the samples lie on the boundary triangles.

n_dofs property

n_dofs

v_start property

v_start

v_end property

v_end

vgeoms property

vgeoms

n_vgeoms property

n_vgeoms

n_vverts property

n_vverts

n_vfaces property

n_vfaces

vvert_start property

vvert_start

vface_start property

vface_start

volume property

volume

area property

area

Total area of the surface triangles.

length property

length

Total length of the rod centerline.

mass property

mass

Methods:

sample

sample()

Build the render geoms and the simulation mesh (tetrahedra, shell triangles or rod segments) of the morph.

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def sample(self):
    """Build the render geoms and the simulation mesh (tetrahedra, shell triangles or rod segments) of the morph."""
    morph = self._morph
    if self._is_rod:
        self._sample_rod()
        return
    if isinstance(morph, gs.morphs.Mesh) and morph.is_format((TET_NODE_FORMAT, TET_VTK_FORMAT)):
        if self._is_shell:
            gs.raise_exception("Shells are surface meshes: a tetrahedral mesh file cannot be used for a shell.")
        load_files = load_tet_files if morph.is_format(TET_NODE_FORMAT) else load_vtk_tet_files
        verts, elems = load_files(morph.file)
        # The morph rotation is applied by `instantiate` (about the vertex centroid, like every other morph).
        verts = verts * np.asarray(morph.scale, dtype=np.float64) + np.asarray(morph.pos, dtype=np.float64)
        self.instantiate(verts, elems)
        surface_tri = self._boundary_triangles(self.elems)
        vmesh = gs.Mesh.from_trimesh(
            trimesh.Trimesh(vertices=self.init_positions, faces=surface_tri, process=False),
            surface=self._surface,
        )
        self._vgeoms = gs.List(
            [
                MochiVisGeom(
                    entity=self,
                    vvert_start=self._vvert_start,
                    vface_start=self._vface_start,
                    vmesh=vmesh,
                    sim_verts_idx=np.arange(self.n_vertices, dtype=gs.np_int),
                )
            ]
        )
        return

    meshes = gs.Mesh.from_morph_surface(morph, self._surface)
    surface_verts, surface_faces, verts_maps = mu.merge_submeshes(
        [mesh.verts for mesh in meshes], [mesh.faces for mesh in meshes]
    )
    self._vgeoms = gs.List()
    vvert_start, vface_start = self._vvert_start, self._vface_start
    for mesh, verts_idx in zip(meshes, verts_maps):
        self._vgeoms.append(
            MochiVisGeom(
                entity=self, vvert_start=vvert_start, vface_start=vface_start, vmesh=mesh, sim_verts_idx=verts_idx
            )
        )
        vvert_start += len(mesh.verts)
        vface_start += len(mesh.faces)

    if self._is_shell:
        # The welded surface triangles are the shell elements.
        self.instantiate(surface_verts + morph.pos, surface_faces)
        return

    # File meshes are tetrahedralized untranslated so that the result and its cache are shared across placements;
    # primitives keep the position baked in, as the rest state is sensitive to the exact refinement.
    is_mesh_morph = isinstance(morph, gs.morphs.Mesh)
    if not is_mesh_morph:
        surface_verts = surface_verts + morph.pos
    surface_trimesh = trimesh.Trimesh(vertices=surface_verts, faces=surface_faces, process=False)
    verts, elems = eu.mesh_to_elements(surface_trimesh, tet_cfg=mu.generate_tetgen_config_from_morph(morph))
    if is_mesh_morph:
        verts = verts + morph.pos
    self.instantiate(verts, elems)

instantiate

instantiate(verts, elems)

Set the rest vertex positions (with the morph orientation applied about their centroid and the morph pose offset composed on) and the elements (tetrahedra, shell triangles or rod segments).

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def instantiate(self, verts, elems):
    """Set the rest vertex positions (with the morph orientation applied about their centroid and the morph pose
    offset composed on) and the elements (tetrahedra, shell triangles or rod segments).
    """
    verts = np.asarray(verts, dtype=gs.np_float)
    elems = np.asarray(elems, dtype=gs.np_int)
    if len(verts) == 0 or len(elems) == 0:
        gs.raise_exception("Entity has no vertices or no elements (tetrahedra, shell triangles or rod segments).")
    # A vertex no element references carries no mass and no stiffness, so the dense island solve would be singular.
    if len(np.unique(elems)) < len(verts):
        gs.raise_exception("Every mesh vertex must belong to at least one element.")
    self._instantiate_verts_COM = verts.mean(axis=0)
    morph_quat = np.array(self._morph.quat, dtype=gs.np_float)
    init_quat = gu.transform_quat_by_quat(np.array(self._morph.offset_quat, dtype=gs.np_float), morph_quat)
    R = gu.quat_to_R(init_quat)
    verts_COM = verts.mean(axis=0)
    init_positions = (verts - verts_COM) @ R.T + verts_COM
    init_positions = init_positions + gu.transform_by_quat(
        np.array(self._morph.offset_pos, dtype=gs.np_float), morph_quat
    )
    if elems.shape[1] == 4:
        # Positive orientation of every tetrahedron (node 3 as origin).
        D = np.stack([init_positions[elems[:, k]] - init_positions[elems[:, 3]] for k in range(3)], axis=-1)
        is_inverted = np.linalg.det(D) < 0.0
        elems = elems.copy()
        elems[is_inverted, 1], elems[is_inverted, 2] = elems[is_inverted, 2], elems[is_inverted, 1]
    self.init_positions = init_positions.astype(gs.np_float)
    self.elems = elems

get_state

get_state()
Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def get_state(self):
    state = MochiSoftEntityState(self, self._sim.cur_step_global)
    self._solver.get_soft_entity_state(self, state.pos, state.vel)
    return state
attach_to_link(link, verts_idx, stiffness=1000000.0, damping=0.0)

Rigidly attach vertices of this deformable body to a rigid link.

Each attached vertex is tied by a stiff penalty (inside the implicit solve) to the point of the link it coincides with when the scene is built, so place the body at its mounted pose. Forces act on both sides and the coupled bodies form one simulation island. Must be called before the scene is built.

PARAMETER DESCRIPTION
link

A link of a rigid or articulated entity simulated by the MochiSolver.

TYPE: RigidLink

verts_idx

Indices of the vertices of this entity to attach.

TYPE: array_like

stiffness

Penalty stiffness k of the energy 1/2 k |c|^2 per vertex, in N/m. Defaults to 1e6 (mochi's constraint default).

TYPE: float DEFAULT: 1000000.0

damping

Damping d on the violation rate, adding 1/2 (d / dt) |c - c_stage_start|^2 per stage. Defaults to 0.

TYPE: float DEFAULT: 0.0

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def attach_to_link(self, link, verts_idx, stiffness=1e6, damping=0.0):
    """Rigidly attach vertices of this deformable body to a rigid link.

    Each attached vertex is tied by a stiff penalty (inside the implicit solve) to the point of the link it
    coincides with when the scene is built, so place the body at its mounted pose. Forces act on both sides and
    the coupled bodies form one simulation island. Must be called before the scene is built.

    Parameters
    ----------
    link : RigidLink
        A link of a rigid or articulated entity simulated by the MochiSolver.
    verts_idx : array_like
        Indices of the vertices of this entity to attach.
    stiffness : float, optional
        Penalty stiffness k of the energy 1/2 k |c|^2 per vertex, in N/m. Defaults to 1e6 (mochi's constraint
        default).
    damping : float, optional
        Damping d on the violation rate, adding 1/2 (d / dt) |c - c_stage_start|^2 per stage. Defaults to 0.
    """
    if self.is_built:
        gs.raise_exception("Attachments must be registered before the scene is built.")
    verts_idx = np.unique(np.asarray(verts_idx, dtype=gs.np_int).reshape((-1,)))
    if len(verts_idx) == 0 or verts_idx[0] < 0 or verts_idx[-1] >= self.n_vertices:
        gs.raise_exception(f"'verts_idx' must be non-empty vertex indices within [0, {self.n_vertices}).")
    if stiffness <= 0.0 or damping < 0.0:
        gs.raise_exception("'stiffness' must be positive and 'damping' non-negative.")
    self._attachments.append(
        {"link": link, "verts_idx": verts_idx, "stiffness": float(stiffness), "damping": float(damping)}
    )

set_visual_mesh

set_visual_mesh(file, pos=(0.0, 0.0, 0.0), euler=(0.0, 0.0, 0.0), scale=1.0)

Replace the render mesh of this solid by a detailed visual mesh skinned by the simulation tetrahedra.

Every vertex of the visual mesh is embedded in the tetrahedron whose barycentric coordinates it takes (points outside the coarse simulation mesh extrapolate linearly from the nearest one), so the visual surface follows the deformation of the simulation mesh - the usual pairing of a fine render mesh with a coarse collision mesh. pos, euler (degrees) and scale place the visual mesh in the frame of the simulation mesh file; the entity's morph transform then applies to both. Must be called before the scene is built.

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def set_visual_mesh(self, file, pos=(0.0, 0.0, 0.0), euler=(0.0, 0.0, 0.0), scale=1.0):
    """Replace the render mesh of this solid by a detailed visual mesh skinned by the simulation tetrahedra.

    Every vertex of the visual mesh is embedded in the tetrahedron whose barycentric coordinates it takes (points
    outside the coarse simulation mesh extrapolate linearly from the nearest one), so the visual surface follows
    the deformation of the simulation mesh - the usual pairing of a fine render mesh with a coarse collision
    mesh. `pos`, `euler` (degrees) and `scale` place the visual mesh in the frame of the simulation mesh file;
    the entity's morph transform then applies to both. Must be called before the scene is built.
    """
    visual_trimesh = self._load_embedded_mesh(file, pos, euler, scale, "visual mesh")
    elems_idx, bary = embed_in_tets(visual_trimesh.vertices, self.init_positions, self.elems)
    vmesh = gs.Mesh.from_trimesh(visual_trimesh, surface=self._surface)
    self._vgeoms = gs.List(
        [
            MochiVisGeom(
                entity=self,
                vvert_start=self._vvert_start,
                vface_start=self._vface_start,
                vmesh=vmesh,
                sim_verts_idx=None,
                elems_idx=elems_idx,
                bary=bary,
            )
        ]
    )

set_contact_skin

set_contact_skin(file, pos=(0.0, 0.0, 0.0), euler=(0.0, 0.0, 0.0), scale=1.0)

Carry the contact samples of this solid on a triangle surface embedded in its simulation tetrahedra.

The samples otherwise lie on the boundary triangles of the tetrahedral mesh, so a coarse mesh contacts through a coarse surface. A detailed skin (fingertip pads, the authored collision surface) places them where the contact actually happens, at the cost of more samples and of contact forces spread over the four vertices of a tetrahedron instead of three. The skin vertices follow the tetrahedra as the visual mesh does (see set_visual_mesh, which takes the same placement arguments). The skin carries the samples only: other bodies still collide against the tetrahedra. Must be called before the scene is built.

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def set_contact_skin(self, file, pos=(0.0, 0.0, 0.0), euler=(0.0, 0.0, 0.0), scale=1.0):
    """Carry the contact samples of this solid on a triangle surface embedded in its simulation tetrahedra.

    The samples otherwise lie on the boundary triangles of the tetrahedral mesh, so a coarse mesh contacts through
    a coarse surface. A detailed skin (fingertip pads, the authored collision surface) places them where the
    contact actually happens, at the cost of more samples and of contact forces spread over the four vertices of
    a tetrahedron instead of three. The skin vertices follow the tetrahedra as the visual mesh does (see
    `set_visual_mesh`, which takes the same placement arguments). The skin carries the samples only: other bodies
    still collide against the tetrahedra. Must be called before the scene is built.
    """
    skin_trimesh = self._load_embedded_mesh(file, pos, euler, scale, "contact skin")
    self._contact_skin = ContactSkin(skin_trimesh.vertices, skin_trimesh.faces)

morph_frame_to_world

morph_frame_to_world(points)

Rest (world) positions of points given in the morph frame, placed as instantiate placed the mesh.

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def morph_frame_to_world(self, points):
    """Rest (world) positions of points given in the morph frame, placed as `instantiate` placed the mesh."""
    R, centre, offset = self._placement()
    return (np.asarray(points, dtype=np.float64) - centre) @ R.T + centre + offset

world_to_morph_frame

world_to_morph_frame(points)

Inverse of morph_frame_to_world: morph-frame positions of rest (world) points.

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def world_to_morph_frame(self, points):
    """Inverse of `morph_frame_to_world`: morph-frame positions of rest (world) points."""
    R, centre, offset = self._placement()
    return (np.asarray(points, dtype=np.float64) - offset - centre) @ R + centre

get_vertices_position

get_vertices_position(envs_idx=None)

World positions of the vertices, shape (n_vertices, 3) or (n_envs, n_vertices, 3).

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def get_vertices_position(self, envs_idx=None):
    """World positions of the vertices, shape (n_vertices, 3) or (n_envs, n_vertices, 3)."""
    return self._solver.get_soft_vertices_position(self, envs_idx)

get_vertices_velocity

get_vertices_velocity(envs_idx=None)

Velocities of the vertices, shape (n_vertices, 3) or (n_envs, n_vertices, 3).

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def get_vertices_velocity(self, envs_idx=None):
    """Velocities of the vertices, shape (n_vertices, 3) or (n_envs, n_vertices, 3)."""
    return self._solver.get_soft_vertices_velocity(self, envs_idx)

get_vertices_contact_force

get_vertices_contact_force(envs_idx=None)

Net contact force applied to each vertex, shape (n_vertices, 3) or (n_envs, n_vertices, 3).

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def get_vertices_contact_force(self, envs_idx=None):
    """Net contact force applied to each vertex, shape (n_vertices, 3) or (n_envs, n_vertices, 3)."""
    return self._solver.get_soft_vertices_contact_force(self, envs_idx)

get_contacts

get_contacts(with_entity=None, exclude_self_contact=False, is_padded=False)

Contact points of the most recent scene.step() involving this body (and with_entity if given), with the keys documented in MochiEntity.get_contacts. On the side of this body a contact point carries the vertices of the surface element (verts_a / verts_b, local to this body, -1 padded) and the weights spreading the force over them (bary_a / bary_b).

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def get_contacts(self, with_entity=None, exclude_self_contact=False, is_padded=False):
    """
    Contact points of the most recent `scene.step()` involving this body (and `with_entity` if given), with the
    keys documented in `MochiEntity.get_contacts`. On the side of this body a contact point carries the vertices of
    the surface element (`verts_a` / `verts_b`, local to this body, -1 padded) and the weights spreading the force
    over them (`bary_a` / `bary_b`).
    """
    return filter_entity_contacts(self._solver, self, with_entity, exclude_self_contact, is_padded)

set_position

set_position(pos, envs_idx=None)

Move the body: a (3,) or (n_envs, 3) offset of the rest positions, or full vertex positions of shape (n_vertices, 3) or (n_envs, n_vertices, 3).

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def set_position(self, pos, envs_idx=None):
    """Move the body: a (3,) or (n_envs, 3) offset of the rest positions, or full vertex positions of shape
    (n_vertices, 3) or (n_envs, n_vertices, 3).
    """
    pos = torch.as_tensor(tensor_to_array(pos), dtype=gs.tc_float, device=gs.device)
    if pos.shape[-2:] != (self.n_vertices, 3):
        pos = torch.as_tensor(self.init_positions, dtype=gs.tc_float, device=gs.device) + pos[..., None, :]
    self._solver.set_soft_vertices_position(self, pos, envs_idx)

set_velocity

set_velocity(vel, envs_idx=None)

Set the vertex velocities: one (3,) or (n_envs, 3) velocity for all vertices, or per vertex of shape (n_vertices, 3) or (n_envs, n_vertices, 3).

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def set_velocity(self, vel, envs_idx=None):
    """Set the vertex velocities: one (3,) or (n_envs, 3) velocity for all vertices, or per vertex of shape
    (n_vertices, 3) or (n_envs, n_vertices, 3).
    """
    vel = torch.as_tensor(tensor_to_array(vel), dtype=gs.tc_float, device=gs.device)
    if vel.shape[-2:] != (self.n_vertices, 3):
        vel = vel[..., None, :].expand(*vel.shape[:-1], self.n_vertices, 3)
    self._solver.set_soft_vertices_velocity(self, vel, envs_idx)

set_vertices_fixed

set_vertices_fixed(verts_idx_local, is_fixed=True, envs_idx=None)

Fix the given vertices at their current position (Dirichlet condition), or release them.

verts_idx_local is one set for every environment, shape (n,), or one set per environment, shape (n_envs, n), whose -1 entries are skipped.

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def set_vertices_fixed(self, verts_idx_local, is_fixed=True, envs_idx=None):
    """Fix the given vertices at their current position (Dirichlet condition), or release them.

    `verts_idx_local` is one set for every environment, shape (n,), or one set per environment, shape (n_envs, n),
    whose -1 entries are skipped.
    """
    self._solver.set_soft_vertices_fixed(self, self._sanitize_vertices_idx(verts_idx_local), is_fixed, envs_idx)

set_vertices_target

set_vertices_target(verts_idx_local, pos, envs_idx=None)

Prescribe the positions the given vertices must reach at the end of the next step (moving Dirichlet condition); the vertices become fixed until released with set_vertices_fixed(..., is_fixed=False).

verts_idx_local is one set for every environment, shape (n,), with pos of shape (n, 3) or (n_envs, n, 3), or one set per environment, shape (n_envs, n) whose -1 entries are skipped, with pos of shape (n_envs, n, 3). This is how a gripper carries whichever vertices it closed on in each environment.

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def set_vertices_target(self, verts_idx_local, pos, envs_idx=None):
    """Prescribe the positions the given vertices must reach at the end of the next step (moving Dirichlet
    condition); the vertices become fixed until released with `set_vertices_fixed(..., is_fixed=False)`.

    `verts_idx_local` is one set for every environment, shape (n,), with `pos` of shape (n, 3) or (n_envs, n, 3),
    or one set per environment, shape (n_envs, n) whose -1 entries are skipped, with `pos` of shape (n_envs, n, 3).
    This is how a gripper carries whichever vertices it closed on in each environment.
    """
    self._solver.set_soft_vertices_target(self, self._sanitize_vertices_idx(verts_idx_local), pos, envs_idx)

set_contact_params

set_contact_params(*, penalty_coefficient=None, friction=None, penalty_smoothing_half_distance=None, penalty_threshold=None, friction_falloff_vel=None, viscous_friction=None, normal_viscous_damping=None)

Update the contact parameters of this body (see eden.engine.solvers.mochi.materials.Elastic).

Source code in eden/engine/solvers/mochi/mochi_soft_entity.py
def set_contact_params(
    self,
    *,
    penalty_coefficient=None,
    friction=None,
    penalty_smoothing_half_distance=None,
    penalty_threshold=None,
    friction_falloff_vel=None,
    viscous_friction=None,
    normal_viscous_damping=None,
):
    """Update the contact parameters of this body (see `eden.engine.solvers.mochi.materials.Elastic`)."""
    self._solver.set_soft_entity_contact_params(
        self,
        penalty_coefficient=penalty_coefficient,
        friction=friction,
        penalty_smoothing_half_distance=penalty_smoothing_half_distance,
        penalty_threshold=penalty_threshold,
        friction_falloff_vel=friction_falloff_vel,
        viscous_friction=viscous_friction,
        normal_viscous_damping=normal_viscous_damping,
    )

Dynamic states queried from the MochiSolver and its deformable entities.

Classes

MochiSoftEntityState

MochiSoftEntityState(entity, s_global)

Dynamic state queried from a MochiSoftEntity: vertex positions and velocities, shape (n_envs, n_vertices, 3).

Source code in eden/engine/solvers/mochi/states.py
def __init__(self, entity, s_global):
    self._entity = entity
    self._s_global = s_global
    args = {"dtype": gs.tc_float, "device": gs.device}
    self.pos = torch.zeros((entity.sim._B, entity.n_vertices, 3), **args)
    self.vel = torch.zeros((entity.sim._B, entity.n_vertices, 3), **args)

Attributes

pos instance-attribute
pos = torch.zeros((entity.sim._B, entity.n_vertices, 3), **args)
vel instance-attribute
vel = torch.zeros((entity.sim._B, entity.n_vertices, 3), **args)
entity property
entity
s_global property
s_global

Methods:

MochiSolverState

MochiSolverState(solver, s_global)

Dynamic state queried from a MochiSolver: generalized coordinates and velocities, link poses, and the multistep history the time integration extrapolates from.

Source code in eden/engine/solvers/mochi/states.py
def __init__(self, solver, s_global):
    self.scene = solver.scene
    self._s_global = s_global

    _B = solver._B
    args = {"dtype": gs.tc_float, "device": gs.device}
    self.qpos = torch.zeros((_B, solver.n_qs), **args)
    self.dofs_vel = torch.zeros((_B, solver.n_dofs), **args)
    self.links_pos = torch.zeros((_B, solver.n_links, 3), **args)
    self.links_quat = torch.zeros((_B, solver.n_links, 4), **args)
    self.qpos_prev = torch.zeros((_B, 2, solver.n_qs), **args)
    self.dofs_vel_prev = torch.zeros((_B, 2, solver.n_dofs), **args)
    self.links_vel = torch.zeros((_B, solver.n_links, 3), **args)
    self.links_ang = torch.zeros((_B, solver.n_links, 3), **args)
    self.links_vsym = torch.zeros((_B, solver.n_links, 3, 3), **args)
    self.links_vel_prev = torch.zeros((_B, 2, solver.n_links, 3), **args)
    self.links_ang_prev = torch.zeros((_B, 2, solver.n_links, 3), **args)
    self.links_vsym_prev = torch.zeros((_B, 2, solver.n_links, 3, 3), **args)
    self.n_hist = torch.zeros((_B,), dtype=gs.tc_int, device=gs.device)
    # Deformable bodies: vertex positions and velocities with their multistep history.
    n_soft_verts = solver.n_soft_verts
    self.soft_pos = torch.zeros((_B, n_soft_verts, 3), **args)
    self.soft_vel = torch.zeros((_B, n_soft_verts, 3), **args)
    self.soft_pos_prev = torch.zeros((_B, 2, n_soft_verts, 3), **args)
    self.soft_vel_prev = torch.zeros((_B, 2, n_soft_verts, 3), **args)

Attributes

scene instance-attribute
scene = solver.scene
qpos instance-attribute
qpos = torch.zeros((_B, solver.n_qs), **args)
dofs_vel instance-attribute
dofs_vel = torch.zeros((_B, solver.n_dofs), **args)
links_pos = torch.zeros((_B, solver.n_links, 3), **args)
links_quat = torch.zeros((_B, solver.n_links, 4), **args)
qpos_prev instance-attribute
qpos_prev = torch.zeros((_B, 2, solver.n_qs), **args)
dofs_vel_prev instance-attribute
dofs_vel_prev = torch.zeros((_B, 2, solver.n_dofs), **args)
links_vel = torch.zeros((_B, solver.n_links, 3), **args)
links_ang = torch.zeros((_B, solver.n_links, 3), **args)
links_vsym = torch.zeros((_B, solver.n_links, 3, 3), **args)
links_vel_prev = torch.zeros((_B, 2, solver.n_links, 3), **args)
links_ang_prev = torch.zeros((_B, 2, solver.n_links, 3), **args)
links_vsym_prev = torch.zeros((_B, 2, solver.n_links, 3, 3), **args)
n_hist instance-attribute
n_hist = torch.zeros((_B,), dtype=gs.tc_int, device=gs.device)
soft_pos instance-attribute
soft_pos = torch.zeros((_B, n_soft_verts, 3), **args)
soft_vel instance-attribute
soft_vel = torch.zeros((_B, n_soft_verts, 3), **args)
soft_pos_prev instance-attribute
soft_pos_prev = torch.zeros((_B, 2, n_soft_verts, 3), **args)
soft_vel_prev instance-attribute
soft_vel_prev = torch.zeros((_B, 2, n_soft_verts, 3), **args)
s_global property
s_global

Methods:

Error codes

The flags the solver's kernels set in the per-environment errno field.

Error flags the MochiSolver's kernels raise in the per-environment errno field of its kinematic tree.

Classes

MochiErrorCode

Bases: IntEnum

Bits above every one of genesis.utils.array_class.ErrorCode, so the two never collide in one field.

Solver.register_errors refuses a bit the base solver reserves (INVALID_GRAD_NAN), so a Genesis bump that adds an ErrorCode must move these above it.

Attributes

OVERFLOW_MOCHI_CONTACT_PAIRS class-attribute instance-attribute
OVERFLOW_MOCHI_CONTACT_PAIRS = 1024
OVERFLOW_MOCHI_CONTACTS class-attribute instance-attribute
OVERFLOW_MOCHI_CONTACTS = 2048
MOCHI_DIVERGED class-attribute instance-attribute
MOCHI_DIVERGED = 4096

See also