Atomic Actions#
Atomic actions are the building blocks for automated robot motion generation. Each action encapsulates a complete, self-contained motion primitive — such as picking up an object or moving to a pose — that can be chained together to form complex manipulation workflows.
Design Overview#
The module is organized into three layers:
AtomicActionEngine ← orchestrates a sequence of actions
│
├── AtomicAction(s) ← each action plans one motion primitive
│ │
│ └── MotionGenerator ← low-level trajectory planner (IK + trajectory optimization)
│
└── SemanticAnalyzer ← resolves object labels → ObjectSemantics
Each action receives a target (object semantics or a pose tensor), runs its planning pipeline, and returns a joint trajectory. The engine threads the end state of each action as the start state of the next, then concatenates all trajectories into one contiguous sequence:
ObjectSemantics ──► AffordanceEstimation ──► AtomicAction.execute()
(label + geometry │
+ affordance ├─ IK solve
+ entity) ├─ Motion plan
└─ Gripper interpolation
│
AtomicActionEngine ◄─────────────── PlanResult ───────┘
(sequences actions, accumulates
full-robot trajectory)
Core Concepts#
ObjectSemantics describes an interaction target. It bundles:
geometry— mesh data (vertices, triangles) used for grasp annotationaffordance— how to interact with the object (e.g. antipodal grasp poses)entity— a live reference to the simulation object, so actions can read its current pose
Affordance is a data class that encodes a specific interaction capability. The built-in affordance types are:
Class |
Use case |
|---|---|
|
Parallel-jaw grasping via antipodal point pairs |
|
Contact-based interactions (push, poke, touch) |
AtomicAction is the abstract base class for all motion primitives. Every action must implement:
execute(target, start_qpos)— plan and return a joint trajectoryvalidate(target, start_qpos)— fast feasibility check without full planning
AtomicActionEngine manages a named registry of actions and runs them in sequence via execute_static(), threading the end state of each action as the start state of the next.
Built-in Actions#
The following actions are available out of the box:
Action |
Config class |
Target type |
Motion phases |
|---|---|---|---|
|
|
|
Move arm to pose |
|
|
|
Approach → close gripper → lift |
|
|
|
Lower → open gripper → retract |
MoveAction#
Moves the end-effector to a target pose in free space.
Config field |
Default |
Description |
|---|---|---|
|
|
Robot control part to move |
|
|
Number of waypoints in the trajectory |
Target: torch.Tensor of shape (4, 4) or (n_envs, 4, 4) — a homogeneous EEF pose.
PickUpAction#
Three-phase grasp motion: approach → close gripper → lift.
Config field |
Default |
Description |
|---|---|---|
|
|
Gripper approach direction in object frame |
|
|
Hover distance before descending (m) |
|
|
Lift height after grasping (m) |
|
|
Required. Gripper open joint positions |
|
|
Required. Gripper closed joint positions |
|
|
Robot control part for the gripper |
|
|
Waypoints for the gripper close phase |
|
|
Total waypoints across all three phases |
Target: ObjectSemantics (grasp pose computed automatically) or a torch.Tensor EEF pose.
PlaceAction#
Three-phase release motion: lower → open gripper → retract. Mirrors PickUpAction.
Inherits all gripper config fields from GraspActionCfg. The approach_direction field is not used — the arm moves straight down to the target pose.
Target: torch.Tensor of shape (4, 4) or (n_envs, 4, 4) — the EEF pose at release.
Typical Workflow#
from embodichain.lab.sim.atomic_actions import (
AtomicActionEngine,
ObjectSemantics,
AntipodalAffordance,
PickUpActionCfg,
PlaceActionCfg,
MoveActionCfg,
)
# 1. Configure each action
pickup_cfg = PickUpActionCfg(
control_part="arm",
hand_control_part="hand",
hand_open_qpos=torch.tensor([0.0, 0.0]),
hand_close_qpos=torch.tensor([0.025, 0.025]),
)
place_cfg = PlaceActionCfg(...)
move_cfg = MoveActionCfg(control_part="arm")
# 2. Build the engine — action order matches target_list order
engine = AtomicActionEngine(
motion_generator=motion_gen,
actions_cfg_list=[pickup_cfg, place_cfg, move_cfg],
)
# 3. Describe the object to pick
semantics = ObjectSemantics(
label="mug",
geometry={"mesh_vertices": ..., "mesh_triangles": ...},
affordance=AntipodalAffordance(object_label="mug", ...),
entity=mug,
)
# 4. Plan the full sequence and replay
is_success, traj = engine.execute_static(
target_list=[semantics, place_pose, rest_pose]
)
# traj: (n_envs, n_waypoints, dof)
How to Extend: Adding a Custom Action#
You can add any motion primitive by subclassing AtomicAction and registering it with the engine.
Step 1 — Define the config#
from embodichain.utils import configclass
from embodichain.lab.sim.atomic_actions import ActionCfg
@configclass
class PushActionCfg(ActionCfg):
name: str = "push"
push_distance: float = 0.05 # metres to push forward
push_speed: int = 30 # waypoints for the push phase
Step 2 — Implement the action#
import torch
from typing import Optional, Union
from embodichain.lab.sim.atomic_actions import AtomicAction, ObjectSemantics
from embodichain.lab.sim.planners import PlanState, MoveType
class PushAction(AtomicAction):
def __init__(self, motion_generator, cfg: PushActionCfg | None = None):
super().__init__(motion_generator, cfg=cfg or PushActionCfg())
self.arm_joint_ids = self.robot.get_joint_ids(name=self.cfg.control_part)
def execute(
self,
target: Union[torch.Tensor, ObjectSemantics],
start_qpos: Optional[torch.Tensor] = None,
**kwargs,
) -> tuple[bool, torch.Tensor, list]:
# Resolve target to a batched [n_envs, 4, 4] EEF pose
# ... your planning logic here ...
return is_success, trajectory, self.arm_joint_ids
def validate(self, target, start_qpos=None, **kwargs) -> bool:
return True # add IK check here if needed
Step 3 — Register and use#
from embodichain.lab.sim.atomic_actions import register_action
register_action("push", PushAction, PushActionCfg)
engine = AtomicActionEngine(
motion_generator=motion_gen,
actions_cfg_list=[PushActionCfg(push_distance=0.08)],
)
is_success, traj = engine.execute_static(target_list=[target_pose])
Tip: The
execute()return signature is always(is_success, trajectory, joint_ids).
trajectoryhas shape(n_envs, n_waypoints, len(joint_ids)).
joint_idstells the engine which columns of the full robot DOF vector the trajectory covers.
Target Resolution#
AtomicActionEngine accepts several target formats in target_list, giving you flexibility without boilerplate:
Input type |
Resolved to |
|---|---|
|
EEF pose, broadcast across envs |
|
Passed directly to the action |
|
Looked up in |
|
Unwrapped to tensor |
|
Analyzed via |
Further Reading#
MotionGenerator — the trajectory planner used by every action
Robot — how control parts and IK solvers are configured
Tutorial:
scripts/tutorials/sim/atomic_actions.py