Refactor atomic_actions to typed targets + WorldState + composition#319
Refactor atomic_actions to typed targets + WorldState + composition#319yuecideng wants to merge 42 commits into
Conversation
Captures the review of embodichain/lab/sim/atomic_actions and the proposed typed-targets / WorldState / TrajectoryBuilder redesign. Co-Authored-By: Claude <noreply@anthropic.com>
13-task TDD plan implementing the design spec at docs/superpowers/specs/2026-06-21-atomic-actions-redesign-design.md. Co-Authored-By: Claude <noreply@anthropic.com>
Move Affordance, AntipodalAffordance, and InteractionPoints out of core.py into a dedicated affordance.py module. AntipodalAffordance now stores mesh_vertices, mesh_triangles, generator_cfg, gripper_collision_cfg, force_reannotate, and is_draw_grasp_xpos as typed dataclass fields rather than smuggling them through a shared geometry / custom_config dict. Remove the geometry-aliasing footgun from ObjectSemantics.__post_init__: it no longer assigns self.affordance.geometry = self.geometry, so an Affordance never silently inherits a mutable dict from its semantics parent. __post_init__ now only binds the affordance's object_label. Update actions.py, engine.py, and __init__.py imports to source the affordance types from the new module. Update test_core.py, test_engine.py, and test_actions.py to import Affordance from affordance, drop the duplicate Affordance/InteractionPoints tests now covered by test_affordance.py, and stop asserting the removed geometry alias. Co-Authored-By: Claude <noreply@anthropic.com>
…ngs) Co-Authored-By: Claude <noreply@anthropic.com>
… slim ABC Co-Authored-By: Claude <noreply@anthropic.com>
…en tests) Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
- MoveAction, PickUpAction, MoveObjectAction, PlaceAction all inherit AtomicAction directly. _HandCloseAction is removed. - Each holds a TrajectoryBuilder for shared helpers. - execute() takes a typed target + WorldState and returns ActionResult with a full-DoF trajectory. - Cfg classes are flat (no inheritance among Grasp/HandClose variants). Co-Authored-By: Claude <noreply@anthropic.com>
…rror types) Co-Authored-By: Claude <noreply@anthropic.com>
Drops SemanticAnalyzer, _resolve_target, _action_context, execute_static. Keeps the global register_action / unregister_action / get_registered_actions. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Refresh the module docstring to reflect the four current primitives (move/pick_up/move_object/place) and the typed-target / WorldState design. Export list was already correct from the incremental task updates. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Rewrite the skill to match the new API: sibling actions inheriting AtomicAction directly, TrajectoryBuilder composition, typed targets, WorldState/ActionResult contract, engine.register + run. Drops the old tuple-return / validate / _builtin_action_map / GraspActionCfg-parent guidance. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors embodichain.lab.sim.atomic_actions into a typed-target + WorldState + composition-based design, updating the engine API, the four built-in actions, and all related tutorials/docs/tests to match the new contract.
Changes:
- Replaces the old
Union/dict-DSL target handling + multi-channel held-state plumbing with typed targets (PoseTarget,GraspTarget,HeldObjectTarget) and explicitWorldState/ActionResult. - Introduces
TrajectoryBuilderas a shared composition helper and rewritesMove/PickUp/MoveObject/Placeas siblingAtomicActionimplementations. - Migrates tutorials, docs, and a rewritten pytest suite to the new
AtomicActionEngine.run([(name, target), ...])API.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
embodichain/lab/sim/atomic_actions/__init__.py |
Updates public exports for the redesigned atomic-actions API (targets/state/result/builder). |
embodichain/lab/sim/atomic_actions/core.py |
Defines typed targets, WorldState, ActionResult, and the slimmed AtomicAction ABC. |
embodichain/lab/sim/atomic_actions/affordance.py |
Moves affordance types into a dedicated module and reshapes AntipodalAffordance fields. |
embodichain/lab/sim/atomic_actions/trajectory.py |
Adds TrajectoryBuilder utilities for IK/FK, waypoint splitting, interpolation, etc. |
embodichain/lab/sim/atomic_actions/actions.py |
Reimplements MoveAction, PickUpAction, MoveObjectAction, PlaceAction using composition + WorldState. |
embodichain/lab/sim/atomic_actions/engine.py |
Replaces execute_static with run(steps, state) and removes the old target resolver/analyzer path. |
tests/sim/atomic_actions/test_affordance.py |
Adds tests for the new affordance module and anti-aliasing behavior. |
tests/sim/atomic_actions/test_trajectory.py |
Adds tests for TrajectoryBuilder helpers and shape/broadcasting contracts. |
tests/sim/atomic_actions/test_core.py |
Updates tests for typed targets, WorldState, ActionResult, and semantics behavior. |
tests/sim/atomic_actions/test_actions.py |
Rewrites action tests to assert full-DoF trajectories and threaded WorldState semantics. |
tests/sim/atomic_actions/test_engine.py |
Rewrites engine tests around name-keyed registration and run() sequencing semantics. |
scripts/tutorials/sim/atomic_actions.py |
Migrates tutorial to explicit action registration + engine.run() with typed targets. |
scripts/tutorials/atomic_action/pickup_atomic_actions.py |
New/updated tutorial showcasing PickUpAction using the new API. |
scripts/tutorials/atomic_action/move_object_atomic_actions.py |
New/updated tutorial showcasing MoveObjectAction with held-object state threading. |
skills/add-atomic-action/SKILL.md |
Updates scaffolding guidance to the new typed-target + WorldState + ActionResult contract. |
docs/source/overview/sim/atomic_actions.md |
Updates conceptual overview, supported actions table, and extension guide for the redesign. |
docs/source/tutorial/atomic_actions.rst |
Updates tutorial narrative and code snippets to the new engine/action interfaces. |
docs/source/api_reference/embodichain/embodichain.lab.sim.atomic_actions.rst |
Expands API reference to cover new targets/state/result/builder (but needs a small completeness tweak). |
docs/superpowers/specs/2026-06-21-atomic-actions-redesign-design.md |
Adds design spec describing the motivation and target architecture for the refactor. |
embodichain/lab/sim/solvers/pytorch_solver.py |
Adjusts default IK sampling count (tangential to atomic-actions refactor). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if qpos_seed is None: | ||
| qpos_seed = self.robot.get_qpos() | ||
| success, qpos = self.robot.compute_ik( | ||
| pose=target_pose.unsqueeze(0), | ||
| qpos_seed=qpos_seed.unsqueeze(0), | ||
| name=control_part, | ||
| ) |
| def apply_local_offset( | ||
| self, pose: torch.Tensor, offset: torch.Tensor | ||
| ) -> torch.Tensor: | ||
| """Apply a translational offset to a batched pose.""" |
| Affordance | ||
| InteractionPoints | ||
| ObjectSemantics |
| :members: | ||
| :show-inheritance: | ||
|
|
| def resolve_pose_target(self, target: torch.Tensor, *, n_envs: int) -> torch.Tensor: | ||
| """Broadcast a (4, 4) pose to (n_envs, 4, 4) or validate batched shape.""" | ||
| if not isinstance(target, torch.Tensor): | ||
| logger.log_error( | ||
| f"target must be torch.Tensor of shape (4, 4) or ({n_envs}, 4, 4)", | ||
| TypeError, | ||
| ) | ||
| if target.shape == (4, 4): | ||
| target = target.unsqueeze(0).repeat(n_envs, 1, 1) | ||
| if target.shape != (n_envs, 4, 4): | ||
| logger.log_error( | ||
| f"target tensor must have shape (4, 4) or ({n_envs}, 4, 4), " | ||
| f"but got {target.shape}", | ||
| ValueError, | ||
| ) | ||
| return target |
There was a problem hiding this comment.
Fixed in 5424e85. resolve_pose_target() now normalizes the target pose to the builder/robot device with float32 before broadcasting and shape validation, matching the downstream planning buffers.
| if not (pose.dim() == 3 and pose.shape[1:] == (4, 4)): | ||
| logger.log_error("pose must have shape [N, 4, 4]", ValueError) | ||
| if offset.dim() == 1: | ||
| offset = offset.unsqueeze(0) | ||
| if not (offset.dim() == 2 and offset.shape[1] == 3): | ||
| logger.log_error("offset must have shape [N, 3] or [3]", ValueError) | ||
| result = pose.clone() | ||
| result[:, :3, 3] += offset | ||
| return result |
There was a problem hiding this comment.
Fixed in 5424e85. apply_local_offset() now casts offset to the pose device/dtype and explicitly validates that the offset batch size is either 1 or matches the pose batch size, so invalid shapes fail with a clear ValueError.
| if self._generator is None: | ||
| self._init_generator() | ||
| results = [] | ||
| for i, obj_pose in enumerate(obj_poses): | ||
| is_success, grasp_poses, _, costs = self._generator.get_valid_grasp_poses( | ||
| obj_pose, approach_direction | ||
| ) |
There was a problem hiding this comment.
Fixed in 5424e85. get_valid_grasp_poses() now normalizes approach_direction to the grasp generator device with float32 before calling GraspGenerator, avoiding CPU/CUDA mismatches.
| if self._generator is None: | ||
| self._init_generator() | ||
| grasp_xpos_list: list[torch.Tensor] = [] | ||
| is_success_list: list[bool] = [] | ||
| open_length_list: list[float] = [] | ||
| for i, obj_pose in enumerate(obj_poses): | ||
| is_success, grasp_xpos, open_length = self._generator.get_grasp_poses( | ||
| obj_pose, approach_direction | ||
| ) |
There was a problem hiding this comment.
Fixed in 5424e85. get_best_grasp_poses() uses the same approach_direction normalization path before calling into the generator, so the default CPU tensor is moved to the generator device.
…/DexForce/EmbodiChain into refactor/atomic-actions-redesign
| if start_qpos is None: | ||
| start_qpos = self.robot.get_qpos(name=control_part) | ||
| if start_qpos.shape == (arm_dof,): | ||
| start_qpos = start_qpos.unsqueeze(0).repeat(n_envs, 1) | ||
| if start_qpos.shape != (n_envs, arm_dof): |
| if state is None: | ||
| state = WorldState(last_qpos=self.robot.get_qpos().clone()) | ||
|
|
| object_to_eef = state.held_object.object_to_eef.to( | ||
| device=self.device, dtype=torch.float32 | ||
| ) | ||
| if object_to_eef.shape == (4, 4): | ||
| object_to_eef = object_to_eef.unsqueeze(0).repeat(self.n_envs, 1, 1) | ||
| move_eef_xpos = torch.bmm(object_target_pose, object_to_eef) |
| See `Academic | ||
| Publications <docs/source/resources/publications/README.md>`__ for a | ||
| complete list of academic papers related to EmbodiChain. |
|
This update addresses four small cleanup items for the atomic action tutorials:
|
Description
Redesign the
embodichain/lab/sim/atomic_actions/abstraction layer from aUnion+kwargs+inheritance design to a typed-target /WorldState/ composition design. The full design rationale is indocs/superpowers/specs/2026-06-21-atomic-actions-redesign-design.md.This PR carries the atomic-actions motion-primitive module (
move,pick_up,move_object,place) in its redesigned form, plus the migrated tutorials, docs, and skill scaffold.What changed in the abstraction
PoseTarget/GraspTarget/HeldObjectTargetreplace the five-wayUnion+ dict/string_resolve_targetDSL. Each action declaresTargetType: ClassVar[type].WorldStatechannel — replaces the four-channel held-state passing (action_contextkwarg,held_object_statekwarg,self._held_object_state,get_held_object_state()+updates_held_object_stateClassVar).execute(target, state) -> ActionResultthreads state explicitly; the engine is a thin sequencer.MoveAction,PickUpAction,MoveObjectAction,PlaceActionall inheritAtomicActiondirectly and compose a statelessTrajectoryBuilder._HandCloseActionis gone;MoveActionis no longer a parent.AtomicActionEngine(motion_generator)+register(action)+run(steps=[(name, target), ...], state=None) -> (success, traj, final_state). Sequence order lives at the call site, not in registration order.execute_static/actions_cfg_list/SemanticAnalyzerdropped.ObjectSemantics.__post_init__no longer mutatesaffordance.geometry;AntipodalAffordancetakesmesh_vertices/mesh_triangles/gripper_collision_cfg/generator_cfgas direct fields.ActionResultwith a(n_envs, n_waypoints, robot.dof)trajectory; the per-calljoint_idsre-indexing is gone.validatedropped from the ABC (every prior implementation wasreturn True # TODO).Module layout
Verification
pytest tests/sim/atomic_actions/— 60 tests pass (rewritten test suite: core, affordance, trajectory, actions, engine).tests/sim/collection of the 172 non-atomic tests is clean (no cross-package breakage — only the 3 tutorials + 5 test files import the package).black --checkclean across the touched surface.execute_static,SemanticAnalyzer,_resolve_target,MoveObjectTarget,updates_held_object_state,_HandCloseAction,custom_configon affordances) anywhere outside the historical design spec/plan underdocs/superpowers/.Notes for reviewers
python scripts/tutorials/sim/atomic_actions.py --num_envs 1 --headless) should be done before merge.atomic_actionsAPI (the only consumers are the 3 tutorial scripts and the test suite, all migrated in this PR — no task envs, RL, or agent code imports the package, so there is no deprecation window needed).Type of change
Checklist
black .command to format the code base.🤖 Generated with Claude Code