Skip to content
Merged
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
13 changes: 13 additions & 0 deletions agent_context/topics/atomic-actions/atomic-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ ActionInvocation semantic program
Planning never steps simulation and never treats command completion as proof of
a physical effect.

`MoveHeldObject` accepts ordered object-pose waypoints without rebasing the
verified grasp. Its goal optionally frees only the final world yaw; default
goals remain exact. Heading alternatives are checked as complete motion paths,
with accepted rows retained independently and selected offsets in diagnostics.

`SlideGoal.joint_target` optionally supplies a public `SlideJointTarget` with an
absolute prismatic coordinate and a calibrated axis sign. Slide resolves the
remaining distance from same-timestamp joint/robot observations per planning row;
already-satisfied rows hold, invalid observations fail, and mixed active directions
are rejected. Omitting the target preserves fixed-distance `SlideOptions` and the
original Slide tutorial. Slide remains open-loop; this extension does not certify
contact or physical goal satisfaction.

## Package map

| Concern | Source of truth |
Expand Down
1 change: 1 addition & 0 deletions docs/source/api_reference/public_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,7 @@ embodichain.lab.sim.atomic_actions.control
GRASP_COMMAND
JointPositionCommand
OPEN_COMMAND
PARK_COMMAND

embodichain.lab.sim.atomic_actions.core
---------------------------------------
Expand Down
20 changes: 14 additions & 6 deletions docs/source/overview/sim/atomic_actions/builtin_actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,10 @@ 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.
`MoveJoints` is also the canonical implementation for home, recovery,
calibration, and other embodiment-named postures. Semantic callers should
constrain those uses through a registered call and keep the named target in the
robot profile.

## Shared goal and configuration rules

Expand All @@ -253,7 +254,11 @@ Explicit pose tensors use `(4, 4)` or `(B, 4, 4)`. Waypoint-capable fields in
`EndEffectorPoseGoal` and `PlaceGoal` also accept `(B, N, 4, 4)`.
`SceneEntityPose` resolves to the latest `(B, 4, 4)` pose from each
`SceneSnapshot`, checks optional perception confidence, and registers that
entity as a recovery dependency.
entity as a recovery dependency. `world_displacement` keeps a translation in
the world frame after local composition. `world_orientation` replaces the
tracked entity's rotation before applying `relative_pose`, allowing a target to
track a moving reference position while retaining a grounded world-frame
orientation.

| Skill / field | `SceneEntityPose` accepted | Automatic scene-motion replan |
|---|---:|---:|
Expand Down Expand Up @@ -379,7 +384,7 @@ than an EEF pose.
| Motion | joint planning/interpolation from observed qpos; supports joint waypoints |
| Completion | `JOINT_GOAL_REACHED` |
| Effect | none |
| Agent visibility | hidden by default (`agent_visible=False`) |
| Agent visibility | visible |

`target` accepts an explicit qpos tensor with shape `(control_dof,)`,
`(B, control_dof)`, or `(B, N, control_dof)`, or a non-empty string resolved
Expand Down Expand Up @@ -1020,6 +1025,7 @@ Both bound grasp endpoints must provide `open` and `grasp`. Important

- `pre_grasp_distance` and `lift_height`;
- `object_motion_keyframes`, `hand_interp_steps`, and `hold_steps`;
- `release`, `release_steps`, `retreat_distance`, and `retreat_steps`;
- `approach_direction`, `left_to_right_arm_direction`, and `middle_empty_ratio`
for affordance-based left/right grasp sampling.

