Skip to content

OpentronsOT2Backend: head8 motion-safety guards - reachability and collision#1134

Draft
BioCam wants to merge 20 commits into
PyLabRobot:mainfrom
BioCam:ot2-visualizer-slots
Draft

OpentronsOT2Backend: head8 motion-safety guards - reachability and collision#1134
BioCam wants to merge 20 commits into
PyLabRobot:mainfrom
BioCam:ot2-visualizer-slots

Conversation

@BioCam

@BioCam BioCam commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Adds the two physical-safety checks for the 8-channel head, both center-based so they work for single and multi pipettes alike.

Reachability: before a pickup, drop, aspirate, or dispense, each declared channel's target is mapped to its mount and per-nozzle y offset, converted into the robot frame, and tested against the OT-2 gantry envelope. Near the front/back deck limits only a subset of a multi's nozzles can reach, so an out-of-envelope move is rejected with the offending channels named rather than driven into a hard stop.

Surrounding-resource collision: a bounding box is taken around the 8-nozzle column (xy tolerance 5 mm); any other deck resource whose footprint overlaps it and whose top sits within 10 mm of the tip-pickup height is flagged, so the head is not lowered into adjacent labware.

Stacked on #1136 (head8 + pickup guard), and additionally builds on #1130 (geometry, for the reach envelope) and #1131 (deck holders, for enumerating surrounding resources). Its own delta over those is the reachability and collision checks.

🤖 Generated with Claude Code

BioCam and others added 11 commits June 29, 2026 22:45
The OT-2's deck/gantry geometry is factory-identical across machines, so it lives as
a constant rather than something probed from the device (unlike STAR hardware). This
dataclass holds the gantry extents, partial-tip padding offsets, and left/right mount
offsets, sourced from the Opentrons shared-data definitions.

