Built-in atomic actions#
EmbodiChain ships thirteen 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 another skill ID.
All built-ins implement
plan(request, context) -> ActionPlan, where request is the engine-resolved
snapshot of an invocation revision. Constructors accept only optional typed
default *Options; the owning AtomicActionEngine supplies the shared motion
generator, trajectory builder, and control-part command profiles when it binds
the built-in catalog. Generic motion and recovery choices belong to the
invocation, and per-call primitive behavior belongs to skill_options.
Registration only installs an implementation. Whether a built-in is executable
for a particular call still depends on its SkillBindingContract, the selected
resource endpoints, semantic command profiles, and task-state preconditions.
Action Agent adapters must also honor agent_visible and filter by embodiment
capability.
Note
The current manipulation primitives consume semantic open and grasp
commands through the control-part command abstraction. The shipped command
implementation is JointPositionCommand. A dexterous hand may register
calibrated joint-position commands immediately; non-position hand policies or
multi-stage in-hand manipulation require additional command types and segments.
Visual catalog#
The animations below are the focused simulator demos under
scripts/tutorials/atomic_action/.
MoveEndEffectormove_end_effector · free-space EEF pose motion
MoveJointsmove_joints · explicit or named joint-space motion
PickUppick_up · approach, close, and lift
AxisAlignaxis_align · grasp, lift, and align an object-local axis
MoveHeldObjectmove_held_object · object-centric transport
Pourpour · rotate an already-held object about its local internal axis
PushObjectpush_object · contact and translate a free rigid object on its support plane
Placeplace · approach, release, and retract
Placeplace + AssembleGoal · base-relative placement
Presspress · close, approach, press, and retract
Slideslide · grasped translation along a constrained axis
OpenDooropen_door · sampled handle grasp and parent-hinge rotation

