How to Perform Joint-Friction Domain Randomization with the BAM Actuator in Microduck RL

Microduck RL implements joint-friction domain randomization by wrapping the BAM actuator in FrictionDRBamActuator, which multiplies the internal friction budget by a per-environment scale factor sampled at the start of each episode, bypassing the ineffective MuJoCo dof_frictionloss field.

The BAM actuator in the pollen-robotics/microduck_rl repository models voltage-controlled XL330 servos with a complex internal friction model. Because this actuator computes its own Coulomb, Stribeck, and load-dependent friction internally, standard MuJoCo domain randomization techniques that write to dof_frictionloss have no effect. Instead, the repository provides a specialized wrapper that injects variability directly into the BAM friction computation.

Why Standard MuJoCo Friction Randomization Fails with BAM

The BAM actuator (Brushless Actuator Model) simulates the dynamics of XL330 servos by calculating friction forces internally rather than relying on MuJoCo's default damping and friction loss parameters. Consequently, the simulation sets the standard MuJoCo field dof_frictionloss to zero for all BAM-controlled joints.

Attempting to use the typical dr.dof_frictionloss domain randomization has no physical effect because the BAM actuator ignores this field during its force computations. To introduce realistic friction variability for sim-to-real transfer, the codebase implements a custom randomization layer that operates within the actuator's own physics model.

The FrictionDRBamActuator Wrapper

The core mechanism for joint-friction domain randomization is the FrictionDRBamActuator class defined in src/mjlab_microduck/actuator/friction_dr_bam.py. This thin wrapper extends the base BAM actuator to accept external scaling factors that modulate its internal friction budget.

Class Implementation

The wrapper maintains a per-environment tensor friction_scale with shape (num_envs, 1) that parallels the existing kp_scale parameter. It exposes two critical methods:

  • reset_friction_scale(env_ids): Resets the scale to the nominal value of 1.0 for the specified environments
  • set_friction_scale(env_ids, value): Writes a new scalar multiplier to the friction scale buffer

During initialization, FrictionDRBamActuator.initialize creates this tensor alongside a default_friction_scale copy to ensure consistent baseline behavior.

Friction Computation Logic

The wrapper overrides the _compute_friction_budget method to inject the randomization:

def _compute_friction_budget(self, physics):
    # Obtain baseline friction (Coulomb + Stribeck + load-dependent)

    base_friction = super()._compute_friction_budget(physics)
    # Apply per-environment scale factor

    return base_friction * self.friction_scale

This multiplication affects all velocity-independent friction components—Coulomb friction, Stribeck effect, and load-dependent terms—ensuring the randomization captures real-world variability in gearbox stiction and bearing resistance.

Episode-Level Randomization Events

The randomization timing is managed through two MDP events in src/mjlab_microduck/tasks/mdp.py that coordinate with the mjlab environment manager.

expand_bam_friction_fields (Startup)

The expand_bam_friction_fields event runs once during environment creation (mode "startup"). Decorated with @requires_model_fields("dof_frictionloss", "dof_damping"), it ensures MuJoCo allocates per-environment copies of these arrays:

@requires_model_fields("dof_frictionloss", "dof_damping")
def expand_bam_friction_fields(env, env_ids):
    # Expands dof_frictionloss and dof_damping to (num_envs, num_dofs)

    # Allows BAM actuator to write per-env friction budgets each step

    env.mj_model.expand_field("dof_frictionloss")
    env.mj_model.expand_field("dof_damping")

This expansion is required because the BAM actuator writes its computed friction budget directly into these fields every simulation step.

randomize_bam_friction (Reset)

The randomize_bam_friction event executes at the beginning of each episode (mode "reset" or "reset" with specific variants). Located at lines 12–26 of src/mjlab_microduck/tasks/mdp.py, it implements non-accumulating randomization:

  1. Resets the scale to 1.0 via actuator.reset_friction_scale(env_ids)
  2. Samples a new multiplier uniformly from scale_range (e.g., (0.5, 1.5))
  3. Applies the sample via actuator.set_friction_scale
def randomize_bam_friction(env, env_ids, scale_range):
    for actuator in env.actuators:
        if isinstance(actuator, FrictionDRBamActuator):
            # Ensure clean slate (non-accumulating)

            actuator.reset_friction_scale(env_ids)
            # Sample new friction multiplier

            lo, hi = scale_range
            samples = torch.rand(len(env_ids), 1) * (hi - lo) + lo
            actuator.set_friction_scale(env_ids, samples)

The explicit reset prevents randomization from accumulating across episodes, ensuring each episode starts from a nominal friction profile before applying new variability.

Task Configuration and Registration

Every environment using BAM actuators must register both events in its task configuration file (e.g., src/mjlab_microduck/tasks/microduck_velocity_env_cfg.py). The registration follows this pattern:


# Startup event: prepare arrays

event_manager.register(
    name="expand_bam_friction_fields",
    func=microduck_mdp.expand_bam_friction_fields,
    mode="startup",
)

# Per-episode randomization

event_manager.register(
    name="randomize_bam_friction",
    func=microduck_mdp.randomize_bam_friction,
    scale_range=(0.7, 1.3),  # 0.7x to 1.3x nominal friction

    mode="reset",
)

The scale_range tuple defines the uniform sampling bounds. Curriculum learning can vary these ranges over training steps by modifying the task configuration, but the underlying mechanism remains consistent: each episode receives a fresh friction factor that directly scales the BAM internal model.

Summary

  • Standard dof_frictionloss is ineffective for BAM actuators because they calculate friction internally for XL330 servo simulation.
  • FrictionDRBamActuator wraps the base actuator to introduce a multiplicative friction_scale factor applied to the internal friction budget.
  • Non-accumulating randomization is enforced by resetting scales to 1.0 before sampling new values at each episode reset.
  • expand_bam_friction_fields prepares MuJoCo arrays for per-environment writes, while randomize_bam_friction handles the sampling logic.
  • Task configurations register both events with specific scale_range values to control the randomization magnitude.

Frequently Asked Questions

Why does the standard MuJoCo dof_frictionloss not work with BAM?

The BAM actuator models voltage-controlled XL330 servos using an internal physics model that computes Coulomb, Stribeck, and load-dependent friction forces independently of MuJoCo's standard fields. Because the BAM actuator writes its own friction budget into the physics state each step, the simulation zeros out dof_frictionloss. Consequently, domain randomization that modifies dof_frictionloss never affects the actual forces generated by the actuator.

How is the friction randomization kept non-accumulating across episodes?

The randomize_bam_friction event explicitly calls actuator.reset_friction_scale(env_ids) to restore the scale to 1.0 before sampling a new random value. This reset ensures that each episode begins from the nominal friction profile, preventing the multiplicative factors from compounding across consecutive episode resets.

What friction components are affected by the scale factor?

The scale factor multiplies the entire velocity-independent friction budget computed by _compute_friction_budget. This includes Coulomb friction (constant resistance), Stribeck effect (velocity-dependent static friction breakaway), and load-dependent friction (torque-dependent resistance terms). velocity-dependent viscous damping is handled separately through the standard dof_damping field.

Where should I register the randomization events in my task config?

Register expand_bam_friction_fields with mode="startup" to run once during environment initialization, and randomize_bam_friction with mode="reset" to run at the beginning of every episode. Both registrations belong in your environment configuration file (e.g., microduck_velocity_env_cfg.py), typically accessed via the event_manager instance provided by the task base class.

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 →