perceive_zone (optional — include only when the task names a
placement sub-region) — type: script, file
scripts/<sg>/perceive_placement_zone.py. Use the canonical script
as-is; do not re-emit a python: block for this
path. Returns {placement_zone_obb: OrientedBoundingBox | None}.
Hard rule on placement_description: like object_name in
perceiving-objects, this is a literal Python string — the natural
noun phrase describing where the held object should land, drawn
from this subgraph's description. It is a constant per subgraph
instance, NOT a binding. DO NOT write
Ref("in.placement_description") or any $ref; the
coordinator does not declare placement_description as a subgraph
input. Write the string directly.
The phrase should describe the placement REGION, not the named
reference object. Examples:
- Task: "Pick up the book and place it in the left compartment of
the caddy." →
"the left compartment of the caddy".
- Task: "Put the chocolate pudding to the left of the plate." →
"the area to the left of the plate".
- Task: "Put the ketchup in the top drawer of the cabinet." →
"the inside of the top drawer of the cabinet".
- Task: "Pick up the book and place it on top of the shelf." →
"on top of the cabinet shelf".
"perceive_zone": {
"type": "script",
"script": "scripts/<sg>/perceive_placement_zone.py",
"inputs": {
"placement_description": "the left compartment of the caddy",
"container_obb": Ref("in.container_obb"),
"container_mask": Ref("in.container_mask")
}
}
Both container_obb and container_mask are OPTIONAL but strongly
recommended:
container_mask filters DINO detections to those whose box center
falls inside the container (caddy/drawer/shelf), which removes
noise boxes elsewhere in the image and frees the limited
labeled-box slots for actual sub-region candidates.
container_obb lets the script reject candidate zones that drift
too far in XY from the container center (a common failure mode
when the VLM's chosen mask projects to background through bad
depth). It's also used to choose between the DINO and SAM3-text
paths — the candidate closer to the container center wins.
When the script returns None (zone not visible, low confidence,
sanity-check failure), the downstream compute_drop falls back to
the bare-container path automatically — no extra graph wiring
needed.
compute_drop — type: script, file
scripts/<sg>/compute_drop_pose.py. Use the canonical script as-is;
do not emit a python:scripts//compute_drop_pose.py
block — the bundle's canonical script is materialized into the
workflow directory automatically and re-emitting it overrides the
correct implementation with an LLM reimplementation.
Hard rule on parameter names: the canonical script's def run
signature is (ctx, container_obb, container_interior_obb=None, ee_pose_at_grasp=None, drop_clearance=0.05, approach_height=0.20, held_obb=None, ...). Bind the held object as held_obb, not
target_obb. Renaming held_obb → target_obb causes the runtime
to silently drop the value (extra kwargs are warned and discarded);
the script then falls into the no-held-geometry branch and the drop
pose is wrong by the held object's full height.
When the optional perceive_zone state is present, bind
container_interior_obb to its placement_zone_obb output so the
drop targets the named sub-region instead of the bare container
center:
"compute_drop": {
"type": "script",
"script": "scripts/<sg>/compute_drop_pose.py",
"inputs": {
"container_obb": Ref("in.container_obb"),
"held_obb": Ref("in.target_obb"),
"ee_pose_at_grasp": Ref("in.ee_pose_at_grasp"),
"container_interior_obb": Ref("perceive_zone.placement_zone_obb")
}
}
When perceive_zone is omitted (3-state flow), drop the
container_interior_obb line:
"compute_drop": {
"type": "script",
"script": "scripts/<sg>/compute_drop_pose.py",
"inputs": {
"container_obb": Ref("in.container_obb"),
"held_obb": Ref("in.target_obb"),
"ee_pose_at_grasp": Ref("in.ee_pose_at_grasp")
}
}
ee_pose_at_grasp is required for the LIBERO In(obj, region)
predicate to fire after release — the script uses it to convert the
desired held-object Z into a TCP target accounting for the panda
hand-to-tcp offset. The upstream grasping-with-planner subgraph
publishes it as a cross-subgraph output (produces_outputs.ee_pose_at_grasp);
this subgraph declares ee_pose_at_grasp in its required_inputs
so the coordinator wires the binding by name.
Returns drop_position (Vec3), drop_pose (Se3Pose), approach_pose
(Se3Pose).
1a. drop_offset — type: script, file
scripts/<sg>/drop_offset_pose.py. REQUIRED whenever the
upstream grasp was on a SUBPART (e.g. frypan handle, kettle
spout, bottle neck, tool grip). Omit ONLY when the grasp was on
the object's geometric centroid (parent_obb == held_obb case).
HOW TO TELL: the upstream perception subgraph is
perceiving-object-parts, OR the task description names the grasp
location explicitly ("grasp the pan by its handle", "lift it by
the spout"). In both cases the gripper closes on a subpart and
the parent body hangs off-axis — without drop_offset the
compute_drop script puts the grasp point at the placement
zone, which means the body lands off the support. This is the
dominant failure mode for handle-grasp-then-place-ON tasks: the
goal predicate reports pan.bottom_z << burner.top_z (pan
centroid below the support) and xy_coverage_over < 0.5 even
though the workflow itself returned placed.
Repair trigger. When repairing a transport subgraph that
failed with the above signature AND the upstream is
perceiving-object-parts, insert drop_offset even if a prior
iteration omitted it. Do not assume the absence of drop_offset
in the existing workflow is intentional; it is the single most
common omission.
Inputs:
drop_pose = Ref("compute_drop.drop_pose"),
ee_pose_at_grasp = Ref("in.ee_pose_at_grasp"),
held_obb = Ref("in.target_obb") (the grasped subpart OBB —
same as compute_drop.held_obb),
parent_obb = Ref("in.parent_obb") (the full object OBB — the
coordinator must declare parent_obb in this subgraph's inputs
and wire it from the perception subgraph's parent output, e.g.
perception_sg.parent_obb).
Returns drop_position, drop_pose, approach_pose — all shifted
in XY so the parent centroid lands at the original drop XY.
Downstream move_above / release then reference
drop_offset.drop_position instead of compute_drop.drop_position.