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
5 changes: 5 additions & 0 deletions agent_context/MAP.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,11 @@ topics:
- whole body resource
- leaf resource claim
- SceneEntityPose
- PressGoal.target_pose
- SlideGoal.target_pose
- TwistGoal.target_pose
- open_loop
- axis_translation_keyframes
- dynamic goal
- error recovery
- ActionOptions
Expand Down
23 changes: 22 additions & 1 deletion agent_context/topics/atomic-actions/atomic-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,9 @@ Scene dependencies must match the poses each primitive actually consumes:
| `CoordinatedPickment` | Goal-owned target/initial `SceneEntityPose` values; the semantic `entity_id` only when `object_initial_pose` is omitted and semantic grounding supplies that pose. |
| `Place` | A `SceneEntityPose` in ordinary `xpos`; for `AssembleGoal`, `base_pose` when supplied. Omitting `base_pose` uses the deprecated live `AssembleAffordance.base_object_entity` fallback with no dependency. |
| `MoveHeldObject` | A `SceneEntityPose` in `object_target_pose`; current object orientation is derived from observed EEF pose plus verified `object_to_eef`, not a scene-object read. |
| `Press` | A `SceneEntityPose` in `xpos`. |
| `Press` | `PressGoal.target_pose` when it is a `SceneEntityPose`; affordance data is entity-free. |
| `Slide` | `SlideGoal.target_pose` when it is a `SceneEntityPose`; the local grasp mesh does not own the link. |
| `Twist` | `TwistGoal.target_pose` when it is a `SceneEntityPose`; affordance data is entity-free. |
| `CoordinatedPlacement` | `SceneEntityPose` values in the placing or support object target pose. |
| `HandOver` | No semantic-object scene dependency. It verifies stable attachment identity and derives current pose from held state; its middle/final option poses are tensors, and the reused `GraspGoal.grasp_xpos` field is ignored. |

Expand Down Expand Up @@ -554,10 +556,29 @@ the legacy core mapping.
| `move_held_object` | `HeldObjectPoseGoal` | manipulator/end effector `primary` |
| `place` | `PlaceGoal`, `AssembleGoal` | manipulator/end effector `primary` |
| `press` | `PressGoal` | manipulator/end effector `primary` |
| `slide` | `SlideGoal` | manipulator/end effector `primary` |
| `twist` | `TwistGoal` | manipulator/end effector `primary` |
| `coordinated_pickment` | `CoordinatedPickGoal` | `left`, `right` |
| `coordinated_placement` | `CoordinatedPlacementGoal` | `placing`, `support` |
| `hand_over` | `GraspGoal` | `source`, `destination` |

`PressAffordance`, `SlideAffordance`, and `TwistAffordance` contain only
target-local geometry and interaction semantics. Their goals own an explicit
`target_pose`, which may be a deterministic tensor snapshot or a late-bound
`SceneEntityPose`. Never put an `Articulation`, `RigidObject`, or live link pose
reader in these affordances.

`Press` and `Slide` use dense axis-aligned Cartesian targets for their contact
motion. The linear motion-generator path solves every output sample with IK;
it does not resample sparse IK endpoints in joint space. `Press` has a distinct
contact segment before penetration. `TwistAffordance.axis_origin` and
`twist_axis` together define the full 3D rotation axis.

These three motion-centric primitives declare `SkillDescriptor.open_loop=True`
and an empty `StateDelta`. Their completion means motion execution only, not
verified button actuation, grasp retention, or articulation travel. Applications
that need semantic completion must observe and verify those physical outcomes.

`GraspGoal.grasp_xpos` accepts an explicit pose tensor, a late-bound
`SceneEntityPose`, or `None` for affordance sampling. A `SceneEntityPose`
registers the referenced entity as a recovery dependency, allowing an executing
Expand Down
Binary file removed docs/source/_static/atomic_actions/press.gif
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ full-robot timed trajectory and uncommitted expected effects.
PlaceOptions
Press
PressOptions
Slide
SlideOptions
Twist
TwistOptions
CoordinatedPickment
CoordinatedPickmentOptions
CoordinatedPlacement
Expand All @@ -47,6 +51,8 @@ full-robot timed trajectory and uncommitted expected effects.
PlaceGoal
AssembleGoal
PressGoal
SlideGoal
TwistGoal
CoordinatedPickGoal
CoordinatedPlacementGoal

Expand Down Expand Up @@ -100,6 +106,22 @@ Press
:show-inheritance:
:exclude-members: __init__, copy, replace, to_dict

Slide
-----

.. automodule:: embodichain.lab.sim.atomic_actions.primitives.slide
:members:
:show-inheritance:
:exclude-members: __init__, copy, replace, to_dict

Twist
-----

