Skip to content

Resource: turn about a joint, and the links and end-effectors that need it - #1249

Open
BioCam wants to merge 5 commits into
PyLabRobot:mainfrom
BioCam:resources-manipulator-primitives
Open

Resource: turn about a joint, and the links and end-effectors that need it#1249
BioCam wants to merge 5 commits into
PyLabRobot:mainfrom
BioCam:resources-manipulator-primitives

Conversation

@BioCam

@BioCam BioCam commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

An articulated device has joints that are not at a resource's own corner, and nothing in PyLabRobot can express one. A carousel, a rotor, a hinge, every link of an arm - all pivot on a point somewhere on the part, and Resource.rotate has always turned about the left front bottom corner, so a link modelled today swings off its joint rather than on it.

  • rotate and rotated take an optional reference: the point to turn about, in the resource's own frame. The resource is carried by however far the turn moved that point, so the point stays put and the resource swings on it. The offset is taken back through the parent's rotation, since reference is measured in the resource's frame and location in the parent's.
  • Link is one rigid member of a chain - a line between two joints, no width or depth, so the joint it turns on is its own origin. Material hangs off it as children through bolt_on, which keeps a link's frame apart from the shape around it.
  • MechanicalGripper is a Link: it spans the joint it turns on to the point it grips at, its tool centre point. Body, fingers and pads are bolted to that span, and how far apart the fingers stand is state rather than shape.

Behaviour: unchanged. reference defaults to None, and both the matrix build and the offset are skipped without one, so rotate costs what it always did at any depth.

Tests: the new primitives carry their own - a chain folding on its joints, turn_to being absolute where rotate accumulates, bolt_on placing material, the jaws standing symmetrically and refusing a width they cannot reach, and a pad sitting the same way on both fingers. resource_tests covers the pivot, asserting the reference point stands still rather than that the location moved. Full suite passes, 3203 tests.

A reference given for a resource with no location is ignored rather than refused, which wants its own decision. This also touches the same three lines of rotate as #1247, so whichever lands second carries a small merge.

🤖 Generated with Claude Code

…need it

An articulated device has joints that are not at a resource's own corner, and nothing in
PyLabRobot could express one. A plate hotel's carousel, a centrifuge rotor, a hinge, and every
link of an arm pivot on a point somewhere on the part; `Resource.rotate` has always turned about
the left front bottom corner, so a link modelled today swings off its own joint.

- `rotate` and `rotated` take an optional `reference`: the point to turn about, in the resource's
  own frame. The resource is carried by however far the turn moved that point, which leaves the
  point where it was and the resource swinging on it. `reference` is measured in the resource's
  frame while `location` is measured in the parent's, so the offset is taken back through the
  parent's rotation, a rotation matrix inverting by transposition.
- `Link` is one rigid member of a chain: a line between two joints, with no width or depth, so the
  joint it turns on is its own origin. Material is `bolt_on`'d as children with their own offsets,
  which is how a robot description keeps a link's frame apart from the shape around it - the shape
  can overhang either joint without the kinematics noticing.
- `MechanicalGripper` is a `Link`, because on an arm that is what it is: it spans the joint it
  turns on to the point it grips at, which is its tool centre point. Its body, fingers and pads
  are material bolted to that span, and how far apart the fingers stand is state rather than
  shape.

Behaviour: `reference` defaults to None and the added path is skipped entirely without one, so
every existing caller turns about the corner exactly as before.

Tests: the primitives are new, so both carry their own - a chain folding on its joints, `turn_to`
being absolute where `rotate` accumulates, `bolt_on` centring material across a link, the jaws
standing symmetrically at a commanded width and refusing one they cannot reach, and a pad sitting
the same way on both fingers. `resource_tests` covers the pivot itself, asserting on the reference
point standing still rather than on the location that moves to keep it there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@BioCam
BioCam requested a review from a team as a code owner September 9, 2026 23:46
@BioCam
BioCam requested a review from rickwierenga September 9, 2026 23:47
BioCam and others added 4 commits September 10, 2026 10:48
…ce and tool centre point

The module docstring named a tool centre point without saying what it is measured from, and
justified the gripper being a `Link` by asserting it ("it is a link, because that is what it is on
an arm"). It now uses the vocabulary a reader arrives with - end-effector, tool and end-of-arm
tooling as one thing, fitted at the wrist flange - and states the tool centre point as an offset
from that flange, belonging to the tool rather than to the arm.

That offset is what makes the gripper a link: it spans the interface it is bolted to and the point
it grips at, which is the same separation ROS-Industrial draws between `flange` and a tool frame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…esource` does not

`Finger` added one attribute over `Resource`: a `pad` pointing at a resource `bolt_on` had already
assigned as its child, so `finger.pad is finger.children[0]`. Nothing outside the module read it,
nothing type-checked against the class, and the viewer tells a finger from a pad by category, which
is set either way. The `cast(Finger, ...)` at its only construction existed to let mypy accept the
`pad` assignment - the class's sole consumer was the attribute that was its sole reason to exist.

Its docstring said as much: a list of two things it would carry "once there is something to read
them from". It can come back the day one of them arrives with a field in it.

The pads are kept as `self.pads`, which is how the one external caller already reaches them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`bolt_on` was a factory wrapped around `assign_child_resource`, and of the five things it added
only one was geometry: `-size_y / 2`, centring material across the link it hangs on. The rest was
naming, a model string, a `category` taken by splitting the name on an underscore, and an `of=`
parameter with no caller left once `Finger` went.

Its four arguments were also a bare five-number tuple - size, offset along, offset above - which
says nothing at the call site about which number is which.

Each part is now constructed and assigned where it is used, with the five numbers unpacked into
named locals, so the placement rule is visible rather than applied out of sight. The pad no longer
has its Y written and then overwritten a line later: the offset is computed once.

Behaviour: both trees are byte-identical to what `bolt_on` built - every name, category, model and
location - checked against a snapshot taken before the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-number tuple

A part arrived as `(size_x, size_y, size_z, along, above)`, which says nothing at the call site
about which number is which and puts two offsets in different axes beside three sizes. It is the
shape an argument gets swapped in, and no type checker would notice.

Each part is now a `Coordinate` for its size and a `Coordinate` for where it sits, which are the
two types the resource model already has: `Resource` carries a size and a category,
`assign_child_resource` takes a location, and `location` moves it afterwards. Nothing new is
defined to hold them.

A finger is the exception and now says so: it takes a size and an X and Z, and its Y belongs to
`jaw_width` outright rather than being declared and overwritten.

Behaviour: unchanged, and the tree is byte-identical to the one the tuples built.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant