Anchors
A skill targets a named frame on an object, not a coordinate. Anchors are what make a skill reusable across objects, worlds, and benches.
A skill that says "move to [0.412, -0.088, 0.235]" works exactly once. Nudge the plate two centimetres, rebuild the world, or run the same protocol on a second bench, and the numbers are wrong.
A skill that says "go to the anchor called A1 on the object I was handed" works for any plate, anywhere on the bench, in any world. The pose is resolved at run time from where that object actually is.
That is the whole idea. A skill targets a named frame on an object, not a coordinate. Anchors are those named frames.
What an anchor is
An anchor is a named pose rigidly attached to a link in the object's URDF. It lives in the object's object_model.yaml, alongside the object's articulations and parts.
Three fields are required on every anchor:
| Field | What it is |
|---|---|
parent_link | The URDF link the anchor is attached to. It must exist in the URDF. |
description | Free text. What the anchor represents, and which frame convention it follows. |
link_T_anchor | The transform from parent_link to the anchor: xyz in metres, and wxyz as a scalar-first quaternion. |
The quaternion is normalized when the file loads, so you do not have to write it to full precision. A quaternion whose norm is near zero is rejected rather than silently repaired.
When an object is loaded, each anchor is injected into a copy of the URDF as a massless link joined to parent_link by a fixed joint. The URDF on disk is not modified. This is why an anchor moves correctly with an articulated object: it is part of the kinematic tree, so forward kinematics carries it along when a lid opens or a rotor turns.
The object anchor
object anchorEvery object model must define an anchor named object. Loading fails without it.
It is the object's own reference frame. When the object model loads, a new root link is inserted above the URDF's existing root so that the URDF origin coincides with the object anchor. Everything downstream (placing the object in a world, snapping it to a measured pose, reporting where it is) speaks in terms of that frame rather than whatever arbitrary origin the mesh happened to be exported with.
Pick it to be the frame a person would naturally use to describe where the object sits. For something that rests on a bench, that is usually its placement footprint.
Frame conventions
XYZ in metres and scalar-first quaternions always hold. Which way +Z points does not. It depends on what the anchor is for:
| Convention | +Z is | Typical use |
|---|---|---|
world | Up. Gravity points along -Z. | Anchors defined in a fixed world-relative orientation, such as support surfaces. |
tcp | The approach direction, along the tool body and into the grasp. | Grasp targets. The pre-grasp pose retracts along -Z. |
camera | The optical axis, pointing toward the scene (OpenCV convention: +X right, +Y down). | Camera target poses. |
look_at | The preferred viewing direction, outward from the target surface. | A thing to be looked at. Not a camera pose itself. |
Nothing enforces these at run time. The convention list is documentation. No loader, validator, or viewer checks that a grasp anchor's +Z actually points into the grasp. That is why the anchor's
descriptionis expected to name the convention it means. Get it wrong and the gripper approaches from the opposite direction, which is a collision, not an error message.
So: read the description before you use an anchor, and write one when you author an anchor. Lead with the convention. A camera anchor whose description opens with "Camera view (OpenCV convention) looking down at ..." tells the next reader everything they need before they get to the numbers.
Choosing and naming anchors
Name the anchor after the physical feature, not after the motion you happen to be performing on it today. Then skill code reads like the lab protocol.
Plate wells are the clearest case. Well anchors are named for the well itself, uppercase, with no prefix: a 96-well plate carries A1 through H12, every one of them parented to the plate's body link. A workflow step that reads "dispense into A1" needs no translation layer.
The same principle generalises:
- Name a hole
hole_1, notleft_target. - Name the point you look at
view_rotor, notcamera_pose_2. - If a feature has one obvious grasp and one obvious approach, the anchor is the feature. If it has two genuinely different grasps, that is two anchors, distinguished by how they differ (short side, long side) rather than by which arm you expect to use.
A second rule falls out of validation: an anchor name must not collide with a URDF link name. An object with a link called body cannot have an anchor called body.
Optional blocks
Beyond the three required fields, an anchor can carry any combination of four optional blocks. Add one when the anchor needs to say something the pose alone cannot.
grasp
graspAdd it when the anchor is somewhere a gripper closes.
| Key | Notes |
|---|---|
width | Required. Gripper opening at the grasp point, in metres. |
standoff | Distance retracted along -Z for the pre-grasp pose. Defaults to 0.05. |
gripper_variant | Free-form label for which gripper the grasp was authored against. Defaults to "stock". |
These travel with the object, which is the point. A skill reads the width and the standoff off the anchor instead of hardcoding numbers that are only true for one piece of labware.
viewpoint
viewpointAdd it when you want cameras positioned around the anchor rather than at it. It describes a spherical distribution: r_min and r_max in metres, a polar range, and an optional azimuth centre and spread. Angles are written in degrees in the YAML and converted to radians on load, so a file that says polar_max_deg: 45 means 45 degrees.
prior_weights and sampling
prior_weights and samplingBoth feed pose fitting. prior_weights is a penalty matrix in the anchor's twist coordinates that says which directions of error you care about. sampling seeds multi-start ICP with a covariance and a num_samples. Each is written either as a 6-element diagonal shorthand or as a symmetric 6x6 nested list, with twist ordering [rot(3), trans(3)].
You will rarely author these by hand. They come from the catalog or from the annotation tooling.
How a skill consumes an anchor
At run time, a skill resolves an anchor name against a live world object and gets back a world-frame pose. That is load_object_anchor, exported from execution.execution_functions:
load_object_anchor(object_name, anchor_name, index=None, joint_config=None) -> dictIt returns xyz, rpy, wxyz, standoff, width, gripper_variant, and object_id. When the object has no grasp block on that anchor, standoff and width come back as 0.0 and gripper_variant as "stock".
The object argument is the world instance. A skill receives one as a parameter annotated SkillObject and passes object.id:
from protocol_schema import SkillObject
from .modules import anchor_preapproach, load_object_anchor, move_arm, set_gripper
def pick_from_anchor(target: SkillObject, anchor: str, speed: float = 40.0):
"""Pick the target object at one of its grasp anchors.
Args:
target: Object to pick.
anchor: Grasp anchor name on that object.
speed: Approach speed.
"""
grasp = load_object_anchor(target.id, anchor)
pre = anchor_preapproach(grasp, default_standoff=0.15)
move_arm(arm="right_arm", position=pre, orientation=grasp["rpy"], speed=speed)
move_arm(arm="right_arm", position=grasp["xyz"], orientation=grasp["rpy"], speed=10)
set_gripper(arm="right_arm", width_m=grasp["width"])
return {"success": True}Nothing in that function names an object, a world, or a coordinate. The object comes from the workflow binding, the anchor name comes from the step, and the geometry is resolved when the skill runs. Point it at a different plate and it still works.
anchor_preapproach is the companion: it backs off along the anchor's local -Z, using the anchor's own standoff when one is set and your default_standoff otherwise. That is the tcp convention doing real work, and it is the reason a mis-oriented grasp anchor drives the arm into the object instead of away from it.
For articulated objects, pass joint_config to resolve the anchor at a chosen joint angle instead of the object's current one:
lid = load_object_anchor(target.id, "lid_open", joint_config={"lid_joint": 0.0})Validation rules that bite
An object model is validated against its URDF when it loads. These fail the load outright:
parent_linkis not a link in the URDF. The error lists the link names that do exist, which is usually enough to spot the typo.- An anchor name collides with a URDF link name. Rename the anchor.
- A quaternion has a near-zero norm. Usually a placeholder
[0, 0, 0, 0]that was never filled in. - No anchor named
object.
A failure here is a good failure. It happens at load time, on your machine, before anything moves.
A loaded object model and a clean validation pass say nothing about whether a motion is safe. They confirm the file is internally consistent, not that the anchor is in the right place or pointing the right way. Before running against physical arms, verify the motion in simulation and follow your site's hardware procedure with someone at the stop control.
Where anchors come from
Most anchors arrive with the object from the mesh database, already authored. You add or edit them when the catalog's version lacks something you need: a custom well, a non-standard pour point, or a description that does not say which convention it means.
Field-by-field reference for the file anchors live in.
How a measured pose gets applied back to an object in the world.
Pulling an object, and its anchors, into your project.
Where SkillObject parameters and the rest of the skill anatomy are covered.
Updated 3 days ago