# How Unactuated Joints Are Handled in Microduck Robot Models

> Learn how unactuated joints are managed in the Microduck robot model. The reinforcement learning pipeline focuses on 14 servo joints by excluding passive joints from actuation, observation, and reward.

- Repository: [Pollen Robotics/microduck_rl](https://github.com/pollen-robotics/microduck_rl)
- Tags: internals
- Published: 2026-09-08

---

**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`](https://github.com/pollen-robotics/microduck_rl/blob/main/microduck_constants.py), these joints are defined when specifying wheel and backlash XML file paths:

```python

# 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`](https://github.com/pollen-robotics/microduck_rl/blob/main/microduck_constants.py) (lines 33-36), the actuator's `target_names_expr` is defined as:

```python
_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`](https://github.com/pollen-robotics/microduck_rl/blob/main/mdp.py) provide helper functions that exclusively return servo joint indices. The `_servo_joint_ids` function (lines 26-33) uses regex filtering:

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

```python

# 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`](https://github.com/pollen-robotics/microduck_rl/blob/main/mdp.py)) builds this filtered metadata:

```python
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`](https://github.com/pollen-robotics/microduck_rl/blob/main/microduck_velocity_env_cfg.py) (line 286-287), observation groups use filtered joint selectors with **zero-padding**:

```python

# 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`](https://github.com/pollen-robotics/microduck_rl/blob/main/src/mjlab_microduck/robot/microduck_constants.py) | Defines joint naming conventions, actuator configs with exclusion regex |
| [`src/mjlab_microduck/tasks/mdp.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/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`](https://github.com/pollen-robotics/microduck_rl/blob/main/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`](https://github.com/pollen-robotics/microduck_rl/blob/main/src/mjlab_microduck/tasks/backlash.py) | Backlash encoder logic linking servos to their passive backlash hinges |
| [`tests/test_wheel_glide.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/tests/test_wheel_glide.py), [`tests/test_infer_policy_bam.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/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.