Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2127f3d
Add press atomic action (#317)
skywhite1024 Jul 1, 2026
7f105c0
Use URRobotCfg in demo UR robots (#348)
yuecideng Jul 2, 2026
1f86895
[debug] solve TYPE_CHECKING problem in shape.py (#335)
wuxinxin27 Jul 2, 2026
38d8386
Add coordinated pickment atomic action (#323)
skywhite1024 Jul 2, 2026
68b1e2c
Add coordinated placement and atomic action benchmarks (#318)
skywhite1024 Jul 3, 2026
11ad967
Fix coordinated atomic action demo assets (#350)
skywhite1024 Jul 3, 2026
aaebdc8
Document coordinated atomic action tutorials (#351)
skywhite1024 Jul 3, 2026
10c4144
Fix excessive EEF rotation for symmetric grasps (#353)
skywhite1024 Jul 3, 2026
0841962
Refactor atomic actions into primitives package (#346)
yuecideng Jul 4, 2026
a025509
Improve Gym overview recording documentation (#357)
yuecideng Jul 4, 2026
ab11a64
Add max_episodes CLI override for run_env (#360)
yuecideng Jul 4, 2026
f463e4e
Fix Place TCP z-roll symmetry opt-in (#355)
skywhite1024 Jul 4, 2026
bf72a8e
Fix CUDA device index preservation in standardize_device_string (#364)
yuecideng Jul 6, 2026
3659b2e
Add env-batched parallel motion generation (#349)
yuecideng Jul 6, 2026
87fc0cc
fix(dexforce_w1): merge PytorchSolver and SRSSolver defaults, fix sol…
yuecideng Jul 6, 2026
3bb4799
Pickup action support upright (#352)
matafela Jul 8, 2026
df80562
Rigid object can use vhacd as decomposition method (#344)
matafela Jul 9, 2026
5c0a152
feat: Support 6 light types (point, sun, direction, spot, rect, mesh)…
yuecideng Jul 9, 2026
52bdf0d
fix(sim): propagate max_linear_velocity and max_angular_velocity in R…
yuecideng Jul 9, 2026
e659b65
Add randomize_anchor_height functor for anchor height randomization (…
yuecideng Jul 9, 2026
c5a33e2
Add articulation and robot joint limit APIs (#359)
yuecideng Jul 10, 2026
944e5a9
feat(robot): add FrankaPandaCfg and dual-arm coordinated placement tu…
yuecideng Jul 10, 2026
bb4e4b9
Simplify atomic action tutorials (#371)
yuecideng Jul 10, 2026
531202c
Fix mesh collision param fallback (#375)
yuecideng Jul 11, 2026
b41716c
Improve resource-aware test execution (#378)
yuecideng Jul 12, 2026
6c2d99e
Fix window camera pose round-trip conversion (#373)
yuecideng Jul 12, 2026
cc7e215
Preserve DexSim world up in window camera capture (#384)
yuecideng Jul 12, 2026
beea6e4
Fix: ur solver now can support ik nearest weight. (#374)
matafela Jul 13, 2026
901c5de
Reset dataset manager and add head drive properties (#369)
yhnsu Jul 13, 2026
d719a0f
Bump version to v0.2.3 (#391)
yuecideng Jul 13, 2026
95fa94f
Defer release publication until publish (#392)
yuecideng Jul 13, 2026
f9dca60
wip
yuecideng Jul 13, 2026
1d4b9eb
docs: define Newton runtime integration contracts
yuecideng Jul 13, 2026
ee88d7f
docs: plan Newton runtime contracts stage 1
yuecideng Jul 13, 2026
a9be048
fix(diff): default environments use Newton solver dynamics
yuecideng Jul 14, 2026
495d3be
fix(diff): persist Newton solver trajectory
yuecideng Jul 14, 2026
a1c4fff
fix(diff): use manager-owned Newton trajectories
yuecideng Jul 15, 2026
b5ac98d
fix(diff): defer resets until Newton backward
yuecideng Jul 15, 2026
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
71 changes: 52 additions & 19 deletions .agents/skills/add-atomic-action/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ full-DoF trajectory.
| Base classes (`ActionCfg`, `AtomicAction`, `WorldState`, `ActionResult`, typed targets, `ObjectSemantics`) | `embodichain/lab/sim/atomic_actions/core.py` |
| Affordance types (`Affordance`, `AntipodalAffordance`, `InteractionPoints`) | `embodichain/lab/sim/atomic_actions/affordance.py` |
| Stateless trajectory helpers (`TrajectoryBuilder`) | `embodichain/lab/sim/atomic_actions/trajectory.py` |
| Built-in actions (reference implementations) | `embodichain/lab/sim/atomic_actions/actions.py` |
| Built-in action primitives (reference implementations) | `embodichain/lab/sim/atomic_actions/primitives/` |
| Backward-compatible action re-export module | `embodichain/lab/sim/atomic_actions/actions.py` |
| Engine + global registry (`register_action`, `AtomicActionEngine.register` / `run`) | `embodichain/lab/sim/atomic_actions/engine.py` |
| Public API exports | `embodichain/lab/sim/atomic_actions/__init__.py` |
| Reference docs | `docs/source/overview/sim/atomic_actions.md` |
| Reference docs | `docs/source/overview/sim/atomic_actions/index.md`, `docs/source/overview/sim/atomic_actions/builtin_actions.md` |

## The Contract (read first)

Expand All @@ -43,19 +44,29 @@ inherit from `MoveEndEffector` or any other action. Each action:
full-DoF shaped `(n_envs, n_waypoints, robot.dof)` and `next_state` is the
successor `WorldState` (advance `last_qpos` to the trajectory's final row;
set/clear/preserve `held_object` per the action's semantics).
- `success` is a per-environment boolean tensor of shape `(n_envs,)` (or a
scalar bool). Use `ActionResult.success_all` (or `.success.all()`) when you
need a single aggregate boolean.

There is **no** `validate` method, **no** `**kwargs`, **no** `start_qpos` parameter,
**no** `updates_held_object_state` flag, and **no** `get_held_object_state`. The
`WorldState` is the single channel for inter-action state.

`ActionCfg` (and therefore every action cfg) carries two motion-source fields used
by `TrajectoryBuilder.plan_arm_traj`:
- `motion_source: str = "ik_interp"` — `"ik_interp"` (batched IK + interpolation)
or `"motion_gen"` (delegates to the batched `MotionGenerator`).
- `planner_type: str | None = None` — `"toppra"` or `"neural"`; required when
`motion_source="motion_gen"`.

## Steps

### 1. Define the config

Add a `@configclass`-decorated class that extends `ActionCfg` **directly** (the cfg
hierarchy is flat — do not inherit from another action's cfg). Place it in
`embodichain/lab/sim/atomic_actions/actions.py` alongside the existing configs, or in
a new file if the action is large.
hierarchy is flat — do not inherit from another action's cfg). For a built-in
primitive, place the config beside the action class in
`embodichain/lab/sim/atomic_actions/primitives/<action_name>.py`.

```python
from __future__ import annotations
Expand Down Expand Up @@ -147,20 +158,20 @@ class Push(AtomicAction):
control_part=self.cfg.control_part,
)

# 3. Plan the arm trajectory via the builder (uses IK + interpolation).
# 3. Plan the arm trajectory via the builder (uses IK + interpolation by default;
# set cfg.motion_source="motion_gen" to use the MotionGenerator instead).
target_states = [
[PlanState(xpos=contact_xpos[i], move_type=MoveType.EEF_MOVE)]
for i in range(self.n_envs)
]
ok, arm_traj = self.builder.plan_arm_traj(
success, arm_traj = self.builder.plan_arm_traj(
target_states,
start_arm_qpos,
self.cfg.sample_interval,
control_part=self.cfg.control_part,
arm_dof=self.arm_dof,
cfg=self.cfg,
)
if not ok:
return self._fail(state)

# 4. Embed the arm slice into a full-DoF trajectory (n_envs, n_wp, robot.dof).
full = torch.empty(
Expand All @@ -172,7 +183,7 @@ class Push(AtomicAction):
full[:, :, self.arm_joint_ids] = arm_traj

return ActionResult(
success=True,
success=success,
trajectory=full,
next_state=WorldState(
last_qpos=full[:, -1, :].clone(),
Expand All @@ -182,7 +193,7 @@ class Push(AtomicAction):

def _fail(self, state: WorldState) -> ActionResult:
return ActionResult(
success=False,
success=torch.zeros(self.n_envs, dtype=torch.bool, device=self.device),
trajectory=torch.empty(
(self.n_envs, 0, self.robot_dof),
dtype=torch.float32,
Expand All @@ -195,6 +206,8 @@ class Push(AtomicAction):
**Rules:**
- `execute()` returns an `ActionResult` — never a bare tuple.
- `trajectory` shape is always `(n_envs, n_waypoints, robot.dof)` (full robot DoF).
- Pass `cfg=self.cfg` to every `self.builder.plan_arm_traj(...)` call so the builder
reads `motion_source` / `planner_type` from the action config.
- Use `self.builder.<helper>` for all trajectory math (`resolve_pose_target`,
`resolve_joint_target`, `resolve_start_qpos`, `apply_local_offset`, `plan_arm_traj`,
`plan_joint_traj`, `split_three_phase`, `interpolate_hand_qpos`). Do not reimplement
Expand Down Expand Up @@ -228,11 +241,25 @@ register_action("push", Push)

### 5. Export from the public API

Add the config, action class, and any new target to
Add the config, action class, and any new target to the package exports. For a
built-in primitive, first export it from
`embodichain/lab/sim/atomic_actions/primitives/__init__.py`:

```python
from .push import Push, PushCfg

__all__ = [
...,
"Push",
"PushCfg",
]
```

Then export it from the public API in
`embodichain/lab/sim/atomic_actions/__init__.py`:

```python
from .actions import Push, PushCfg
from .primitives import Push, PushCfg
# (and from .core import PushTarget if you defined one)

__all__ = [
Expand All @@ -242,12 +269,16 @@ __all__ = [
]
```

Keep `embodichain/lab/sim/atomic_actions/actions.py` as a compatibility facade;
update it only if the new built-in should also be available from the legacy
`embodichain.lab.sim.atomic_actions.actions` import path.

### 6. Update the supported actions table

Add a row to the table in `docs/source/overview/sim/atomic_actions.md`:
Add a row to the table in `docs/source/overview/sim/atomic_actions/builtin_actions.md`:

```markdown
| `Push` | `PushCfg` | `PushTarget` — contact pose | Approach → push forward |
| `Push` | Single | `PushTarget` — contact pose | Approach → push forward | Add a demo asset or `N/A` |
```

### 7. Write a test
Expand All @@ -273,7 +304,7 @@ def test_push_action_returns_full_dof_trajectory():
):
result = action.execute(PushTarget(contact_pose=torch.eye(4)), state)
assert isinstance(result, ActionResult)
assert result.success is True
assert result.success_all is True
assert result.trajectory.shape == (NUM_ENVS, 10, TOTAL_DOF)
# push preserves held_object
assert result.next_state.held_object is state.held_object
Expand All @@ -289,6 +320,8 @@ def test_push_action_returns_full_dof_trajectory():
| `execute(target, start_qpos=None, **kwargs)` | Signature is `execute(self, target, state: WorldState) -> ActionResult`. No `**kwargs`, no `start_qpos`. |
| Reimplementing IK / interpolation inline | Use `self.builder.plan_arm_traj(...)`, `self.builder.plan_joint_traj(...)`, and friends. |
| Returning arm-only or arm+hand trajectory | Always embed into full `robot.dof` before returning. |
| Forgetting `cfg=self.cfg` in `plan_arm_traj` | The builder defaults to `motion_source="ik_interp"`; pass `cfg=self.cfg` to opt into `motion_gen` / `planner_type`. |
| Treating `ActionResult.success` as a scalar | It is `(n_envs,)` for batched actions; use `.success_all` or `.success.all()` for a single bool. |
| `name` not matching the engine registration key | Keep `cfg.name` identical to the key passed to `engine.register(...)` / `register_action(...)`. |
| Forgetting to export from `__init__.py` | Users import from the public API — missing exports cause `ImportError`. |
| Inheriting another action's cfg | Cfgs are flat; extend `ActionCfg` directly and declare the fields you need. |
Expand All @@ -299,8 +332,8 @@ def test_push_action_returns_full_dof_trajectory():
|------|--------|
| 1 | Define a flat `@configclass` extending `ActionCfg` with a unique `name` |
| 2 | Define a typed target (or reuse `EndEffectorPoseTarget` / `JointPositionTarget` / `NamedJointPositionTarget` / `GraspTarget` / `HeldObjectPoseTarget`) |
| 3 | Subclass `AtomicAction` directly, set `TargetType`, compose `TrajectoryBuilder`, implement `execute(target, state) -> ActionResult` |
| 3 | Subclass `AtomicAction` directly, set `TargetType`, compose `TrajectoryBuilder`, implement `execute(target, state) -> ActionResult` (pass `cfg=self.cfg` to `plan_arm_traj` and return per-env `success`) |
| 4 | Register: `engine.register(Push(mg, cfg=...))` (instance) or `register_action("push", Push)` (class) |
| 5 | Export config + action (+ target) from `__init__.py` |
| 6 | Add a row to the supported-actions table in the overview docs |
| 5 | Export config + action (+ target) from `primitives/__init__.py` and `atomic_actions/__init__.py` |
| 6 | Add a row to the supported-actions table in `builtin_actions.md` and update API reference docs |
| 7 | Write behavioural tests (target type, full-DoF shape, `WorldState` contract) |
38 changes: 36 additions & 2 deletions .agents/skills/add-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class TestMySimComponent:

def teardown_method(self):
self.sim.destroy()
SimulationManager.flush_cleanup_queue()

def test_basic_behavior(self):
result = self.sim.do_something()
Expand All @@ -99,6 +100,32 @@ class TestMySimComponent:
self.sim.do_something(bad_input)
```

## Resource-Aware Test Classification

Use the narrowest test type that proves the behavior:

- Prefer mocks or CPU-only inputs for pure logic and shape validation.
- `tests/conftest.py` automatically classifies conventional CUDA, renderer, and
real-simulation tests. Add `@pytest.mark.gpu`, `@pytest.mark.slow`, or
`@pytest.mark.requires_sim` explicitly only when the test's node id/source
cannot reveal that requirement (for example, a hidden CUDA helper or an
end-to-end toolkit test).

GPU tests are skipped by default to keep normal test runs within the shared VRAM budget. Run them explicitly and serially:

```bash
# Default suite: GPU-marked tests are skipped.
pytest tests/

# Dedicated GPU suite. Do not add -n unless pytest-xdist is installed.
pytest tests/ --run-gpu -m gpu
```

For backend/device matrices, run the complete contract on one representative
configuration and use small (one environment, low-resolution) smoke tests for
the remaining configurations. Always destroy a real `SimulationManager` and
flush its cleanup queue in teardown.

## Mocking Patterns for Functor Tests

Most functor tests don't need a live simulation. Use mock objects following the pattern in `tests/gym/envs/managers/test_reward_functors.py`:
Expand Down Expand Up @@ -199,6 +226,9 @@ pytest tests/<subpath>/test_<module>.py -v
# Single test function
pytest tests/<subpath>/test_<module>.py::test_expected_output -v

# GPU-specific test
pytest tests/<subpath>/test_<module>.py --run-gpu -m gpu -v

# Single test class method
pytest tests/<subpath>/test_<module>.py::TestMyClass::test_basic_behavior -v
```
Expand All @@ -219,6 +249,8 @@ black tests/<subpath>/test_<module>.py
| `from __future__` | Required after header |
| Magic numbers | Define as named constants with explanatory comments |
| Simulation tests | Initialize/teardown in `setup_method`/`teardown_method` |
| CUDA coverage | Use `@pytest.mark.gpu`; run with `--run-gpu -m gpu` |
| Long integration | Use `@pytest.mark.slow`; keep it out of normal PR runs |
| Pure-logic tests | Use mock objects, no real sim |
| `SceneEntityCfg` | Use `MagicMock(uid="...")` in tests |
| Assertions | `assert`, `pytest.approx`, `torch.allclose`, `pytest.raises` |
Expand All @@ -232,14 +264,16 @@ black tests/<subpath>/test_<module>.py
| Using real `SimulationManager` for functor tests | Use `MockEnv`/`MockSim` — much faster, no GPU needed |
| Hardcoded numbers without explanation | Define as `EXPECTED_DISTANCE = 0.5 # cube at origin, target at (0.5, 0, 0)` |
| Testing multiple concepts in one function | Split into separate `test_<scenario>` functions |
| Forgetting `teardown_method` | Always call `self.sim.destroy()` in teardown |
| Forgetting cleanup | Call `self.sim.destroy()` and `SimulationManager.flush_cleanup_queue()` in teardown |
| Using full matrices | Use one full representative case and low-resource smoke coverage elsewhere |
| Not running `black` on test file | CI checks all files including tests |

## Quick Reference

| Action | Command |
|--------|---------|
| Run all tests | `pytest tests/` |
| Run default tests | `pytest tests/` |
| Run GPU tests | `pytest tests/ --run-gpu -m gpu` |
| Run single file | `pytest tests/<path>/test_<name>.py -v` |
| Run single test | `pytest tests/<path>::test_<name> -v` |
| Run with print output | `pytest -s tests/<path>/test_<name>.py` |
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/project-dev-context/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Use this skill when:
- the request says `刷新项目上下文`
- the request says `更新项目上下文`
- the request says `写项目上下文`
- the request names a known project topic such as `env-framework`, `manager-functor`, or `ik-solvers`
- the request names a known project topic such as `env-framework`, `manager-functor`, `ik-solvers`, or `atomic-actions`

## Start here

Expand Down
23 changes: 17 additions & 6 deletions .github/workflows/docs-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ on:
description: "Existing CI/CD Pipeline run ID to deploy from instead of rebuilding"
required: false
type: string
release:
types: [published]

permissions:
actions: read
Expand All @@ -30,7 +32,8 @@ jobs:
if: >
(github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push') ||
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main') ||
(github.event_name == 'workflow_dispatch' &&
inputs.artifact_run_id != '')
runs-on: ubuntu-latest
Expand All @@ -56,7 +59,11 @@ jobs:
uses: actions/deploy-pages@v4

build-and-deploy:
if: github.event_name == 'workflow_dispatch' && inputs.artifact_run_id == ''
if: >
(github.event_name == 'workflow_dispatch' && inputs.artifact_run_id == '') ||
(github.event_name == 'release' &&
github.event.action == 'published' &&
startsWith(github.event.release.tag_name, 'v'))
runs-on: Linux
env:
NVIDIA_DRIVER_CAPABILITIES: all
Expand All @@ -77,7 +84,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref != '' && inputs.ref || 'main' }}
ref: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.ref != '' && inputs.ref || 'main' }}
- uses: ./.github/actions/activate-conda

- name: Restore full multi-version docs site
Expand All @@ -92,7 +99,7 @@ jobs:
shell: bash
run: |
SITE_URL="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}"
TARGET_REF="${{ inputs.ref != '' && inputs.ref || 'main' }}"
TARGET_REF="${{ github.event_name == 'release' && github.event.release.tag_name || inputs.ref != '' && inputs.ref || 'main' }}"
SKIP_VERSION="main"
if [[ "${TARGET_REF}" == v* ]]; then
SKIP_VERSION="${TARGET_REF}"
Expand All @@ -105,7 +112,7 @@ jobs:
- name: Build docs site
shell: bash
run: |
TARGET_REF="${{ inputs.ref != '' && inputs.ref || 'main' }}"
TARGET_REF="${{ github.event_name == 'release' && github.event.release.tag_name || inputs.ref != '' && inputs.ref || 'main' }}"
pip install -e ".[gensim]" \
--extra-index-url http://pyp.open3dv.site:2345/simple/ \
--trusted-host pyp.open3dv.site \
Expand Down Expand Up @@ -144,7 +151,11 @@ jobs:
path: ${{ github.workspace }}/docs/build/html

deploy-manual-build:
if: github.event_name == 'workflow_dispatch' && inputs.artifact_run_id == ''
if: >
(github.event_name == 'workflow_dispatch' && inputs.artifact_run_id == '') ||
(github.event_name == 'release' &&
github.event.action == 'published' &&
startsWith(github.event.release.tag_name, 'v'))
needs: build-and-deploy
runs-on: ubuntu-latest
environment:
Expand Down
35 changes: 12 additions & 23 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,27 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/activate-conda
- name: Run tests
- name: Install test package
run: |
pip install -e ".[gensim]" \
--extra-index-url http://pyp.open3dv.site:2345/simple/ \
--trusted-host pyp.open3dv.site \
--extra-index-url https://download.blender.org/pypi/
echo "Unit test Start"
pip install pytest-xdist

- name: Run default tests
run: |
echo "Default test suite (GPU-marked tests are skipped)"
export HF_ENDPOINT=https://hf-mirror.com
pytest tests/docs -q --confcutdir=tests/docs
pytest tests

- name: Run GPU tests serially
run: |
echo "Dedicated GPU test suite"
export HF_ENDPOINT=https://hf-mirror.com
pytest tests --run-gpu -m gpu

release-build:
if: startsWith(github.ref, 'refs/tags/v')
needs: lint
Expand Down Expand Up @@ -204,24 +214,3 @@ jobs:
with:
name: python-distributions
path: dist/

release-publish:
if: startsWith(github.ref, 'refs/tags/v')
needs: release-build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/embodichain
permissions:
contents: read
id-token: write # PyPI Trusted Publishing

steps:
- name: (Release) Download distributions
uses: actions/download-artifact@v4
with:
name: python-distributions
path: dist/

- name: (Release) Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
Loading