Twisttwist · grasped rotation about a configured axis
CoordinatedPickmentcoordinated_pickment · dual-arm shared-object pick
CoordinatedPlacementcoordinated_placement · align two held objects
HandOverhand_over · pick, transfer, place, and release with two arms
Capability matrix#
Skill ID |
Accepted goal |
Required endpoints |
Required profile commands |
Required task state |
Expected task effect |
|---|---|---|---|---|---|
|
|
|
none |
none |
none |
|
|
|
named target only: command matching |
none |
none |
|
|
|
|
semantic object/entity |
attach object to the |
|
|
|
|
unheld object with |
open-loop pick and align while retaining the grasp |
|
|
|
|
object held exclusively by the |
preserve attachment |
|
|
|
|
exclusively held object with |
preserve attachment; open-loop rotate and return |
|
|
|
|
free rigid object plus target support pose |
open-loop planar push; application validates the measured landing pose |
|
|
|
|
any active attachment must be exclusive to |
detach object |
|
|
|
|
|
open-loop motion; application verifies contact/actuation |
|
|
|
|
|
open-loop motion; application verifies joint travel/grasp |
|
|
|
|
|
open-loop motion; application verifies hinge travel/grasp |
|
|
|
|
|
open-loop motion; application verifies joint travel/grasp |
|
|
|
both grasp endpoints: |
semantic object/entity |
attach the shared object to both motion targets |
|
|
|
|
two distinct objects, each held exclusively by its motion target |
optionally detach placing object; preserve support attachment |
|
|
|
both grasp endpoints: |
both candidate motion targets unoccupied; unheld object with |
open-loop pick, transfer, place, and release |
Participant slot meanings#
Slots are action-local semantic participants declared by
SkillBindingContract. Each slot contains endpoint requirements such as
motion and grasp; the profile binder matches their capabilities and typed
commands to a robot resource, then adapters produce the generic
EndpointBinding values owned by ActionBinding.
Slot |
Used by |
Meaning |
|---|---|---|
|
Single-participant skills |
Principal participant for this invocation; it has no inherent left/right or default-robot meaning |
|
|
Two candidate participants; the action assigns the nearer one to pickup and the other one to receive |
|
|
Participants on whose sides the affordance samples left/right grasps |
|
|
Participant that aligns and optionally releases the placing object |
|
|
Participant that keeps holding and positioning the support object |
Each endpoint requirement declares an open capability set and optional typed
semantic commands. Intra-slot and inter-slot disjointness constraints express
physical compatibility without global arm/tool categories. The built-in
control-part adapter resolves current joint-backed endpoints through
Robot.control_parts; custom adapters may instead return mobile, whole-body, or
other runtime targets.
MoveJoints is intentionally agent_visible=False: it is useful for home,
recovery, calibration, and scripted postures, but is not exposed to an Action
Agent by default.
MoveEndEffector#
Plans a free-space motion for the bound primary.motion endpoint to reach one
EEF pose or an ordered set of pose waypoints.
Contract |
Value |
|---|---|
Skill ID |
|
Goal |
|
Binding contract |
|
Motion |
EEF planning from observed arm qpos; output expanded to full robot DoF |
Completion |
|
Effect |
none |
Skill options |
none; reusable motion choices are in |
Use an explicit pose for a fixed target, SceneEntityPose for a tracked target,
or (B, N, 4, 4) for intermediate waypoints. The action does not command an end
effector/hand resource.
Example: scripts/tutorials/atomic_action/move_end_effector.py
MoveJoints#
Plans directly in joint space. This is appropriate for known safe postures, homing, scripted recovery, or motions whose desired outcome is a qpos rather than an EEF pose.
Contract |
Value |
|---|---|
Skill ID |
|
Goal |
|
Binding contract |
|
Motion |
joint planning/interpolation from observed qpos; supports joint waypoints |
Completion |
|
Effect |
none |
Agent visibility |
hidden by default ( |
target accepts an explicit qpos tensor with shape (control_dof,),
(B, control_dof), or (B, N, control_dof), or a non-empty string resolved
from the bound primary.motion endpoint’s command profile. Named poses remain
embodiment knowledge without becoming separate goal types:
engine = AtomicActionEngine(
motion_generator,
control_profiles={
"left_arm": ControlPartCommandProfile.joint_positions(home=home_qpos),
},
)
explicit_goal = JointPositionGoal(target=home_qpos)
named_goal = JointPositionGoal(target="home")
Example: scripts/tutorials/atomic_action/move_joints.py
PickUp#
Plans approach -> close hand -> lift and declares the object attached to the bound motion target.
Contract |
Value |
|---|---|
Skill ID |
|
Goal |
|
Binding contract |
|
Precondition |
|
Effect |
write |
Verification |
the attachment effect must be verified during closed-loop execution |
grasp_xpos may be (4, 4), (B, 4, 4), or a SceneEntityPose. A scene
reference resolves the latest grasp pose and registers its entity as a recovery
dependency, so material target motion invalidates and replans an executing
PickUp while its approach segment is active. Once approach has been
dispatched, dependency monitoring stops: contact-, close-, and lift-induced
object motion must not be misclassified as an external target update. Tracking
and collision-world checks remain active independently. When grasp_xpos is
omitted and fixed_object_to_eef is configured, the action composes that
object-relative calibration directly with the observed object pose. This path
bypasses affordance sampling, rotate_upright, and grasp_frame_to_eef. Without
the fixed calibration, the action samples valid affordance grasps and evaluates
reachability. Both paths store the selected object_to_eef transform in the
expected held-object state so later object-centric skills can reuse it.
Set ObjectSemantics.entity_id to the same stable ID used by the scene
snapshot. PickUp resolves that object pose once per planning attempt, uses the
same tensor for grasp sampling, upright adjustment, and object_to_eef, and
automatically records the ID as a scene dependency. An explicit ID never falls
back to a live simulation entity when the snapshot entry is missing.
PickUp requires typed open and grasp commands on primary.grasp.
Important PickUpOptions fields:
Field |
Purpose |
|---|---|
|
Pre-grasp offset and world-frame approach direction |
|
Lift distance and close-segment discretization |
|
Closed-hand hold frames before lifting |
|
Fixed SE(3) calibration from canonical grasp frames to the robot TCP |
|
Optional task/robot-calibrated SE(3) grasp that bypasses affordance sampling when the goal has no explicit grasp |
|
Affordance region: currently |
|
Optional TCP approach-alignment filter |
|
Optional future reachability constraints used in grasp selection |
|
Optional orientation-selection behavior |
ObjectSemantics.entity without an ID is a deprecated compatibility path. Its
live pose does not create an automatic scene dependency.
Example: scripts/tutorials/atomic_action/pickup.py currently exercises the
deprecated entity-only fallback. For canonical snapshot grounding and moving
target recovery, see
scripts/tutorials/atomic_action/moving_target_recovery.py.
AxisAlign#
Executes approach -> reach -> close -> lift -> align while
grouping arm motion into two planner calls: the open-gripper approach phase
contains the pre-grasp and grasp targets, and the closed-gripper manipulate
phase contains the lift and alignment targets. The close segment is local
hand interpolation and does not call the motion generator.
Only the final aligned pose is sent to the planner; the alignment sample budget
controls trajectory resolution without expanding the rotation into one CuRobo
plan_pose call per intermediate orientation.
The object’s AxisAlignAffordance.internal_axis is expressed in the
object-local frame, while AxisAlignOptions.target_axis is expressed in the
world frame. The final alignment target applies the shortest rotation about
the lifted object’s origin so that
aligned_object_rotation @ internal_axis == normalized_target_axis, then
derives every end-effector keyframe through the fixed grasp transform.
Contract |
Value |
|---|---|
Skill ID |
|
Goal |
|
Binding |
manipulator + end effector role |
Precondition |
an |
Motion |
approach, grasp, lift, and rotate in place while retaining the grasp |
Effect |
explicitly open-loop; no final object-pose success is claimed |
An explicit grasp_xpos accepts the same pose forms as GraspGoal; omitting it
prefers valid antipodal grasps whose TCP y-axis is perpendicular to the object
rotation axis, using grasp cost as the tie-breaker. When a currently horizontal
object axis is aligned to world-up, the initial grasp orientation is pre-rotated
45 degrees opposite the alignment rotation. This reduces the arm’s table-side
sweep during upright manipulation. AxisAlignOptions extends PickUpOptions
with target_axis. Shared and per-environment target axes use shapes (3,)
and (B, 3) respectively. Zero or non-finite axes are
rejected, and exactly opposite axes use a deterministic 180-degree rotation
rather than an unstable cross-product direction.
Example: scripts/tutorials/atomic_action/axis_align.py provides
--alignment upright (align object-local X to world Z) and
--alignment horizontal_align (align object-local X to world Y).
MoveHeldObject#
Moves an already attached object to an object-frame target while keeping the
hand closed. The caller specifies the desired object pose, not an EEF pose;
the action derives target_object_pose @ object_to_eef from verified task state
and sends that exact EEF target to the motion planner. It does not replace the
requested orientation with an implicit transport orientation. A caller that
needs upright or tilted transport must encode that orientation in the object
target itself.
After a successfully accepted Task Program call, the execution layer reconciles an active held relation from the terminal object observation and forward kinematics when both are available. Consequently, later object-space calls use the measured attachment instead of indefinitely projecting the originally selected grasp.
Contract |
Value |
|---|---|
Skill ID |
|
Goal |
|
Binding contract |
|
Precondition |
a |
Motion |
single object-centric transport segment with closed-hand qpos |
Effect |
none; the existing attachment is preserved |
Dynamic target |
explicit pose or |
The bound primary.grasp endpoint must provide grasp. The participant’s
motion and grasp endpoints are selected through ActionBinding; generic timing
is explicit on the planner result or planning context, while trajectory
sampling remains in MotionPolicy. In a vectorized batch, rows
where another manipulator holds the same semantic object or live entity are
marked unsuccessful and held in place.
Example: scripts/tutorials/atomic_action/move_held_object.py
Pour#
Rotates the object already held by the bound primary manipulator. PourGoal
contains no object pose because the action consumes the verified
HeldObjectState created by PickUp. PourOptions contains only the signed
rotate_angle in radians.
The held object’s semantics must use AxisAlignAffordance. The action obtains
the current EEF pose from FK at the observed starting arm qpos, reconstructs
the current object pose using eef_pose @ inverse(object_to_eef), transforms
the affordance’s object-local internal_axis into world space, and applies the
requested rotation while keeping the object origin fixed. The resulting EEF
target is target_object_pose @ object_to_eef. A second target then returns to
the EEF pose observed by FK, thereby reversing the rotation by the same angle.
Both targets are submitted in one motion-generation call so a collision-aware
planner can chain the outbound and return legs.
Contract |
Value |
|---|---|
Skill ID |
|
Goal |
|
Options |
|
Binding |
manipulator + end effector role |
Precondition |
an exclusive |
Motion |
rotate by |
Effect |
none; the existing attachment is preserved |
Example: scripts/tutorials/atomic_action/pour.py compiles a horizontal
PickUp followed by Pour.
PushObject#
Pushes a free rigid object toward an object-space target on the target pose’s
support plane. PushObjectGoal owns the object’s semantic identity and an
explicit pose or late-bound SceneEntityPose. The action closes the configured
end effector, approaches a calibrated contact point from above, makes contact,
translates along the measured planar object-to-target direction, and retracts.
The primitive intentionally declares no symbolic placement effect. Contact and
sliding are open-loop physics interactions, so a task must use a measured
segment validator such as object_near_target before accepting a demonstration.
Object and target scene dependencies are monitored only through approach;
motion caused by the contact and push phases is therefore not misclassified as
an external dynamic-goal update.
Contract |
Value |
|---|---|
Skill ID |
|
Goal |
|
Binding |
manipulator + end effector role |
Precondition |
a free rigid object and a target pose whose local Z axis is the support normal |
Motion |
close, approach, contact, planar push, retract |
Effect |
none; verify the measured object pose at the task boundary |
PushObjectOptions owns contact distance, overshoot, approach/retract heights,
the object-local contact point, and an optional support-frame planar offset.
The support-frame override keeps corrective pushes on the same side even when
a thin object flips or yaws after first contact. completion_tolerance makes a
later corrective invocation return a hold trajectory when the latest measured
pose is already close enough. PushObjectToolCalibration can override the
contact transform and clearance for a bound control part, which keeps
asymmetric left/right tool geometry in the robot profile rather than in task
control code.
Place#
Plans approach/descend -> open hand -> retract. A multi-waypoint
PlaceGoal visits all supplied release waypoints in order and opens at the last
one.
Contract |
Value |
|---|---|
Skill ID |
|
Goal |
|
Binding contract |
|
State |
consumes the bound motion target’s attachment when present and exclusive |
Effect |
detach the object from the bound motion target |
Verification |
release must be verified during closed-loop execution |
Dynamic target |
explicit pose/waypoints or |
Set tcp_symmetry="z_roll_180" only if TCP x/y can be flipped while TCP z and
translation remain physically equivalent. The action selects the closer
orientation variant from the observed starting state and uses it consistently
across all waypoints. An ordinary PlaceGoal may still open an unattached
gripper, but it will not release one side of a shared multi-manipulator object.
The bound primary.grasp endpoint must provide open and grasp. Important
PlaceOptions fields:
Field |
Purpose |
|---|---|
|
Approach and retract height |
|
Open-segment discretization |
|
Open-hand hold frames before retracting |
|
Optional world-Z ceiling for approach/retract poses |
|
Fixed-orientation translation keyframes per segment |
|
Keep the observed object orientation while using the target translation |
Example: scripts/tutorials/atomic_action/place.py
Assembly through Place#
Place also accepts
AssembleGoal(affordance=..., base_pose=SceneEntityPose("base")). There is no
separate assembly skill: it derives the assemble-object target from the base
object’s snapshot pose and reuses the normal place/release segments.
base_object_pose @ assemble_to_base_pose = assemble_object_target_pose
assemble_object_target_pose @ held.object_to_eef = release_eef_pose
The AssembleAffordance stores the relative assembly pose. A prior verified
PickUp must have populated the held object’s object_to_eef transform, and
that attachment must be exclusive. base_pose is resolved from each planning
snapshot and automatically becomes a recovery dependency. Omitting it
temporarily falls back to the affordance’s base_object_entity with a
deprecation warning; that fallback is not a scene dependency. Planning declares
the same detach effect as a normal place.
Example: scripts/tutorials/atomic_action/assemble.py currently exercises
the legacy base_object_entity fallback and is not the canonical base_pose
form. It remains a compatibility example until the registry-backed tutorial
migration.
Press#
Plans close hand -> approach target -> contact -> press along axis -> return
to the approach pose. For an articulation link,
sample_initial_articulation_geometry() builds typed target-link-local target
and whole-articulation geometry. Its to_object_geometry() conversion stores
the target, complete-articulation, and non-target-articulation clouds plus the
nearest parent prismatic joint axis in
ObjectSemantics.geometry.
PressAffordance uses target_link_prismatic_joint_axis for its axis direction;
only nearby non-target link surfaces select the sign. It can also derive the
outer-surface press_position when it is omitted. If no non-target surface lies
in the target neighborhood, direction is ambiguous and must be configured
without automatic axis inference. 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 |
|
Goal |
|
Binding contract |
|
Motion |
close, approach, contact, axis-constrained press, axis-constrained retract |
Effect |
explicitly open-loop; no physical button/contact effect is claimed |
Dynamic target |
explicit pose or |
PressOptions controls hand-close interpolation, approach distance,
press distance, and an optional target-local press_position. An options-level
position overrides the affordance’s resolved surface point. The bound
primary.grasp endpoint must provide grasp; both endpoints come from the
generic ActionBinding, and the action keeps the gripper closed for all arm
motion segments. Applications that require force/contact confirmation must
verify it externally.
Example: scripts/tutorials/atomic_action/press.py
Slide#
Plans a grasped linear interaction for one articulation link. The entity-free
SlideAffordance stores the link-local grasp mesh and resolves its
translation_axis from initial articulation point-cloud geometry, plus
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 resolved link-frame translation axis
transformed by the current link rotation.
The Atomic Action articulation-geometry adapter samples the target link, the
merged articulation surface, and the merged surface of every non-target link at
ArticulationCfg.init_qpos, expressed in the target link’s initial local frame.
Every non-empty surface cloud uses Open3D uniform sampling; a target-only
articulation records an explicit empty non-target cloud. Sampling each merged
mesh preserves triangle-area weighting instead of giving every link an equal
point budget, which would over-represent tiny decorative links.
To keep the target and merged clouds geometrically consistent, every non-empty
link mesh must contain at least one non-degenerate triangle surface; the adapter
rejects vertices-only and fully degenerate link meshes before merging.
The sampler also walks the target’s parent chain and transforms the nearest
prismatic joint axis into the target link’s initial local frame. The target
cloud center and twice its distribution radius define a spherical neighborhood
in the non-target cloud. The dot product between that neighborhood-center
offset and the normalized joint axis selects its sign. Target-link samples from
the complete cloud never contribute direction evidence, so independent Open3D
sampling cannot randomly flip the axis when no other link is nearby. It does
not quantize an oblique joint axis to a Cartesian basis direction. A missing or
empty non-target neighborhood, or an axial offset that does not exceed both one
percent of the target radius and four estimated standard errors, is rejected as
directionally ambiguous.
Automatic resolution requires all four entries: target_link_point_cloud,
articulation_point_cloud, non_target_articulation_point_cloud, and
target_link_prismatic_joint_axis. With none of those inference entries, an
explicit compatibility axis is preserved; missing provenance or another
partial set is rejected.
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 |
|
Goal |
|
Binding contract |
|
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 primary.grasp
endpoint 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.
OpenDoor#
Plans approach -> reach -> close -> open -> release -> retract for a door
handle. Construct OpenDoorAffordance with
OpenDoorAffordance.from_articulation(articulation, link_name). Starting at the
configured handle link, the factory consumes
Articulation.get_parent_joint_chain(), skips only fixed intermediates, and
automatically selects the hinge only when the chain has one active revolute
ancestor. A prismatic ancestor, revolute latch/handle joint, or any other
multi-active chain is ambiguous and requires hinge_joint_name. The selected
axis and origin are converted into the handle-link frame without exposing
native simulator joint-info objects to the affordance. The affordance owns the
joint-coordinate opening direction: it defaults to increasing qpos, while
reverse-coordinate hinges pass opening_direction=-1 to the factory. It
stores only local geometry, the handle mesh, resolved joint name, limits, and
opening direction; it does not retain the live articulation.
The action infers the positive-opening approach direction from the hinge axis
and the hinge-to-handle radial vector, then samples a handle grasp as Slide
does. OpenDoorGoal.open_fraction is an absolute semantic target: 0 maps to
the affordance-owned closed legal endpoint and 1 maps to its open endpoint,
including hinges whose opening direction decreases joint position. At planning
time, the resolved joint name must uniquely match a live
SceneSnapshot.articulation_joints observation. Each environment rotates only
by target_position - observed_position; invalid observations, out-of-range
targets, and targets that would move toward closing fail that row, while rows
already at the target succeed with a hold trajectory.
For active rows, the opening segment interpolates handle-link poses around the
resolved hinge axis and applies the initial rigid link -> EEF transform to
recover the corresponding EEF poses. After release, retract follows the
approach direction after it has rotated with the open door.
Contract |
Value |
|---|---|
Skill ID |
|
Goal |
|
Binding contract |
|
Motion |
approach, reach, close, hinge arc, release, rotated-direction retract |
Effect |
explicitly open-loop; no hinge travel or grasp success is claimed |
OpenDoorOptions controls hand close/open interpolation, circular-arc
keyframes, approach/retract distances, and joint-position comparison
tolerance. The desired opening state belongs to OpenDoorGoal, not the
planner options. The bound primary.grasp endpoint must provide open and
grasp. The planner reserves at least one opening-segment sample for the
segment start plus one for every configured door-arc keyframe; the full motion
policy therefore needs
sample_count >= 2 * hand_interp_steps + door_waypoint_count + 7.
Example: scripts/tutorials/atomic_action/open_door.py configures only the
microwave’s door_handle link. Automatic traversal resolves door_hinge
through the intermediate fixed joint. Its absolute --open_angle value is
normalized against the resolved hinge limits and passed as the goal’s
open_fraction.
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, plus optional joint name/limits. For an
articulation link, the same initial target-neighborhood geometry used by
Slide and Press signs the normalized target_link_revolute_joint_axis to
resolve twist_axis. The independent
target_link_revolute_axis_origin geometry entry sets axis_origin to the
nearest parent revolute joint’s initial origin, expressed in the target link’s
initial local frame. Resolution walks through fixed parent joints. The sampled
target-link centroid is only a neighborhood/contact reference; it is never used
as the rotation origin.
Automatic axis resolution requires the revolute joint-axis entry plus the
target, complete-articulation, and non-target-articulation clouds. When all four
axis-inference entries are absent, the legacy twist_axis fallback is
preserved; missing non-target provenance or another partial set is rejected.
Axis-origin resolution is independent, so missing origin metadata does not
overwrite an explicit axis_origin. If neither source supplies an origin,
planning reports the missing rotation-axis point. A rigid object without
articulation context may still provide explicit compatibility values.
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 |
|
Goal |
|
Binding contract |
|
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. The bound primary.grasp endpoint must provide open and
grasp.
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 four 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
CoordinatedPickment#
Coordinates two arms around one shared object: approach both grasps -> close both hands -> lift -> move object -> hold.
Contract |
Value |
|---|---|
Skill ID |
|
Goal |
|
Binding contract |
disjoint |
Precondition |
an |
Goal geometry |
shared-object target pose and optional initial object pose; left/right grasps are sampled from the affordance |
Effect |
write one |
Verification |
coordinated attachment must be externally verified |
The left/right grasp poses are not supplied by the caller. At planning time the
action calls AntipodalAffordance.get_dual_arm_valid_grasp_poses with the
approach_direction, left_to_right_arm_direction, and middle_empty_ratio
options to partition the object into left/right grasp regions and select the
lowest-cost grasp on each side. Each derived object_to_eef transform is stored
in the corresponding projected HeldObjectState. Later object-centric skills
can inspect those per-manipulator entries directly; sharing the same
ObjectSemantics instance identifies the common object. Single-arm transport
and release skills reject those shared rows rather than moving or detaching
just one participant. The unified HandOver action starts before pickup and
therefore requires both candidate arms to be unoccupied.
The object target and optional initial pose may use SceneEntityPose. Those
references declare their own scene dependencies. When object_initial_pose is
omitted, the action grounds the initial pose from
ObjectSemantics.entity_id and declares that ID as a dependency; the deprecated
no-ID entity fallback is live and therefore cannot trigger scene-motion
replanning. Supplying object_initial_pose disables this implicit semantic
dependency because the explicit pose value is authoritative.
Both bound grasp endpoints must provide open and grasp. Important
CoordinatedPickmentOptions fields group into:
pre_grasp_distanceandlift_height;object_motion_keyframes,hand_interp_steps, andhold_steps;approach_direction,left_to_right_arm_direction, andmiddle_empty_ratiofor affordance-based left/right grasp sampling.
The left/right motion and grasp endpoints come exclusively from the
corresponding participant slots. Coordinated dual-arm planning with
strategy="motion_gen" is not
supported by the cuRobo backend; use the supported IK/interpolation path for
this primitive.
Example: scripts/tutorials/atomic_action/coordinated_pickment.py
CoordinatedPlacement#
Moves a support object and a placing object together: align both objects -> hold -> optionally release the placing hand -> retreat the placing arm.
Contract |
Value |
|---|---|
Skill ID |
|
Goal |
|
Binding contract |
disjoint |
Precondition |
each bound motion target exclusively holds a different object |
Goal geometry |
placing/support object target poses, optional height offsets, optional release override |
Effect |
preserve support attachment; remove or preserve placing attachment according to |
Both object targets may use SceneEntityPose, so either can participate in
dynamic-goal invalidation. Goal-level height/release values override
CoordinatedPlacementOptions for that invocation. Two entries that identify
the same semantic object or live entity are a shared grasp, not a placing and
support pair, and their environment rows are rejected.
The placing.grasp endpoint must provide open and grasp; support.grasp
must provide grasp. Important CoordinatedPlacementOptions fields group into:
default
release, placing/support height offsets, andlift_height;hand_interp_steps,hold_steps, andretreat_steps.
The placing/support motion and grasp endpoints come exclusively from the
corresponding participant slots. The same cuRobo restriction as coordinated
pickment applies to dual-arm strategy="motion_gen" planning.
Example: scripts/tutorials/atomic_action/coordinated_placement.py
HandOver#
Runs the full two-arm manipulation as one action: choose the nearer arm -> grasp the nearer end of the object’s longest axis -> lift and move to the computed middle point -> the other arm grasps the opposite end -> transfer the grasp -> place and release.
Contract |
Value |
|---|---|
Skill ID |
|
Goal |
|
Binding contract |
disjoint |
Precondition |
both candidate arms start unoccupied; object semantics use |
Effect |
none; both grippers are open after placing the object |
Verification |
open-loop physical pickup, transfer, placement, and release |
Both grasp endpoints must provide open and grasp. The two binding slots
are candidate motion/grasp pairs rather than a caller-selected transfer
direction. For every environment, the action compares the observed object
position with both configured solver root-link positions. The nearer arm picks
up the object; the other arm receives it.
To keep both grasps spatially separated, HandOver deterministically samples at
most 1000 points from the object’s triangle-mesh surface, transforms them by
the current object pose, and uses SVD to find the widest distribution direction
(obj_longest_axis). The handover arm selects the projected end nearest its
current TCP, and the receiving arm selects the opposite end. Grasp generation
receives this world-frame axis plus is_positive_part; a None axis retains
the ordinary unpartitioned center behavior for other callers.
When the longest axis is within 45 degrees of world Z, HandOver uses vertical mode: each approach’s horizontal projection points from the acting arm’s current TCP toward the corresponding object position and is tilted downward by 45 degrees. Otherwise it uses horizontal mode and both approaches are world-Z downward. Pickup uses the observed object position, while receiving uses the predicted middle object position because that pose is not observed again during open-loop planning.
After pickup, the action lifts in world Z and computes the middle object pose
by finding the root-link separation’s largest-magnitude coordinate and setting
only that object coordinate to the two roots’ midpoint. From the first grasp
through middle transfer, and from the receiving grasp through final lowering,
EEF waypoint rotations remain fixed; only translations change. The final
object translation comes from HandOverGoal.target_pose, while its execution
orientation stays consistent with the handover grasp. HandOverOptions owns
only the approach/lift distances and gripper interpolation count. The first
placement waypoint changes only horizontal coordinates and preserves the
handover height exactly; the second waypoint lowers to the final target.
Planning failures are reported with semantic waypoint names and affected environment IDs. For a failed motion phase, HandOver diagnoses each target with IK; if all target waypoints are reachable, the report identifies the interval between them as a likely path- or collision-planning failure.
As with the other coordinated primitive, cuRobo does not currently support its
dual-arm strategy="motion_gen" path.
Example: scripts/tutorials/atomic_action/hand_over.py
Running the demos#
Every focused script is interactive by default. Add --auto_play to skip
keyboard prompts and combine it with --headless --device cpu for a headless
run that records video under outputs/videos:
python scripts/tutorials/atomic_action/move_end_effector.py --headless --auto_play --device cpu
python scripts/tutorials/atomic_action/pickup.py --headless --auto_play --device cpu
python scripts/tutorials/atomic_action/hand_over.py --headless --auto_play --device cpu
See Atomic actions for engine setup, static compilation, closed-loop execution, effect verification, and custom-action guidance.