`single_channel_reach` / `can_reach_position` / `channel_y_offsets` mirror
`STARBackend.can_reach_position`; the head8 channel offsets are validated against the
Opentrons eight_channel nozzle map (A1 back-most at +31.5, 9 mm pitch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Model the 12 slots as ResourceHolder children (one per slot) so the deck geometry has
a single source of truth that serializes and renders like the rest of PLR, instead of
a bare coordinate list paralleled by hardcoded grids elsewhere.

Re-base the slots onto the deck plate corner (the Opentrons cornerOffsetFromOrigin of
115.65, 68.03), so they sit correctly inside the plate rather than flush in the
front-left. Set size_z to 0 to match the flat reference surface, and size slot 12's
holder to the fixed-trash footprint (172.86 x 165.86 x 82) since the trash overhangs a
standard slot. Default deck name is now ot2_deck.

`OpentronsOT2Backend`: convert deck-frame coordinates to the robot frame before issuing
moves, which cancels the inset so real hardware moves are unchanged; and walk
holder->deck for the slot lookup now that labware sits in a holder. Deserializing a deck
saved before slots were holders now errors loudly rather than misplacing labware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
assign_child_resource replaced the existing slot holder via list.index(existing), but `existing`
is typed Resource (not ResourceHolder), which mypy rejected. Find it by identity with an enumerate
loop instead; behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…so mypy passes

The serialize/deserialize test indexed loaded.slots[6]/[3] (typed Resource | None) directly for
.name and get_slot; bind them and assert non-None first. Behaviour unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Characterization tests ahead of the transport-seam refactor (Phase 0). The
existing suite already asserts the pickup/drop/aspirate/dispense wire calls;
this adds the four remaining ot_api call sites so the seam refactor cannot
change them silently:

- home() issues one ot_api.health.home()
- list_connected_modules() returns ot_api.modules.list_connected_modules() verbatim
- stop() cancels the active run via the requestor and clears mounted pipettes
- a discard to the deck trash at api_version >= 7.1.0 routes through the
  addressable area (move_to_addressable_area_for_drop_tip + drop_tip_in_place),
  not drop_tip

No production code changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduces a single transport seam (Phase 1): every ot_api call - including the
run-cancel requestor calls in stop() - now goes through self._ot, set to the
ot_api module in __init__. Behaviour is unchanged (self._ot is ot_api), but a
subclass can now dry-run the backend by swapping the handle for a recording
stand-in, the way STARChatterboxBackend overrides STAR's transport.

The dedicated `import ot_api.requestor as _req` is dropped; ot_api.requestor is
reachable through the handle (self._ot.requestor).

Guarded by the Phase 0 characterization tests, which patch ot_api.* and stay
green because self._ot is the same module object.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nsport seam

Phase 2. A chatterbox sibling to OpentronsOT2Backend that swaps the self._ot
transport handle for a recorder, so the real backend logic - pipette selection,
tip and volume tracking, the per-operation wire calls - runs unchanged with no
hardware and no ot_api library. Mirrors STARChatterboxBackend (transport-only
override) rather than OpentronsOT2Simulator (which reimplements the high-level
methods single-channel).

Issued calls are printed and collected in .commands. The recorder returns canned
data for the reads setup() and the operations make back (mounted pipettes, api
version, labware define, save_position, modules). Exported from backends/__init__.

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

Phase 3. Direct tests run the chatterbox with no ot_api installed: setup resolves
both mounts, a pickup -> aspirate -> dispense -> trash-discard records exactly one
wire call per operation, an unknown pipette name raises, and serialize() captures
the mounts. A differential audit runs the same single-channel protocol through the
chatterbox and the reference OpentronsOT2Simulator and asserts identical tracked
outcomes (tip mounted, source/destination volumes). The audit is scoped to the
single-channel overlap, since the simulator cannot represent a multi-channel head.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The OT-2 talks HTTP through ot_api rather than a pylabrobot.io transport, so its
wire traffic never reached the IO log other backends produce. Wrap the real ot_api
handle in a transparent _IOLogger proxy that logs every call (submodules recursed,
plain attributes passed through) at LOG_LEVEL_IO. The chatterbox recorder logs the
same way, so a dry run captures the same wire trace as a real run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Now that OTDeck's slots are ResourceHolder children, draw them from the serialized
resource tree like any other holder instead of a hardcoded slot grid. Drop the deck
`location` override that double-counted the plate-corner offset (labware was rendering
and measuring one slot-offset too far). Slot-number labels are inset to match the
holders and drawn on top of empty slots only, so labware in an occupied slot is shown
instead of being covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Trash renderer and the drag-snapping helpers looked up `resources["deck"]`,
assuming the deck is named "deck". Renaming the OT-2 deck to ot2_deck left that lookup
undefined, so `Trash.drawMainShape` read `undefined.constructor` and crashed the whole
render (blank canvas and empty tree).

Add a `getDeck()` helper that finds the deck by `instanceof Deck` and route all three
call sites through it, so the visualizer is robust to any deck name. The Trash stays
hidden whenever a deck is present, as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BioCam BioCam marked this pull request as draft June 30, 2026 08:27
BioCam and others added 9 commits June 30, 2026 10:44
First slice of building the 8-channel head into the backend itself. Adds the
channel model: _pipette_channel_count (8 for a multi, 1 for a single), _channel_map
(per-mount channel blocks, left then right), num_channels derived from it, and
_pipette_id_for_channel routed through it. A multi mount now reports 8 channels
(0 = back / row A) instead of 1; single-channel setups are unchanged, so the
characterization net and the chatterbox tests stay green.

Per-column command issuing and the pickup geometry guard build on this next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ep 2)

Routes pick_up_tips / aspirate / dispense / drop_tips through a new
_resolve_pipette_and_primary(ops, use_channels): it maps the channels to the
single mount they address (rejecting a mix of mounts) and picks the primary op
(lowest nozzle). A multi column op now issues exactly one ot_api command at the
primary well while PLR tracks all 8 channels. can_pick_up_tip is likewise routed
through the channel map so the multi's channels 1-7 are accepted (previously
hardcoded channel 0 = left, 1 = right).

The single-channel _get_*_pipette helpers stay - OpentronsOT2Simulator still uses
them. Verified in simulation through the chatterbox: one pick_up_tip / one
aspirate_in_place / one dispense_in_place for a full 8-channel column, all eight
channels tracked, and a cross-mount selection rejected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Remove test_multi_mount_reports_eight_channels: num_channels==9 is already
  pinned by test_channel_map_multi_mount_is_eight_channels and exercised by the
  8-channel column test.
