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 of1.0for the specified environmentsset_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:
- Resets the scale to
1.0viaactuator.reset_friction_scale(env_ids) - Samples a new multiplier uniformly from
scale_range(e.g.,(0.5, 1.5)) - 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_frictionlossis ineffective for BAM actuators because they calculate friction internally for XL330 servo simulation. FrictionDRBamActuatorwraps the base actuator to introduce a multiplicativefriction_scalefactor applied to the internal friction budget.- Non-accumulating randomization is enforced by resetting scales to
1.0before sampling new values at each episode reset. expand_bam_friction_fieldsprepares MuJoCo arrays for per-environment writes, whilerandomize_bam_frictionhandles the sampling logic.- Task configurations register both events with specific
scale_rangevalues 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →