Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions .agents/skills/add-robot/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
name: add-robot
description: Use when adding a new robot to EmbodiChain — scaffolds a RobotCfg subclass (single-file or package layout) with the _build_defaults hook, build_pk_serial_chain, registration, docs page, and test stub.
---

# Add Robot

## When to Use

- Adding a new robot to EmbodiChain.
- Adding a variant to an existing robot (a new version / arm kind / hand brand).
- Scaffolding a `RobotCfg` subclass.

## The RobotCfg Protocol

Every robot config subclasses `RobotCfg` and overrides two hooks:

- `_build_defaults(self, init_dict=None)` — read variant fields from `init_dict`,
set them on `self`, then populate `urdf_cfg` / `control_parts` / `solver_cfg` /
`drive_pros` / `attrs`.
- `build_pk_serial_chain(self, device=...)` — return `{control_part: pk.SerialChain}`,
reading the PK URDF from a single `_pk_urdf_path` source.

`from_dict` is a 3-line template (do not reimplement):

```python
cfg = cls()
cfg._build_defaults(init_dict)
return merge_robot_cfg(cfg, init_dict)
```

`to_dict` / `to_string` / `save_to_file` are inherited from `RobotCfg` and round-trip.

## Two Layouts

| Layout | When | Files |
|--------|------|-------|
| Single-file | Variant-less robot | `my_robot.py` |
| Package | Robot with variants (versions / arm kinds / hand brands) | `types.py`, `cfg.py`, optional `params.py`/`utils.py`, `__init__.py` |

## The Contract (read first)

A cfg's `_build_defaults` must populate:

- `uid` (str)
- `urdf_cfg` (URDFCfg) or `fpath`
- `control_parts` (Dict[str, List[str]]; joint names support regex)
- `solver_cfg` (Dict[str, SolverCfg]; keys match `control_parts`)
- `drive_pros` (JointDrivePropertiesCfg)
- `attrs` (RigidBodyAttributesCfg)

`build_pk_serial_chain` must read from `_pk_urdf_path` (a property for
constant-path robots, a method for variant-dependent paths). The PK chain's DOF
must match the matching `control_parts` entry (the test stub asserts this).

## Steps

1. **Pick a layout** using the table above. Single-file for variant-less robots;
package for robots with variants.
2. **Create the cfg file(s).** Subclass `RobotCfg`. Declare variant fields (enums)
if using the package layout.
3. **Implement `_build_defaults(self, init_dict=None)`.** Set variant fields from
`init_dict`, then populate the Contract fields. Single-file template:

```python
def _build_defaults(self, init_dict=None):
init_dict = init_dict or {}
self.uid = "MyRobot"
self.urdf_cfg = URDFCfg(components=[...])
self.control_parts = {"arm": ["JOINT[1-6]"]}
self.solver_cfg = {"arm": OPWSolverCfg(end_link_name="link6", root_link_name="base_link")}
self.drive_pros = JointDrivePropertiesCfg(stiffness={"JOINT[1-6]": 1e4})
```

Variant-aware template (reads version / arm_kind):

```python
def _build_defaults(self, init_dict=None):
init_dict = init_dict or {}
self.version = MyRobotVersion(init_dict.get("version", "v1"))
self.arm_kind = MyRobotArmKind(init_dict.get("arm_kind", "default"))
... # then urdf_cfg / control_parts / solver_cfg / drive_pros / attrs
```

4. **Implement `build_pk_serial_chain`** reading from `_pk_urdf_path`:

```python
@property
def _pk_urdf_path(self) -> str:
return get_data_path("MyRobot/arm.urdf")

def build_pk_serial_chain(self, device=torch.device("cpu"), **kwargs):
chain = create_pk_serial_chain(
urdf_path=self._pk_urdf_path, device=device,
end_link_name="link6", root_link_name="base_link",
)
return {"arm": chain}
```

5. **Keep `from_dict` as the 3-line template** — do not reimplement it.
6. **Add `__all__` and register** in `embodichain/lab/sim/robots/__init__.py`:

```python
from .my_robot import MyRobotCfg
__all__ = ["MyRobotCfg"]
```

7. **Add documentation:** create `docs/source/resources/robot/<name>.md` and add
it to `docs/source/resources/robot/index.rst`.
8. **Add a test stub** with a `__main__` smoke test + the DOF drift guard. Use
`/add-test` for full test scaffolding; the guard snippet is:

```python
chains = cfg.build_pk_serial_chain()
for part, chain in chains.items():
assert len(chain.get_joint_parameter_names()) == len(cfg.control_parts[part])
```

9. **Verify:** `preview-asset` CLI + `RobotCfg.from_dict(cfg.to_dict())` round-trip.

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| `all` instead of `__all__` | Use `__all__` — lowercase `all` breaks `import *`. |
| `solver_cfg` set twice | Set it once in `_build_defaults` only. |
| PK URDF drifts from sim URDF | Route PK through `_pk_urdf_path`; keep the DOF guard. |
| Reimplementing `from_dict` | Keep the 3-line template; put logic in `_build_defaults`. |
| `root_link_name` as a tuple | It must be a `str`. |
| Calling a nonexistent `validate` | Don't call methods that don't exist. |

## Quick Reference

| Parameter | Type | Description |
|-----------|------|-------------|
| `uid` | str | Unique robot identifier |
| `urdf_cfg` | URDFCfg | URDF file and components |
| `control_parts` | Dict[str, List[str]] | Joint groups for control |
| `solver_cfg` | Dict[str, SolverCfg] | IK solver configurations |
| `drive_pros` | JointDrivePropertiesCfg | Joint stiffness, damping, force |
| `attrs` | RigidBodyAttributesCfg | Rigid-body physics attributes |
| variant fields | enum / str / bool | Optional subclass fields |
| `_pk_urdf_path` | property or method → str | URDF for the FK/IK serial chain |

**File locations:**

- Config: `embodichain/lab/sim/robots/<name>.py` or `embodichain/lab/sim/robots/<name>/`
- Registry: `embodichain/lab/sim/robots/__init__.py`
- Docs: `docs/source/resources/robot/<name>.md`
- Tests: `tests/sim/objects/test_robot_cfg.py`
- Base class: `embodichain/lab/sim/cfg.py` (`RobotCfg`)
- Guide: `docs/source/guides/add_robot.rst` · Tutorial: `docs/source/tutorial/add_robot.rst`
3 changes: 3 additions & 0 deletions .agents/skills/add-robot/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: add-robot
canonical_skill: .agents/skills/add-robot/SKILL.md
project: EmbodiChain
23 changes: 23 additions & 0 deletions .claude/skills/add-robot/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
name: add-robot
description: Claude adapter for the canonical EmbodiChain add-robot skill.
---

# Add Robot - Claude Adapter

Canonical source: `.agents/skills/add-robot/`

## When to use

- Adding a new robot to EmbodiChain.
- Adding a variant to an existing robot.
- Scaffolding a `RobotCfg` subclass.

## Start here

1. Use this adapter when adding or extending a robot config.
2. Then follow `.agents/skills/add-robot/SKILL.md`.

The canonical skill covers the `RobotCfg` protocol (`_build_defaults` hook +
`build_pk_serial_chain` + inherited serialization), the single-file vs package
layouts, the 9-step scaffold, common mistakes, and a quick reference.
9 changes: 9 additions & 0 deletions .github/copilot/add-robot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Add Robot for GitHub Copilot

Canonical source: `.agents/skills/add-robot/`

Use this adapter when adding a new robot to EmbodiChain or adding a variant to an
existing robot (a new version / arm kind / hand brand). Then follow
`.agents/skills/add-robot/SKILL.md` for the `RobotCfg` protocol (`_build_defaults`
hook + `build_pk_serial_chain` + inherited serialization), the single-file vs
package layouts, and the scaffold steps.
1 change: 1 addition & 0 deletions .github/copilot/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ follow the canonical routing rules in `.agents/skills/project-dev-context/`.
- Add task environments: `.github/copilot/add-task-env.md`
- Add functors: `.github/copilot/add-functor.md`
- Add atomic actions: `.github/copilot/add-atomic-action.md`
- Add robots: `.github/copilot/add-robot.md`
- Add tests: `.github/copilot/add-test.md`
- Run pre-commit checks: `.github/copilot/pre-commit-check.md`
- Draft or create pull requests: `.github/copilot/pr.md`
Expand Down
55 changes: 47 additions & 8 deletions agent_context/topics/robot-system/robot-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
| DexforceW1 config package | `embodichain/lab/sim/robots/dexforce_w1/` |
| CobotMagic config | `embodichain/lab/sim/robots/cobotmagic.py` |
| Add-robot tutorial | `docs/source/tutorial/add_robot.rst` |
| Add-robot quick-reference | `docs/source/guides/add_robot.rst` |

## Overview