- Remove the chatterbox-vs-simulator differential: both backends share the PLR
  frontend that does the tracking, so they can only diverge by raising, which the
  full-protocol recording test already guards.
- Strip tautological assertEqual(offset_x, offset_x) (x3 in each of test_tip_pick_up
  and test_tip_drop) and a duplicate well_name assert in test_tip_drop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p 3)

Adds _check_head8_pickup: a multi pickup must use a channel-0-anchored contiguous
block [0..k] (the API anchors at the back nozzle and only declares front-anchored
layouts), and the head also grabs occupied tipspots below the selection within its
8-nozzle reach. pick_up_tips rejects such a pickup unless allow_undeclared_tip_pickup
is set (per call, keyword-only, or on the backend), in which case _absorb_undeclared_tips
accounts for the grabbed tips so PLR tracking stays consistent, with a warning.

The keyword-only override keeps OpentronsOT2Simulator's **kwargs signature compatible.
Verified through the chatterbox: full column OK, back-anchored [1..7] rejected,
grab-extra A1:F1 rejected, and allow_undeclared absorbs all eight with a warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pirate/dispense

Wires the merged OT2RobotGeometry reach predicate into the operations (the STAR
pattern). Adds can_reach_position(channel_idx, position): maps the channel to its
mount and per-nozzle y offset - a single pipette resolves to its centre, a multi to
each nozzle's offset - converts the target to the robot frame via _deck_to_robot_frame,
and tests the gantry envelope. ensure_can_reach_position raises if any declared
channel can't reach its target, called at the start of pick_up_tips, aspirate, and
dispense, and in drop_tips for rack-drops only.

drop_tips guards the check with `if not use_fixed_trash`: the fixed-trash drop routes
through the addressable area, not a reach-bounded move, so the trash (which sits outside
the gantry envelope) must be exempt - caught while prototyping the integration.

Composes with the existing _check_head8_pickup tip-grab guard: reach-envelope (can the
nozzles get there) plus tip-grab geometry (which spots a multi grabs). Works for single
and multi alike. The chatterbox carries a geometry instance too so dry runs check reach.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…artial-pickup tests

Adds a second head8 guard: a bounding box around the eight-nozzle column (padded by
_HEAD8_XY_TOLERANCE in x/y) is tested against every other deck resource; one whose footprint
overlaps the box and whose top sits within _HEAD8_Z_TOLERANCE of the head's lowest travel point
is a collision, and pick_up_tips rejects it. Complements _check_head8_pickup (same-rack tip-grab,
which ignores z): this is z-aware, so a short resource under the overhang is cleared while a tall
one (a tiprack) is caught.

The reference height is the nozzle-engagement z (tipspot z + tip length - fitting depth), where
the bare nozzles sit during the descent - not the tipspot anchor, which is the tip bottom and for
300/1000 uL tips sits near the deck (driving the z threshold negative and flagging everything).

Tests: partial pickup picks exactly k for k=1..7, bottom-rows overhang allowed at slot 1 and slot
10, grab-extra boundary, and the surrounding-resource guard (tall neighbour rejected, short
neighbour cleared).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on z-reference

Addresses review notes on the head8 guards:
- The 9 mm nozzle pitch now lives in one constant (_HEAD8_NOZZLE_PITCH); both _check_head8_pickup
  and the surrounding-resource check alias it instead of two hardcoded copies.
- Adds the z-reference regression test: a 300 uL partial pickup over a short 96-well plate must be
  allowed. With the buggy tipspot-anchor reference a 300 uL tip's threshold goes negative and would
  reject it, so this fails if the nozzle-engagement reference is reverted - the previous 20 uL tests
  could not catch that.
- Adds a synthetic-coordinate unit test of _check_head8_surrounding_resources (stub deck/resources)
  that exercises the x-y box overlap and the z threshold directly, decoupled from deck geometry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BioCam BioCam changed the title Visualizer: render the OT-2 deck slots from resource holders OpentronsOT2Backend: head8 motion-safety guards - reachability and collision Jun 30, 2026
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