Skip to content

feat(opentrons): Opentrons Flex liquid handler (plain-class, mount-addressed heads) - #1184

Open
vcjdeboer wants to merge 1 commit into
PyLabRobot:mainfrom
vcjdeboer:feat/opentrons-plain-class
Open

feat(opentrons): Opentrons Flex liquid handler (plain-class, mount-addressed heads)#1184
vcjdeboer wants to merge 1 commit into
PyLabRobot:mainfrom
vcjdeboer:feat/opentrons-plain-class

Conversation

@vcjdeboer

@vcjdeboer vcjdeboer commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Opentrons Flex liquid handler (plain-class, mount-addressed heads)

Adds the Opentrons Flex as a plain-class device, following the post-capability architecture. It drives the on-robot robot-server HTTP API (Protocol Engine: /runs, /commands, /instruments) directly.

Architecture

  • OpentronsRobot(abc.ABC) — shared base owning the transport, the run/command lifecycle, and instrument discovery. The transport sits behind a small OpentronsTransport Protocol with two implementations: an httpx transport for real hardware and an offline recording transport for dry runs (so the whole device is testable without a robot).
  • OpentronsFlex(OpentronsRobot) — the device. setup() discovers the mounted pipette(s) and composes a head sub-object per gantry mount (flex.left / flex.right), or flex.head96 for the 96-channel head. stop() drops any mounted tips to the trash, homes the gantry, then cancels the run and disconnects.
  • FlexHead1 / FlexHead8 / FlexHead96 — mount-addressed fixed heads. Each operation sends one robot-server command anchored at the reference well; the fixed head fans it out to its N nozzles (a column for Head8, the whole plate for Head96, a single well for Head1). Head8 also supports single/partial pickup via configureNozzleLayout.

Behaviour

  • Resource-tree state. Tip and volume state commit to TipSpot.tracker / Well.tracker, only for the channels actually actuated (None-skip), and only through a transactional stage → wire → verify → commit/rollback flow — so an infeasible operation raises before any hardware motion, and a failed wire command leaves no partial state. Gated by the global does_tip_tracking() / does_volume_tracking() switches.
  • Hardware tip-presence authority. The Flex's per-pipette tipDetected sensor is the ground truth for tip presence: pickups are verified against it and rolled back on a missed pickup, and get_mounted_tips() (public per-channel telemetry) is reconciled against it.
  • Positioning. Aspirate/dispense default to 1 mm above the well bottom (the Opentrons default) and auto-issue prepareToAspirate before the first aspirate after a pickup.
  • Name-based labware. The robot owns the authoritative labware geometry, resolved from the Opentrons load name (ot_load_name); the PLR resources carry a nominal SBS grid for tracking/addressing only. FlexDeck models slots as ResourceHolders; name-based tip-rack and plate factories are included.

Hardware status

FlexHead8 is verified on real Opentrons Flex hardware (robot-server API 8.8: setup, homing, and column tip pickup confirmed against the tip-presence sensor). FlexHead1 and FlexHead96 are implemented but not yet hardware-verified (they need 1-channel / 96-channel pipettes) and emit a one-time warning on first use.

Docs & tests

  • A Head8 hello-world notebook (docs/user_guide/opentrons/flex/hello-world.ipynb) and an API reference page, wired into the docs toctrees.
  • Offline unit tests for the transport, device composition, and all three heads (via the recording transport).

Follow-ups (not in this PR)

  • FlexRobotGeometry native reach checks (per-mount / nozzle-envelope), folding a grounded operating-envelope model.
  • FlexDeck deserialization guard (reject labware assigned directly to the deck).
  • OT-2 on the shared OpentronsRobot base (pending verification that the two share enough at the wire level).

@rickwierenga
rickwierenga force-pushed the v1b1 branch 2 times, most recently from 6af085c to 1ae9dc6 Compare August 1, 2026 20:32
@Silousr