Expand Down Expand Up @@ -1112,7 +1118,9 @@ 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
the approach/lift distances, gripper interpolation, hold, and retreat phases.
With `release_at_target=False`, the source hand opens and retreats while the
destination remains the verified holder for a later placement action. The first
placement waypoint changes only horizontal coordinates and preserves the
handover height exactly; the second waypoint lowers to the final target.

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 @@ -362,7 +362,7 @@ instances to the engine's planning services:
```python
engine = AtomicActionEngine(motion_generator, control_profiles=profiles)

# All eleven built-ins are immediately usable by stable skill ID.
# All twelve built-ins are immediately usable by stable skill ID.
assert "move_end_effector" in engine.actions
assert "pick_up" in engine.actions
```
Expand Down
8 changes: 6 additions & 2 deletions embodichain/lab/sim/atomic_actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
from .affordance import (
Affordance,
AntipodalAffordance,
AssembleAffordance,
AxisAlignAffordance,
AssembleAffordance,
InteractionPoints,
OpenDoorAffordance,
PressAffordance,
Expand All @@ -64,6 +64,7 @@
GRASP_COMMAND,
JointPositionCommand,
OPEN_COMMAND,
PARK_COMMAND,
)
from .core import AtomicAction, ObjectSemantics, SkillDescriptor
from .effects import StateDelta
Expand Down Expand Up @@ -214,6 +215,7 @@
PushObjectToolCalibration,
Slide,
SlideGoal,
SlideJointTarget,
SlideOptions,
Twist,
TwistGoal,
Expand Down Expand Up @@ -274,12 +276,12 @@
"ArticulationAffordanceGeometry",
"ArticulationGeometryProvider",
"ArticulationJointGeometry",
"AxisAlignAffordance",
"create_rigidized_articulation_antipodal_affordance",
"ArticulationJointState",
"AssembleAffordance",
"AssembleGoal",
"AxisAlign",
"AxisAlignAffordance",
"AxisAlignGoal",
"AxisAlignOptions",
"AtomicAction",
Expand Down Expand Up @@ -377,6 +379,7 @@
"OPEN_COMMAND",
"ObservationProvider",
"ObservedArticulationJointState",
"PARK_COMMAND",
"PickUp",
"PickUpOptions",
"Place",
Expand All @@ -402,6 +405,7 @@
"SlideAffordance",
"Slide",
"SlideGoal",
"SlideJointTarget",
"SlideOptions",
"Twist",
"TwistGoal",
Expand Down
4 changes: 4 additions & 0 deletions embodichain/lab/sim/atomic_actions/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
GRASP_COMMAND = "grasp"
"""Conventional semantic command for an object-holding end effector."""

PARK_COMMAND = "park"
"""Conventional semantic command for an embodiment-defined parked posture."""