.. automodule:: embodichain.lab.sim.atomic_actions.primitives.twist
:members:
:show-inheritance:
:exclude-members: __init__, copy, replace, to_dict

CoordinatedPickment
-------------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ embodichain.lab.sim.atomic_actions
PlaceGoal
AssembleGoal
PressGoal
PressOptions
PressAffordance
SlideGoal
SlideOptions
SlideAffordance
TwistGoal
TwistOptions
TwistAffordance
CoordinatedPickGoal
CoordinatedPlacementGoal
MoveEndEffector
Expand All @@ -88,6 +96,8 @@ embodichain.lab.sim.atomic_actions
MoveHeldObject
Place
Press
Slide
Twist
CoordinatedPickment
CoordinatedPlacement
HandOver
Expand Down
139 changes: 116 additions & 23 deletions docs/source/overview/sim/atomic_actions/builtin_actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
```{currentmodule} embodichain.lab.sim.atomic_actions
```

EmbodiChain ships nine built-in action implementations with stable skill IDs;
EmbodiChain ships eleven built-in action implementations with stable skill IDs;
`AtomicActionEngine` creates and registers a fresh instance of every built-in by
default. Applications select them by stable skill ID rather than registering
routine instances themselves.
`Place` additionally accepts an `AssembleGoal`, so assembly reuses the same
release primitive instead of introducing a tenth skill ID.
release primitive instead of introducing another skill ID.

All built-ins implement
`plan(request, context) -> ActionPlan`, where `request` is the engine-resolved
Expand Down Expand Up @@ -99,9 +99,21 @@ The animations below are the focused simulator demos under
:link: builtin-press
:link-type: ref

`press` · close, contact, and return
`press` · close, approach, press, and retract
:::

:::{grid-item-card} `Slide`
:link: builtin-slide
:link-type: ref

`slide` · grasped translation along a constrained axis
:::

:::{grid-item-card} `Twist`
:link: builtin-twist
:link-type: ref

<img src="../../../_static/atomic_actions/press.gif" alt="Press demo" width="480" style="max-width: 100%;" />
`twist` · grasped rotation about a configured axis
:::

:::{grid-item-card} `CoordinatedPickment`
Expand Down Expand Up @@ -140,12 +152,14 @@ The animations below are the focused simulator demos under
| `move_end_effector` | `EndEffectorPoseGoal` | manipulator `primary` | none | none | none |
| `move_joints` | `JointPositionGoal` | manipulator `primary` | named target only: command matching `target` | none | none |
| `pick_up` | `GraspGoal` | manipulator + end effector `primary` | primary: `open`, `grasp` | semantic object/entity | attach object to `primary` manipulator |
| `move_held_object` | `HeldObjectPoseGoal` | manipulator + end effector `primary` | primary: `grasp` | object held exclusively by `primary` | preserve attachment |
| `place` | `PlaceGoal`, `AssembleGoal` | manipulator + end effector `primary` | primary: `open`, `grasp` | an active attachment must be exclusive to `primary`; `AssembleGoal` requires one | detach object |
| `press` | `PressGoal` | manipulator + end effector `primary` | primary: `grasp` | none | none |
| `coordinated_pickment` | `CoordinatedPickGoal` | manipulator + end effector `left`, `right` | both: `open`, `grasp` | semantic object/entity | attach the shared object to both manipulators |
| `coordinated_placement` | `CoordinatedPlacementGoal` | manipulator + end effector `placing`, `support` | placing: `open`, `grasp`; support: `grasp` | two distinct objects, each held exclusively by its arm | optionally detach placing object; preserve support attachment |
| `hand_over` | `GraspGoal` | manipulator + end effector `source`, `destination` | both: `open`, `grasp` | object held exclusively by source arm | transfer attachment to destination arm |
| `move_held_object` | `HeldObjectPoseGoal` | manipulator + end effector `primary` | primary: `grasp` | object held by `primary` | preserve attachment |
| `place` | `PlaceGoal`, `AssembleGoal` | manipulator + end effector `primary` | primary: `open`, `grasp` | `AssembleGoal` requires an object held by `primary`; ordinary `PlaceGoal` has no planner-enforced attachment precondition | detach object |
| `press` | `PressGoal` | manipulator + end effector `primary` | primary: `grasp` | `PressAffordance` + target pose | open-loop motion; application verifies contact/actuation |
| `slide` | `SlideGoal` | manipulator + end effector `primary` | primary: `open`, `grasp` | `SlideAffordance` + link pose | open-loop motion; application verifies joint travel/grasp |
| `twist` | `TwistGoal` | manipulator + end effector `primary` | primary: `open`, `grasp` | `TwistAffordance` + target pose | open-loop motion; application verifies joint travel/grasp |
| `coordinated_pickment` | `CoordinatedPickGoal` | manipulator + end effector `left`, `right` | both: `open`, `grasp` | semantic object/entity | create coordinated attachment; clear individual attachments |
| `coordinated_placement` | `CoordinatedPlacementGoal` | manipulator + end effector `placing`, `support` | placing: `open`, `grasp`; support: `grasp` | one individually held object per arm | optionally detach placing object; preserve support attachment |
| `hand_over` | `GraspGoal` | manipulator + end effector `source`, `destination` | both: `open`, `grasp` | object held by source arm | transfer attachment to destination arm |

