How Unactuated Joints Are Handled in Microduck Robot Models

Unactuated ("passive") joints in the Microduck robot are excluded from actuation, observation, and reward calculations using a consistent naming convention and regex filtering, ensuring the reinforcement learning pipeline operates on only 14 servo-driven joints.

The Microduck robot from pollen-robotics/microduck_rl contains multiple unactuated joints—including wheels, jaw-linkage hinges, and backlash hinges—that exist for realistic physics simulation but are not motor-driven. These passive joints are systematically identified and filtered throughout the codebase to prevent the policy from attempting to control joints with no actuators.

The Naming Convention for Passive Joints

All unactuated joints follow a strict naming scheme using the passive_ prefix. This convention appears in joint definitions across the robot's URDF/MJCF models.

Examples include:

  • passive_LF_wheel (left front wheel)
  • passive_RR_wheel (right rear wheel)
  • passive_left_hip_yaw_backlash (backlash hinge for hip yaw joint)

In microduck_constants.py, these joints are defined when specifying wheel and backlash XML file paths:


# Lines 28-33 in microduck_constants.py

WHEEL_XML = "passive_LF_wheel,passive_RF_wheel,passive_LR_wheel,passive_RR_wheel"
BACKLASH_JOINTS = [
    "passive_left_hip_yaw_backlash",
    "passive_right_hip_yaw_backlash",
    # ... additional backlash joints

]

This uniform prefix enables single regex-based filtering across all pipeline stages.

Excluding Passive Joints from Actuation

The BAM (Bilinear Adaptive Motor) actuator configuration uses a negative lookahead regex to skip passive joints when creating motor controllers. In microduck_constants.py (lines 33-36), the actuator's target_names_expr is defined as:

_BAM_ACTUATOR_KWARGS = dict(
    motor_name="xl330",
    model="m6",
    target_names_expr=(r"^(?!passive_).*",),  # Excludes passive_* joints

    kp_fw=200.0,
    # ... additional parameters

)

The same regex pattern is applied to encoder variants for backlash-compensated position feedback. This ensures exactly 14 joints receive actuation commands matching the hardware servo count.

Filtering in Observation and Reward Calculations

The MDP (Markov Decision Process) utilities in mdp.py provide helper functions that exclusively return servo joint indices. The _servo_joint_ids function (lines 26-33) uses regex filtering:

def _servo_joint_ids(env, robot):
    """Returns joint IDs for the 14 servo-actuated joints only."""
    return robot.find_joints(r"^(?!passive_).*")[0]

Downstream functions like _servo_joint_pos, _servo_joint_vel, and all reward terms rely on these filtered indices. This guarantees index stability—passive joints never shift the position of servo joints in state arrays.

Reward and penalty functions explicitly exclude passive joints to avoid KeyError exceptions or spurious gradients. The codebase comments (lines 68-74 in mdp.py) note:


# Filter out passive joints before iterating—reward functions should

# never see joints without actuators, as this would produce undefined

# behavior when accessing actuator-specific data.

ONNX Export and Policy Metadata

When exporting trained policies for hardware deployment, joint metadata must match the 14-DOF actuation space. The _get_base_metadata_no_passive function (lines 79-86 in mdp.py) builds this filtered metadata:

def _get_base_metadata_no_passive(env, run_path):
    robot = env.scene["robot"]
    full_names = list(robot.joint_names)
    # Keep indices of non-passive joints only

    keep_idx = [i for i, n in enumerate(full_names) if not n.startswith("passive_")]
    joint_names = [full_names[i] for i in keep_idx]
    
    return {
        "joint_names": joint_names,
        "num_joints": len(joint_names),  # Always 14

        # ... additional metadata

    }

This prevents action dimension mismatches between simulation and real hardware.

Observation Zero-Padding for Variable Joint Counts

Environment configurations handle cases where some variants include passive joints while others don't. In microduck_velocity_env_cfg.py (line 286-287), observation groups use filtered joint selectors with zero-padding:


# Observation config that maintains 61D tensor regardless of passive joints

obs["joint_pos"] = JointPositionsObservationCfg(
    joint_names=(r"^(?!passive_).*",),  # Exclude passive

    missing_ok=True,  # Zero-pad if joint not present

    scale=1.0,
)

This preserves constant tensor shapes (61-dimensional observations) across environment variants, enabling batch-consistent training.

Key Files and Their Roles

File Purpose
src/mjlab_microduck/robot/microduck_constants.py Defines joint naming conventions, actuator configs with exclusion regex
src/mjlab_microduck/tasks/mdp.py Core utilities: _servo_joint_ids, _get_base_metadata_no_passive, reward filtering
src/mjlab_microduck/tasks/microduck_velocity_env_cfg.py Observation group configs with zero-padding for passive joint variants
src/mjlab_microduck/tasks/backlash.py Backlash encoder logic linking servos to their passive backlash hinges
tests/test_wheel_glide.py, tests/test_infer_policy_bam.py Validate passive joint exclusion in inference and physics checks

Summary

  • Naming convention: All unactuated joints use the passive_ prefix, enabling regex-based filtering
  • Actuation: BAM actuators exclude passive_* via r"^(?!passive_).*" regex in target_names_expr
  • Observation: Helper functions return only servo joint indices; reward code filters passive joints to prevent errors
  • Export: ONNX metadata explicitly drops passive joints to match 14-DOF hardware
  • Consistency: Zero-padding in observation configs maintains stable tensor shapes across environment variants

Frequently Asked Questions

What joints in Microduck are considered unactuated?

The unactuated joints include four wheels (passive_LF_wheel, passive_RF_wheel, passive_LR_wheel, passive_RR_wheel), jaw-linkage hinges, and backlash hinges for hip and knee servos (e.g., passive_left_hip_yaw_backlash). These provide realistic physics for wheel rolling and encoder compliance without motor control.

Why not simply remove passive joints from the robot model entirely?

Passive joints are required for physical accuracy—wheels need rolling constraints, and backlash hinges simulate realistic encoder behavior. Removing them would produce training-to-reality gaps. The passive_ convention keeps them in simulation while making them invisible to the policy.

How does the regex r"^(?!passive_).*" work?

This is a negative lookahead pattern that matches any string not starting with passive_. The ^(?!passive_) asserts that the position after start-of-string is not followed by passive_, then .* matches the remainder. It functions as a compact exclusion filter across the codebase.

Does hardware deployment use the same 14-joint indexing as simulation?

Yes. The _get_base_metadata_no_passive function ensures exported policies carry metadata listing exactly the 14 servo joints in hardware order. The ONNX runtime maps policy outputs directly to hardware servo IDs without passive joint slots.

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 →