Silousr commented Aug 4, 2026

Copy link
Copy Markdown

Downstream report: bridged this driver into an agent-facing protocol (simulation only)

I maintain the labwire bridge that wraps PyLabRobot for AI-agent control (discussed in the earlier thread about cancellation on the Flex). Since the plain-class redesign will eventually reach us, I ran an experiment against this PR to see how a protocol adapter fares in the new architecture. Everything below is observation from that exercise, in case any of it is useful mid-redesign. Nothing here needs action on my account.

Setup: pinned this PR's head commit (6ee378e), drove OpentronsFlex through our protocol layer, and exercised it against a simulation of the robot-server command layer (mock HTTP for /health, /runs, /runs/{id}/commands, /instruments), which I understood to be how the PR itself was validated. No hardware involved on my side either.

What ported cleanly:

  • FlexDeck behaves as a first-class Deck throughout. Resource traversal, name lookup, per-well tip and volume trackers, and our reference validation all worked over it with zero changes to our resource code. The slot-as-child-resource design is what made that true.
  • The list-shaped method signatures (pick_up_tips, drop_tips, aspirate, dispense) are close enough to the LiquidHandler shapes that our existing handlers called the driver without modification.
  • One command per HTTP POST maps well to honest cancellation semantics. After the earlier Flex discussion we ended up declaring every atomic operation non-cancellable and only bridge-sequenced compounds stoppable between steps; this driver's command granularity made that mapping exact.

Things I hit that may be worth knowing:

  • Installing the PR head as a package (pip install from the commit) cannot import pylabrobot.liquid_handling: the chain fails at pylabrobot.molecular_devices.imageXpress, which is missing from the installed package at that commit. Looks like a packaging artifact of the older v1b1 base rather than anything in the driver files; current v1b1 does not have the problem, so a rebase presumably clears it. Only matters to someone consuming the PR as an install rather than a checkout.
  • Separately, on current v1b1 (1ae9dc6), our shipped LiquidHandler-based test suite passes 110 of 113 through the legacy shims. The three failures, in case the data is useful: pylabrobot.liquid_handling.errors has no shim module (old-path import of ChannelizedError breaks), and the Cor_96_wellplate_360ul_Fb factory now produces model string cor_96_wellplate_360uL_Fb, which breaks anything keyed on the old model string. In our case that key was a safety annotation, so the mismatch failed silent rather than loud, which was a good lesson about string-keyed config on our side.
  • Multi-element calls: the driver builds each robot command from element [0] of its list parameters but commits tracker state for every zipped element, so a two-well aspirate updates both wells' volume trackers while the robot aspirates only the first. The PR already describes itself as single-channel-first, so this is expected territory; the part that bit us as a consumer is that the tracker side is silent about it. Our adapter refuses multi-element calls until batching lands, which was an easy guard once we saw it.
  • Channel state: which physical channel holds which tip lives in _channel_tips, which is private. LiquidHandler.head was public, and our telemetry read it. In the plain-class model there was nothing public for an adapter to read for that, so we read the private attribute and documented that we did. Just noting where the public surface ended for us; the resource-tree trackers covered everything else.

The driver held up well under a fairly adversarial consumer. Happy to share the simulation harness or the full compatibility notes if either is ever useful.

@rickwierenga

Copy link
Copy Markdown
Member

thank you @vcjdeboer !

could you please rebase this onto main?

@vcjdeboer
vcjdeboer force-pushed the feat/opentrons-plain-class branch from 6ee378e to 06971f2 Compare August 5, 2026 07:58
@vcjdeboer
vcjdeboer changed the base branch from v1b1 to main August 5, 2026 07:58
@vcjdeboer
vcjdeboer force-pushed the feat/opentrons-plain-class branch from 06971f2 to 5166ba4 Compare August 5, 2026 13:37
@vcjdeboer vcjdeboer changed the title feat(opentrons): Opentrons Flex liquid handler driver feat(opentrons): Opentrons Flex liquid handler (plain-class, mount-addressed heads) Aug 5, 2026
…dressed heads)