Expand Down Expand Up @@ -44,8 +45,38 @@ Key fields on `RobotCfg`:
| `urdf_cfg` | `URDFCfg \| None` | Multi-component URDF assembly (e.g. left_arm + right_arm) |
| `solver_cfg` | `SolverCfg \| Dict[str, SolverCfg] \| None` | IK solver config; dict keys must match `control_parts` keys |
| `drive_pros` | `JointDrivePropertiesCfg` | Default drive type is `"force"` (overrides Articulation's `"none"`) |
| `attrs` | `RigidBodyAttributesCfg` | Rigid-body physics attributes (mass, friction, damping, ...) |
| variant fields | `enum \| str \| bool` | Optional subclass fields (e.g. `version`, `arm_kind`, `with_default_eef`) |
| `_pk_urdf_path` | `property \| method → str` | URDF for the FK/IK serial chain (one source, so it can't drift from sim) |

All robot configs support `from_dict(init_dict)` class method for dict-based construction.
## The robot config protocol

Every robot config subclasses `RobotCfg` and overrides two hooks. `from_dict` is a
3-line template — do not reimplement it:

```python
@classmethod
def from_dict(cls, init_dict):
cfg = cls()
cfg._build_defaults(init_dict)
return merge_robot_cfg(cfg, init_dict)
```

- **`_build_defaults(self, init_dict=None)`** — read variant fields from `init_dict`,
set them on `self`, then populate `urdf_cfg`, `control_parts`, `solver_cfg`,
`drive_pros` and `attrs`. (Base `RobotCfg._build_defaults` is a no-op.)
- **`build_pk_serial_chain(self, device=...)`** — return `{control_part: pk.SerialChain}`,
reading the PK URDF from a single `_pk_urdf_path` source (a property for
constant-path robots, a method when the path depends on a variant).

Serialization (`to_dict` / `to_string` / `save_to_file`) is **inherited** from
`RobotCfg` and round-trips: `RobotCfg.from_dict(cfg.to_dict())` reproduces the cfg.

.. note::
`merge_robot_cfg` calls the base `RobotCfg.from_dict` internally, so the
subclass `from_dict` template must stay the 3-line form above — making
`RobotCfg.from_dict` itself call `_build_defaults` → `merge_robot_cfg` would
infinite-recurse.

## Control Parts

Expand Down Expand Up @@ -83,17 +114,21 @@ When using a dict, keys are joint names or regex patterns matching joint names.

## Adding a New Robot

Full guide: `docs/source/tutorial/add_robot.rst`
Full guide: `docs/source/tutorial/add_robot.rst` · Quick reference: `docs/source/guides/add_robot.rst`

Minimal checklist:
1. Create a `@configclass` inheriting `RobotCfg`.
2. Define `urdf_cfg` with URDF component paths and transforms.
3. Define `control_parts` mapping part names to joint name lists.
4. Set `drive_pros` with appropriate stiffness/damping per joint or part.
2. Override `_build_defaults(self, init_dict=None)` — read variant fields from `init_dict`, then populate `urdf_cfg`, `control_parts`, `solver_cfg`, `drive_pros` and `attrs`.
3. Keep `from_dict` as the 3-line template (`cls()` → `_build_defaults` → `merge_robot_cfg`); do not reimplement.
4. Define `control_parts` mapping part names to joint name lists.
5. Configure `solver_cfg` (one `SolverCfg` per control part).
6. For complex robots with multiple variants, use a sub-package with `types.py`, `params.py`, `utils.py`, `cfg.py` (see `dexforce_w1/` as example).
7. Export from `embodichain/lab/sim/robots/__init__.py`.
8. Add robot docs in `docs/source/resources/robot/` and update `docs/source/resources/robot/index.rst`.
6. Implement `build_pk_serial_chain` reading from `_pk_urdf_path` (property for constant paths, method for variant-dependent).
7. For robots with variants, use a sub-package with `types.py` (enums + `__all__`), `cfg.py` (variant-aware `_build_defaults`), optional `params.py` / `utils.py` helpers (see `dexforce_w1/` as example).
8. Export from `embodichain/lab/sim/robots/__init__.py` and set `__all__`.
9. Add robot docs in `docs/source/resources/robot/` and update `docs/source/resources/robot/index.rst`.
10. Test — a `__main__` smoke test + the DOF drift guard + `preview-asset` CLI.

Serialization (`to_dict` / `save_to_file`) is inherited — no need to implement it.

## Available Robots

Expand All @@ -110,3 +145,7 @@ Minimal checklist:
- **Missing `urdf_cfg` for multi-component robots** — single-file robots use `fpath`; multi-component robots (e.g. dual-arm) require `urdf_cfg` with component transforms.
- **Mimic joints not excluded** — `get_joint_ids(remove_mimic=False)` includes mimic joints by default. Pass `remove_mimic=True` for active-only joints.
- **`init_qpos` shape mismatch** — must be `(num_joints,)`. A wrong-length array causes silent truncation or index errors at sim start.
- **`all` instead of `__all__`** — lowercase `all` does not work with `from module import *`; use `__all__`.
- **`solver_cfg` set in multiple places** — set it once in `_build_defaults` only; setting it elsewhere (e.g. a build helper) gets overwritten and is dead code.
- **PK URDF drifts from the sim URDF** — route `build_pk_serial_chain` through `_pk_urdf_path` and keep the DOF drift-guard test so silent drift is caught.
- **Reimplementing `from_dict`** — keep the 3-line template; put construction logic in `_build_defaults`. (Making the base `RobotCfg.from_dict` call `merge_robot_cfg` would infinite-recurse, since `merge_robot_cfg` calls `RobotCfg.from_dict`.)
Loading
Loading