Skip to content

OpentronsOT2Backend: validate head8 pickup geometry against the Opentrons API#1136

Draft
BioCam wants to merge 9 commits into
PyLabRobot:mainfrom
BioCam:ot2-head8-pickup-guard
Draft

OpentronsOT2Backend: validate head8 pickup geometry against the Opentrons API#1136
BioCam wants to merge 9 commits into
PyLabRobot:mainfrom
BioCam:ot2-head8-pickup-guard

Conversation

@BioCam

@BioCam BioCam commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

The 8-channel head is rigid: the Opentrons API anchors a multi pickup at the back (channel-0) nozzle and only approves front-anchored, contiguous nozzle layouts. Without a check, the backend could send a pickup the API cannot declare, or one whose tipspots do not line up with the nozzles.

This validates an 8-channel pickup before it is sent: the channels must be a channel-0-anchored contiguous block [0, 1, ..., k], the tipspots must come from a single rack and a single column, and they must run consecutively down that column in channel order at the 9 mm nozzle pitch. A back-anchored selection such as [1..7] (which would leave row A and overhang the back edge) is rejected with an explanation. Tips that undeclared nozzles still grab are accounted for in PLR tip tracking so a partial pickup leaves the trackers consistent.

Stacked on #1135, which adds the 8-channel model this guards. The self-contained change here is the commit above that base.

🤖 Generated with Claude Code

BioCam and others added 5 commits June 29, 2026 22:56
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>
@BioCam BioCam marked this pull request as draft June 30, 2026 08:27
BioCam and others added 3 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>
@BioCam BioCam force-pushed the ot2-head8-pickup-guard branch from d656480 to 2a2c4d9 Compare June 30, 2026 09:48
…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>
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