Add the Opentrons Flex as a plain-class device (post-capability architecture).

- OpentronsRobot(abc.ABC): shared base owning the robot-server HTTP transport
  (behind a swappable OpentronsTransport Protocol, with an httpx transport and
  an offline recording transport for dry runs), the run/command lifecycle, and
  instrument discovery.
- OpentronsFlex(OpentronsRobot): the device. setup() discovers the mounted
  pipette(s) and composes a head sub-object per mount (flex.left / flex.right),
  or flex.head96 for the 96-channel head. stop() drops any mounted tips to the
  trash, homes the gantry, then cancels the run and disconnects.
- FlexHead1 / FlexHead8 / FlexHead96: mount-addressed fixed heads. Each op
  sends ONE robot-server command anchored at the reference well; the hardware
  fans it out to the head's N nozzles. Tip/volume state commits to the resource
  tree (TipSpot.tracker / Well.tracker), only for actuated channels (None-skip)
  and only via a transactional stage -> wire -> verify -> commit/rollback. The
  Flex hardware tip-presence sensor is the authority for tip presence: pickups
  are verified against it (rolling back on a missed pickup) and get_mounted_tips()
  is reconciled against it. Aspirate/dispense default to 1 mm above the well
  bottom and auto-issue prepareToAspirate before the first aspirate after a
  pickup.
- Labware is name-based: the robot owns the authoritative geometry, resolved
  from the Opentrons load name (ot_load_name); PLR resources carry a nominal
  SBS grid for tracking/addressing only. FlexDeck models slots as
  ResourceHolders; name-based tip-rack and plate factories.
- Docs: a Head8 hello-world notebook and API reference, wired into the docs
  toctrees.

FlexHead8 is verified on real Opentrons Flex hardware (robot-server API 8.8:
setup, homing, and column tip pickup against the tip-presence sensor).
FlexHead1 and FlexHead96 are implemented but not yet hardware-verified and emit
a one-time warning on first use.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vcjdeboer
vcjdeboer force-pushed the feat/opentrons-plain-class branch from 5166ba4 to 5f3869c Compare August 5, 2026 13:48
@vcjdeboer

Copy link
Copy Markdown
Contributor Author

@Silousr thanks for the test. The PR has been reworked since the commit you pinned. It is now a mount-addressed head model (flex.left / flex.right / flex.head96, with FlexHead1 / FlexHead8 / FlexHead96), so two of your points are addressed:

  • Multi-element tracker divergence: the fixed-head model has no arbitrary use_channels path. Each op sends one command anchored at the reference well and the hardware fans it to the head's nozzles; trackers
  • commit only for actuated channels (None-skip), via stage → wire → verify → commit/rollback. Arbitrary multi-well isn't offered rather than silently mishandled.
  • Channel→tip state: get_mounted_tips() is public now, and has_tip_on_hardware() reads the Flex tipDetected sensor, which is the authority. Pickups verify against it and roll back on a miss.

The import artifact was a v1b1-base issue and clears on the current main rebase. FlexHead8 is verified on a real Flex; Head1/Head96 are coded but not yet hardware-tested.

Also, the bigger reason was the abstraction: the Flex only has fixed heads (1/8/96-channel across two gantry mounts), so a flat single-channel class with a use_channels list was modelling independently-addressable channels the machine doesn't have (I think use_channels is a Hamilton STAR concept). The head model matches the real hardware, and it lets the robot stay the authority for the things it owns anyway (labware geometry via load names, tip presence via the tipDetected sensor).

So this build doesn't include a reach-geometry model like the OT2RobotGeometry that @BioCam added for the OT-2 (in resources/opentrons/, though nothing wires it into a device yet I think). Worth deciding whether the Flex should use the same approach.

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.

3 participants