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

> Learn how to perform joint-friction domain randomization with the BAM actuator in Microduck RL. This guide explains effective friction randomization techniques for better simulation results.

- Repository: [Pollen Robotics/microduck_rl](https://github.com/pollen-robotics/microduck_rl)
- Tags: how-to-guide
- Published: 2026-09-01

---

**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`](https://github.com/pollen-robotics/microduck_rl/blob/main/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:

```python
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`](https://github.com/pollen-robotics/microduck_rl/blob/main/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:

```python
@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`](https://github.com/pollen-robotics/microduck_rl/blob/main/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`

```python
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`](https://github.com/pollen-robotics/microduck_rl/blob/main/src/mjlab_microduck/tasks/microduck_velocity_env_cfg.py)). The registration follows this pattern:

```python

# 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`](https://github.com/pollen-robotics/microduck_rl/blob/main/microduck_velocity_env_cfg.py)), typically accessed via the `event_manager` instance provided by the task base class.