class ControlCommand(ABC):
"""Immutable-by-ownership command associated with one control part.
Expand Down Expand Up @@ -282,4 +285,5 @@ def as_flat_mapping(
"GRASP_COMMAND",
"JointPositionCommand",
"OPEN_COMMAND",
"PARK_COMMAND",
]
8 changes: 7 additions & 1 deletion embodichain/lab/sim/atomic_actions/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
normalize_success_mask,
)
from .policies import DynamicCollisionMode
from .requirements import SkillBindingContract
from .requirements import GRASP_CAPABILITY, SkillBindingContract
from .runtime_commands import (
EndpointCommand,
JointPositionPayload,
Expand Down Expand Up @@ -745,6 +745,12 @@ def _tracking_sequence(
for command in frame.commands:
endpoints = endpoints_by_destination[command.destination_key]
for endpoint in endpoints:
# Contact intentionally prevents grasping endpoints from
# reaching their unobstructed close setpoint. Attachment
# and release are accepted by typed effect evidence; joint
# tracking remains authoritative for motion endpoints.
if GRASP_CAPABILITY in endpoint.capabilities:
continue
for channel_id in metrics_by_channel:
channel = endpoint.tracking_channels.get(channel_id)
if channel is None:
Expand Down
7 changes: 7 additions & 0 deletions embodichain/lab/sim/atomic_actions/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,13 @@ def _install_plan(

def _validate_phase_effect_gates(self, plan: ActionPlan) -> None:
"""Bind invocation-owned gates to non-initial named plan segments."""
if not plan.plan_success.any():
# A fully failed plan owns no executable phase boundary. Preserve
# its typed PlannerDiagnostics so ordinary retry/exhaustion logic
# can handle the failure; validating gate segment names against an
# empty failed-plan trajectory would replace that cause with a
# misleading preparation error.
return
request = self._requests[self._invocation_index]
for requirement in request.phase_effect_gates:
if type(requirement) is not PhaseEffectGateRequirement:
Expand Down
68 changes: 61 additions & 7 deletions embodichain/lab/sim/atomic_actions/goals.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ class SceneEntityPose:
minimum_confidence: float = 0.0
"""Minimum accepted perception confidence."""

world_displacement: torch.Tensor | None = None
"""Optional world-frame translation applied after ``relative_pose``."""

world_orientation: torch.Tensor | None = None
"""Optional world-frame orientation applied before ``relative_pose``."""

def __post_init__(self) -> None:
if not isinstance(self.entity_id, str) or not self.entity_id.strip():
raise ValueError("entity_id must be a non-empty string.")
Expand All @@ -57,6 +63,32 @@ def __post_init__(self) -> None:
allow_waypoints=False,
)
object.__setattr__(self, "relative_pose", self.relative_pose.clone())
if self.world_displacement is not None:
displacement = self.world_displacement
if not isinstance(displacement, torch.Tensor):
raise TypeError("world_displacement must be a torch.Tensor or None.")
if displacement.dim() not in (1, 2) or displacement.shape[-1] != 3:
raise ValueError(
"world_displacement must have shape (3,) or (num_envs, 3)."
)
if displacement.dim() == 2 and displacement.shape[0] == 0:
raise ValueError("world_displacement batches must not be empty.")
if not torch.isfinite(displacement).all():
raise ValueError("world_displacement must contain finite values.")
object.__setattr__(self, "world_displacement", displacement.clone())
if self.world_orientation is not None:
orientation = self.world_orientation
if not isinstance(orientation, torch.Tensor):
raise TypeError("world_orientation must be a torch.Tensor or None.")
if orientation.dim() not in (2, 3) or orientation.shape[-2:] != (3, 3):
raise ValueError(
"world_orientation must have shape (3, 3) or " "(num_envs, 3, 3)."
)
if orientation.dim() == 3 and orientation.shape[0] == 0:
raise ValueError("world_orientation batches must not be empty.")
if not torch.isfinite(orientation).all():
raise ValueError("world_orientation must contain finite values.")
object.__setattr__(self, "world_orientation", orientation.clone())
if not 0.0 <= self.minimum_confidence <= 1.0:
raise ValueError("minimum_confidence must be in [0, 1].")

Expand All @@ -70,6 +102,8 @@ def snapshot(self) -> SceneEntityPose:
self.entity_id,
relative_pose=self.relative_pose,
minimum_confidence=self.minimum_confidence,
world_displacement=self.world_displacement,
world_orientation=self.world_orientation,
)


Expand Down Expand Up @@ -154,14 +188,34 @@ def resolve_pose_goal(
raise ValueError(
f"Scene entity {value.entity_id!r} pose must match planning batch size."
)
if value.world_orientation is not None:
orientation = value.world_orientation.to(device=pose.device, dtype=pose.dtype)
if orientation.shape == (3, 3):
orientation = orientation.unsqueeze(0).expand(context.batch_size, -1, -1)
elif orientation.shape != (context.batch_size, 3, 3):
raise ValueError(
f"{name}.world_orientation must match planning batch size."
)
pose = pose.clone()
pose[:, :3, :3] = orientation
if value.relative_pose is None:
return pose.clone()
relative = value.relative_pose.to(device=pose.device, dtype=pose.dtype)
if relative.shape == (4, 4):
relative = relative.unsqueeze(0).expand(context.batch_size, -1, -1)
elif relative.shape != (context.batch_size, 4, 4):
raise ValueError(f"{name}.relative_pose must match planning batch size.")
return torch.bmm(pose, relative)
resolved = pose.clone()
else:
relative = value.relative_pose.to(device=pose.device, dtype=pose.dtype)
if relative.shape == (4, 4):
relative = relative.unsqueeze(0).expand(context.batch_size, -1, -1)
elif relative.shape != (context.batch_size, 4, 4):
raise ValueError(f"{name}.relative_pose must match planning batch size.")
resolved = torch.bmm(pose, relative)
if value.world_displacement is None:
return resolved
displacement = value.world_displacement.to(device=pose.device, dtype=pose.dtype)
if displacement.shape == (3,):
displacement = displacement.unsqueeze(0).expand(context.batch_size, -1)
elif displacement.shape != (context.batch_size, 3):
raise ValueError(f"{name}.world_displacement must match planning batch size.")
resolved[:, :3, 3] += displacement
return resolved


def _resolve_object_pose(
Expand Down
2 changes: 2 additions & 0 deletions embodichain/lab/sim/atomic_actions/primitives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
from .slide import (
Slide,
SlideGoal,
SlideJointTarget,
SlideOptions,
)
from .twist import Twist, TwistGoal, TwistOptions
Expand Down Expand Up @@ -124,6 +125,7 @@
"PushObjectToolCalibration",
"Slide",
"SlideGoal",
"SlideJointTarget",
"SlideOptions",
"Twist",
"TwistGoal",
Expand Down
13 changes: 13 additions & 0 deletions embodichain/lab/sim/atomic_actions/primitives/axis_align.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,19 @@ def _plan(
),
),
segment_lengths=segment_lengths,
# Contact during close/manipulate intentionally moves the aligned
# semantic object. Independent scene-referenced grasp targets
# remain monitored for the full action.
scene_dependency_monitor_until={
entity_id: max(
1,
math.ceil(
segment_lengths["approach"] * options.grasp_commit_fraction
),
)
for entity_id in self._scene_dependencies(request)
if entity_id == request.goal.semantics.entity_id
},
)

def _resolve_grasp_pose(
Expand Down
Loading
Loading