### Binding role meanings

Expand Down Expand Up @@ -199,7 +213,6 @@ entity as a recovery dependency.
| `MoveJoints.target` | no | no |
| `MoveHeldObject.object_target_pose` | yes | yes |
| `Place.xpos` | yes | yes |
| `Press.xpos` | yes | yes |
| `CoordinatedPickGoal.object_target_pose` / `object_initial_pose` | yes | yes |
| `CoordinatedPlacementGoal` placing/support poses | yes | yes |
| `PickUp.grasp_xpos` | yes | yes |
Expand Down Expand Up @@ -471,27 +484,107 @@ migration.

## `Press`

Plans **close hand -> move to contact pose -> return to the observed starting
arm qpos**. It is intended for button-like or contact interactions where the
arm should retreat along its planned path after reaching the target.
Plans **close hand -> approach target -> contact -> press along axis -> return
to the approach pose**. `PressAffordance` is entity-free and stores an explicit
target-local surface `press_position` and `press_axis`. `PressGoal.target_pose`
is either a pose snapshot or `SceneEntityPose`, which resolves through the
current `PlanningContext.scene` and participates in dynamic-goal recovery.

The contact, press, and retract segments use axis-aligned Cartesian keyframes;
each output sample is grounded with IK instead of being interpolated only in
joint space. The generated tool frame uses an adaptive reference axis and is a
right-handed orthonormal rotation even for vertical or oblique press axes.

| Contract | Value |
|---|---|
| Skill ID | `press` |
| Goal | `PressGoal(xpos=...)` |
| Goal | `PressGoal(semantics=..., target_pose=...)` |
| Binding | manipulator + end effector role `primary` |
| Motion | close, press, joint-space return |
| Effect | none; existing attachment state is unchanged |
| Dynamic target | explicit pose or `SceneEntityPose` |
| Motion | close, approach, contact, axis-constrained press, axis-constrained retract |
| Effect | explicitly open-loop; no physical button/contact effect is claimed |

The bound end-effector profile must provide `grasp`, while
`PressOptions.hand_interp_steps` controls the close interpolation. The arm and
hand control parts come from `ActionBinding`. Contact detection is not itself a
symbolic effect in the current action; applications that require force/contact
confirmation should verify it externally.
`PressOptions` controls hand-close interpolation, approach distance,
press distance, and an optional target-local `press_position`. An options-level
position overrides the affordance's explicit surface point. The bound
end-effector profile must provide `grasp`; the action keeps the gripper closed
for all arm-motion segments.

**Example:** `scripts/tutorials/atomic_action/press.py`

(builtin-slide)=

## `Slide`

Plans a grasped linear interaction for one articulation link. The entity-free
`SlideAffordance` stores the link-local grasp mesh, `translation_axis`, and
optional joint name/limits. `SlideGoal.target_pose` supplies the link pose as a
snapshot or `SceneEntityPose`. The positive axis direction means approach and
push/close; pull/open uses its negative direction. The affordance inherits
`AntipodalAffordance` and selects a grasp with `get_best_grasp_poses()`. The grasp
approach direction is the link-frame translation axis transformed by the
current link rotation.

With `direction="pull"`, the sequence is **approach -> reach -> close -> pull ->
open**. With `direction="push"`, it is **approach -> reach -> close -> push -> open
-> return**, where `return` moves the open gripper back to the original approach
pose.

| Contract | Value |
|---|---|
| Skill ID | `slide` |
| Goal | `SlideGoal(semantics=..., target_pose=...)` |
| Binding | manipulator + end effector role `primary` |
| Motion | pull: approach, reach, close, pull, open; push adds return to approach |
| Effect | explicitly open-loop; no articulation travel or grasp success is claimed |

`SlideOptions` controls `direction`, hand close/open
interpolation, approach distance, and translation distance. The link-frame
translation axis belongs to `SlideAffordance`; the bound end-effector profile
must provide `open` and `grasp`. Reach, pull/push, and push-return use
axis-aligned Cartesian samples rather than sparse joint-space endpoints.

**Example:** `scripts/tutorials/atomic_action/slide.py`
plans and replays a pull first, then replans a push from the drawer's measured
post-pull link pose.

(builtin-twist)=

## `Twist`

