How FrictionDRBamActuator Works for Domain Randomization in microduck_rl

The FrictionDRBamActuator injects a per-environment friction_scale scalar into the BAM actuator's internal friction budget, enabling non-accumulating friction randomization that standard MuJoCo dof_frictionloss cannot achieve.

The microduck_rl repository implements a custom voltage-controlled actuator model for the real-world XL330 servo. Because this BAM (Bang-Bang with Anti-Windup Model) actuator computes friction internally rather than relying on MuJoCo's native physics, conventional domain randomization through dr.dof_frictionloss has no effect. The FrictionDRBamActuator wrapper solves this gap by exposing a tunable, per-environment friction multiplier that task configurations can randomize on every episode reset.

Why Standard MuJoCo Friction Randomization Fails

The BAM actuator in src/mjlab_microduck/actuator/bam_actuator.py overrides torque computation with a custom friction model incorporating Coulomb, viscous, Stribeck, and load-dependent terms. Since MuJoCo's dof_frictionloss field is never read during BamActuator._compute_friction_budget(), the simulator's built-in friction randomization (dr.dof_frictionloss) is silently ignored. This architectural detail forces any domain randomization strategy to operate at the actuator level rather than the physics engine level.

Core Architecture of FrictionDRBamActuator

The implementation resides in src/mjlab_microduck/actuator/friction_dr_bam.py and extends the base BamActuator with three key mechanisms:

Per-Environment Friction Scale Storage

During initialization, the actuator allocates a friction_scale tensor matching the environment count:


# friction_dr_bam.py, lines 31-36

def __init__(self, cfg: FrictionDRBamActuatorCfg, *args, **kwargs):
    super().__init__(cfg, *args, **kwargs)
    self.friction_scale = torch.ones_like(self.kp_scale)  # line 34

    self._friction_scale_default = self.friction_scale.clone()

This tensor maintains independent friction multipliers for each parallel environment, enabling batched randomization without cross-contamination.

Friction Budget Multiplication

Every simulation step, the computed friction budget is scaled by the current friction_scale:


# friction_dr_bam.py, lines 43-48

def _compute_friction_budget(self, pos, vel, load):
    base = super()._compute_friction_budget(pos, vel, load)
    fs = self.friction_scale
    return base if fs is None else base * fs  # line 47

The multiplication occurs after the base friction model computes nominal values, preserving all physical structure (velocity-dependence, load-dependence) while adjusting magnitude.

Reset-Safe Scale Management

Two methods enable non-accumulating randomization:


# friction_dr_bam.py, lines 50-56

def reset_friction_scale(self, env_ids):
    """Restore nominal friction (scale = 1.0) for specified environments."""
    self.friction_scale[env_ids] = self._friction_scale_default[env_ids]

def set_friction_scale(self, env_ids, values):
    """Apply new per-environment friction scalars."""
    self.friction_scale[env_ids] = values

The explicit reset-before-set pattern prevents randomization from accumulating across episodes—a failure mode documented in the project's AGENTS.md where unbounded drift caused policy divergence.

The Randomization Event: randomize_bam_friction

Task implementations trigger friction randomization through a registered event in src/mjlab_microduck/tasks/mdp.py:

Event Registration and Model Fields


# mdp.py, lines 3197-3202

@requires_model_fields("dof_frictionloss", "dof_damping")
def randomize_bam_friction(env, env_ids: torch.Tensor, scale_range: tuple, asset_cfg=_DEFAULT_ASSET_CFG):
    """Sample per-env friction scalars for all FrictionDRBamActuator instances."""

The @requires_model_fields decorator ensures the MJCF includes dof_frictionloss and dof_damping fields, even though the BAM actuator overrides their values. This maintains compatibility with inspection tools and potential hybrid actuator configurations.

Non-Accumulating Sampling Logic


# mdp.py, lines 3212-3220 (excerpt with clarification)

for act in asset.actuators:
    if isinstance(act, FrictionDRBamActuator):
        act.reset_friction_scale(env_ids)  # line 39 equivalent: restore 1.0

        lo, hi = scale_range
        samples = torch.rand(len(env_ids), 1, device=env.device) * (hi - lo) + lo
        act.set_friction_scale(env_ids, samples)  # apply fresh randomization

The strict ordering—reset to nominal, then sample new values—guarantees that friction variation remains bounded within the configured scale_range regardless of training duration.

Task Configuration Integration

Training tasks integrate the randomization event through their environment configuration files. The microduck_velocity_env_cfg.py demonstrates standard usage:


