# Purpose of the `_servo_joint_ids()` Helper in Microduck RL: How It Isolates Active Joints in Isaac Gym Simulations

> Discover how Microduck RL's _servo_joint_ids() helper isolates active joints, preventing accidental manipulation of physics elements and ensuring accurate reward functions in Isaac Gym simulations.

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

---

**The `_servo_joint_ids()` helper in Microduck RL filters out passive joints to return only the 14 actuated servo joint indices, ensuring reward functions and observations never accidentally manipulate non-actuated physics elements.**

The `microduck_rl` repository implements reinforcement learning for a 14-DOF robot arm using NVIDIA's Isaac Gym. The simulation model includes both **servo joints** (the 14 actuated degrees of freedom) and **passive joints** (backlash hinges, wheels, jaw linkages) whose names are prefixed with `passive_`. When physics engines load these additional joints, the entity's joint array grows and the servo indices become non-contiguous. The `_servo_joint_ids()` helper solves this indexing problem by providing a canonical, cached mapping to the active joints.

## Why Passive Joints Break Index Assumptions

Robotic simulators like Isaac Gym represent every articulated element as a joint in the physics engine. The Microduck robot's URDF/MJCF includes:

- 14 servo-actuated joints (shoulder, elbow, wrist, gripper)
- Multiple passive joints (linkage mechanisms, compliance elements, wheel pivots)

When the simulation initializes, these passive joints intersperse with servos in the joint array. Code that assumes joint 0-13 are the servos will silently read or write to passive joints instead, causing:

- **Physics corruption** — torques applied to passive joints violate constraints
- **NaN propagation** — invalid force calculations destabilize training
- **Policy failure** — observations include non-controllable state

As documented in [`AGENTS.md`](https://github.com/pollen-robotics/microduck_rl/blob/main/AGENTS.md), the codebase enforces an invariant: *"Joint layout … never hard-code joint indices."* The `_servo_joint_ids()` helper implements this invariant programmatically.

## How `_servo_joint_ids()` Works

Located in [`src/mjlab_microduck/tasks/mdp.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/src/mjlab_microduck/tasks/mdp.py) at lines 26-44, the helper performs three core operations:

1. **Regex filtering** — applies `r"^(?!passive_).*"` to exclude all joint names starting with `passive_`
2. **Per-asset caching** — stores results in `_servo_joint_ids_cache` keyed by `asset.id` for O(1) subsequent access
3. **Index tensor construction** — returns a list of integer indices suitable for PyTorch advanced indexing

The implementation ensures that every downstream component receives a stable view into exactly the 14 controllable joints regardless of model variations or joint ordering changes.

## Convenience Wrappers Built on the Helper

The same file provides three higher-level utilities that wrap `_servo_joint_ids()` for common access patterns:

```python

# Lines 46-52 in src/mjlab_microduck/tasks/mdp.py

_servo_joint_pos(env, asset)      # Returns joint positions for 14 servos only

_servo_joint_vel(env, asset)      # Returns joint velocities for 14 servos only

_servo_default_joint_pos(env, asset)  # Returns default positions for 14 servos only

```

These functions encapsulate the indexing logic so reward and observation code remains clean and auditable.

## Practical Usage Examples

### Isolating Servo Positions for Observations

```python
from mjlab_microduck.tasks.mdp import _servo_joint_pos

def compute_observation(env):
    robot = env.scene["robot"]
    # Shape: (batch_size, 14) — guaranteed servo-only

    servo_angles = _servo_joint_pos(env, robot)
    return servo_angles

```

### Direct Index Access for Custom Rewards

```python
from mjlab_microduck.tasks.mdp import _servo_joint_ids

def joint_torque_regularization(env, asset):
    servo_ids = _servo_joint_ids(env, asset)
    # Safely index full joint array without passive contamination

    torques = asset.data.joint_torque[:, servo_ids]
    return -torch.sum(torch.square(torques), dim=1)

```

### Velocity Penalty on Actuated Joints Only

```python
from mjlab_microduck.tasks.mdp import _servo_joint_vel

def joint_velocity_penalty(env, asset):
    # Penalize high velocities — but only where motors can actually respond

    vel = _servo_joint_vel(env, asset)
    return -torch.mean(torch.square(vel), dim=1)

```

## Architecture Impact

By centralizing joint filtering in [`mdp.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/mdp.py), Microduck RL achieves:

- **Single source of truth** — one regex definition controls servo identification
- **Zero runtime cost after first call** — caching eliminates repeated string matching
- **Model flexibility** — passive joint additions never require consumer code changes
- **Auditability** — all joint-dependent logic traces back to explicit helper calls

Reward implementations throughout the repository (joint-limit penalties, domain randomization, curriculum events) rely on this helper to maintain physical correctness across training scales from single-environment debugging to thousand-GPU distributed runs.

## Summary

- **`_servo_joint_ids()`** filters passive joints via regex and returns cached indices for the 14 actuated servos
- Implemented in [`src/mjlab_microduck/tasks/mdp.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/src/mjlab_microduck/tasks/mdp.py) with O(1) caching per environment
- Enables **`_servo_joint_pos`**, **`_servo_joint_vel`**, and **`_servo_default_joint_pos`** convenience wrappers
- Prevents physics corruption, NaN propagation, and policy training failures from misindexed joints
- Enforces the "never hard-code joint indices" invariant from [`AGENTS.md`](https://github.com/pollen-robotics/microduck_rl/blob/main/AGENTS.md)

## Frequently Asked Questions

### What happens if I use raw joint indices instead of `_servo_joint_ids()`?

Your code will address passive joints that lack actuation physics. This produces invalid torque applications, constraint violations, and frequently destabilizes training with NaN gradients. The helper guarantees you only manipulate controllable degrees of freedom.

### Does `_servo_joint_ids()` impact simulation performance?

After the first call per environment, performance impact is negligible. The function caches results in `_servo_joint_ids_cache` keyed by asset ID, making subsequent calls O(1) tensor retrieval with no regex re-evaluation.

### Why use a regex instead of a hardcoded joint list?

The regex `r"^(?!passive_).*" ` automatically adapts to model variations. If the URDF adds new passive joints or reorders the joint list, the helper continues returning correct servo indices without code changes. This aligns with the Microduck RL design principle of model-driven configuration over manual index maintenance.

### Where else in the codebase is this helper used?

Search [`src/mjlab_microduck/tasks/mdp.py`](https://github.com/pollen-robotics/microduck_rl/blob/main/src/mjlab_microduck/tasks/mdp.py) for calls to `_servo_joint_ids()` and its wrappers. The helper appears in joint-limit rewards, default position initializations, domain randomization routines, and curriculum event conditions—anywhere joint-specific logic must exclude passive physics elements.