Plans **approach -> reach -> close -> twist -> open -> retract** for an
articulation link or a rigid object. The entity-free `TwistAffordance` stores an
explicit local `grasp_position`, `twist_axis`, and `axis_origin`, plus optional
joint name/limits. `TwistGoal.target_pose` supplies the grounded target pose.

The grasp frame's z-axis follows the world-transformed twist axis; an adaptive
reference completes a right-handed orthonormal frame. Twist keyframes rotate
around the full 3D axis defined by `axis_origin + twist_axis`, not implicitly
around the target link origin.

| Contract | Value |
|---|---|
| Skill ID | `twist` |
| Goal | `TwistGoal(semantics=..., target_pose=...)` |
| Binding | manipulator + end effector role `primary` |
| Motion | approach, reach, close, rotate about the target-local axis, open, retract |
| Effect | explicitly open-loop; no articulation travel or grasp success is claimed |

`TwistOptions` controls the pre-grasp distance, close/open interpolation,
Cartesian twist keyframes, and twist angle. The pre-grasp pose is offset along
the grasp pose's negative z-axis; the target-local twist axis belongs to
`TwistAffordance`.

`Twist` is intentionally a pure-rotation primitive. Thread pitch, coupled axial
translation, and regrasping are outside its contract; an `Unscrew` action should
model those behaviors separately.

For all three primitives, `SkillDescriptor.open_loop` is `True`. Trajectory
completion therefore means commanded motion completion only. Applications that
need semantic success must observe button/contact or articulation state and
verify it outside the side-effect-free planner.

**Example:** `scripts/tutorials/atomic_action/twist.py`

(builtin-coordinated-pickment)=

## `CoordinatedPickment`
Expand Down
2 changes: 1 addition & 1 deletion docs/source/overview/sim/atomic_actions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ instances to the engine's planning services:
```python
engine = AtomicActionEngine(motion_generator, control_profiles=profiles)

# All nine built-ins are immediately usable by stable skill ID.
# All eleven built-ins are immediately usable by stable skill ID.
assert "move_end_effector" in engine.actions
assert "pick_up" in engine.actions
```
Expand Down
2 changes: 1 addition & 1 deletion docs/source/overview/sim/planners/curobo_planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ assert result.success.all()
Single-arm MoveEndEffector is supported through the normal
`strategy="motion_gen"` route. MoveJoints can opt in to collision-aware
joint-space planning with `strategy="motion_gen"`; the action uses the planner
already owned by its MotionGenerator. Movement phases of PickUp, Place, Press,
already owned by its MotionGenerator. Movement phases of PickUp, Place,
and MoveHeldObject can use the same single-arm static-world route.

This first release intentionally has the following limits:
Expand Down
2 changes: 2 additions & 0 deletions docs/source/tutorial/atomic_actions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ Focused examples live under ``scripts/tutorials/atomic_action``:
* ``place.py``
* ``assemble.py``
* ``press.py``
* ``slide.py``
* ``twist.py``
* ``coordinated_pickment.py``
* ``coordinated_placement.py``
* ``hand_over.py``
Expand Down
28 changes: 28 additions & 0 deletions embodichain/data/assets/obj_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,20 @@ def __init__(self, data_root: str = None):
super().__init__(prefix, data_descriptor, path)


class MicrowaveOven(EmbodiChainDataset):
"""get_data_path("MicrowaveOven/microwave_oven.urdf")"""

def __init__(self, data_root: str = None):
data_descriptor = o3d.data.DataDescriptor(
os.path.join(EMBODICHAIN_DOWNLOAD_PREFIX, obj_assets, "MicrowaveOven.zip"),
"5672da2d5a888a12469d6277636646b0",
)
prefix = type(self).__name__
path = EMBODICHAIN_DEFAULT_DATA_ROOT if data_root is None else data_root

super().__init__(prefix, data_descriptor, path)


class PlasticTray(EmbodiChainDataset):
"""get_data_path("PlasticTray/plastic_tray.glb")"""

Expand All @@ -268,3 +282,17 @@ def __init__(self, data_root: str = None):
path = EMBODICHAIN_DEFAULT_DATA_ROOT if data_root is None else data_root

super().__init__(prefix, data_descriptor, path)


class Drawer(EmbodiChainDataset):
"""get_data_path("Drawer/model_split_links_with_inertials.urdf")"""

def __init__(self, data_root: str = None):
data_descriptor = o3d.data.DataDescriptor(
os.path.join(EMBODICHAIN_DOWNLOAD_PREFIX, obj_assets, "Drawer.zip"),
"3981636db1f4188146fce25d54084612",
)
prefix = type(self).__name__
path = EMBODICHAIN_DEFAULT_DATA_ROOT if data_root is None else data_root

super().__init__(prefix, data_descriptor, path)
Loading
Loading