# microduck_velocity_env_cfg.py, line 477

@configclass
class EventCfg:
    randomize_friction = EventTerm(
        func=microduck_mdp.randomize_bam_friction,
        mode="reset",           # execute once per episode reset

        params={
            "scale_range": (0.8, 1.2),  # ±20% friction variation

            "asset_cfg": SceneEntityCfg("robot"),
        },
    )

Multiple task variants use different ranges. The microduck_roller_crouch_env_cfg.py (line 297) applies the same event with potentially distinct parameters, allowing per-task tuning of friction uncertainty without code changes.

Sim-to-Real Transfer Benefits

The FrictionDRBamActuator randomization strategy targets specific hardware uncertainty sources:

  • Coulomb friction variation from temperature-dependent grease viscosity in the XL330 gearbox
  • Stribeck effect uncertainty from seal condition and manufacturing tolerance
  • Load-dependent friction from belt tension variations in the microduck drivetrain

By scaling the complete internal friction budget rather than perturbing individual parameters, the randomization preserves the BAM model's physical correlations—critical for policies that must infer unmeasured friction states from actuator voltage and velocity observations.

Usage Example: Adding Friction DR to a New Task

To enable friction domain randomization in a custom microduck_rl task:


# my_task_env_cfg.py

from mjlab_microduck.actuator.friction_dr_bam import FrictionDRBamActuatorCfg
from mjlab_microduck.tasks import mdp as microduck_mdp
from mjlab.managers import EventTerm, SceneEntityCfg
from omni.isaac.lab.utils import configclass

@configclass
class MyRobotCfg:
    # Replace standard BAM with DR-enabled variant

    actuator_cfg = FrictionDRBamActuatorCfg(
        kp=500.0,
        kd=30.0,
        max_torque=1.0,
        # friction_scale initialized automatically

    )

@configclass
class EventCfg:
    randomize_bam_friction = EventTerm(
        func=microduck_mdp.randomize_bam_friction,
        mode="reset",
        params={"scale_range": (0.7, 1.3)},  # 30% variation for aggressive DR

    )

The FrictionDRBamActuatorCfg is API-compatible with BamActuatorCfg, requiring no changes to robot MJCF definitions or control code.

Summary

  • Problem: BAM actuator computes friction internally, bypassing MuJoCo dof_frictionloss randomization.
  • Solution: FrictionDRBamActuator in friction_dr_bam.py injects a per-environment friction_scale multiplier into the friction budget computation.
  • Safety: The reset_friction_scale → set_friction_scale pattern in randomize_bam_friction (mdp.py) prevents accumulation across episodes.
  • Integration: Tasks register the event via EventTerm with mode="reset" and tunable scale_range parameters.
  • Outcome: Policies train against diverse, bounded friction magnitudes without physical model corruption, improving robustness to real XL330 hardware variation.

Frequently Asked Questions

Why can't I use Isaac Lab's built-in dr.dof_frictionloss for the BAM actuator?

The BAM actuator's _compute_friction_budget method overrides MuJoCo's native friction computation with a custom model based on XL330 datasheet parameters. Since the physics engine's dof_frictionloss value is never read during torque calculation, randomizing it has no effect on simulation behavior. The FrictionDRBamActuator restores randomization capability by operating at the actuator abstraction layer where friction is actually computed.

What happens if I forget to call reset_friction_scale before set_friction_scale?

Without the reset step, friction scales would compound multiplicatively across episodes (e.g., 1.2 × 0.9 × 1.1...), eventually diverging to near-zero or extreme values. This accumulation bug was explicitly documented in project notes as a cause of policy training failures. The randomize_bam_friction event in mdp.py enforces correct usage by always calling reset_friction_scale first.

How do I choose the scale_range parameter for my task?

Start with ±20% (0.8–1.2) based on XL330 friction variation measurements across temperature and load conditions documented in the hardware characterization. For tasks requiring aggressive sim-to-real transfer (e.g., roller crouching with ground contact), expand to ±30% or implement curriculum randomization that widens the range during training. Monitor policy entropy—if entropy collapses, reduce the range; if real-world performance lags simulation, increase it.

Can I combine FrictionDRBamActuator with other domain randomization techniques?

Yes. The actuator composes naturally with Isaac Lab's model randomization (link masses, inertias), observation noise injection, and action delays. The @requires_model_fields decorator only specifies MJCF field requirements without excluding other randomizers. Configure multiple EventTerm entries with distinct priority values to control execution order during resets.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →