Skip to content

STARBackend: create 96-head mix96#1096

Open
BioCam wants to merge 4 commits into
PyLabRobot:mainfrom
BioCam:create-starbackend-mix96
Open

STARBackend: create 96-head mix96#1096
BioCam wants to merge 4 commits into
PyLabRobot:mainfrom
BioCam:create-starbackend-mix96

Conversation

@BioCam

@BioCam BioCam commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Creates mix96 - in-place mixing with the rigid 96-head, built on the head96_experimental_aspirate / head96_experimental_dispense primitives. It positions the head over a target and runs repeated surface-following aspirate / dispense cycles, taking the mix parameters directly rather than a liquid class. Builds on #1089 (the experimental aspirate / dispense), #1081 (the head96_move_x acceleration control), and #1086 / #1088 (the tip-bottom Z reference and per-drive speed / acceleration scoping).

  @_requires_head96
  @need_iswap_parked
  async def mix96(
    self,
    mix: Mix,
    resource: Optional[Union[Plate, Container, List[Well]]] = None,  # aspirate96-style target
    a1_coordinate: Optional[Coordinate] = None,        # or an explicit channel-A1 deck target
    minimum_traverse_height_start: Optional[float] = None,  # abs Z before X/Y; None -> Z-safety
    offset: Coordinate = Coordinate.zero(),            # added to the target; offset.z lifts the floor
    lld_mode: Optional[LLDMode] = None,                # None -> LLDMode.OFF (only OFF supported)
    descent_speed: float = 80.0,                       # mm/s, fast descent to just above the well
    swap_speed: float = 5.0,                           # mm/s, careful descent into the well and exit
    settling_time: float = 0.0,                        # s, wait after the last cycle before exit
    minimum_traverse_height_end: Optional[float] = None,    # abs Z after mixing; None -> Z-safety
  ):

The target is declared in exactly one of two ways, like aspirate96: an explicit a1_coordinate, or a resource (a Plate - head A1 over well A1 - a Container, or a list of Wells) plus offset. All Z is tip-bottom referenced: the declared Z (or a well's cavity_bottom) is the floor, the deepest the tip end reaches, and offset.z lifts the whole stroke off it.

Motion: parks the iSWAP, traverses to a start height, moves X/Y (X acceleration dialled down below y=200 mm, the low-Y zone where the head is cantilevered off the X-drive), descends in two stages (descent_speed to just above the well, then swap_speed into it), runs mix.repetitions symmetric cycles between the floor and surface_following_distance above it, waits settling_time, then retracts at swap_speed before the traverse out.

# explicit channel-A1 deck coordinate
await star.mix96(Mix(volume=50, repetitions=3, flow_rate=100), a1_coordinate=Coordinate(500, 150, 100))

# aspirate96-style resource target (head A1 over well A1)
await star.mix96(Mix(volume=50, repetitions=3, flow_rate=100), resource=plate)

lld_mode currently accepts only LLDMode.OFF; liquid-level-driven auto surface following (computing the surface-following distance from a cLLD probe) is planned as a follow-up.

Validated on hardware: the surface-following primitive this builds on moved the tip exactly 8 mm down and back, repeatably with no drift (#1089).

Tests: two minimal orchestration assertions for the highest-risk paths - the experimental command's minimum_height resolves to the tip-bottom floor (a well's cavity_bottom plus offset.z), and the careful descent starts surface following at floor + surface_following_distance so the stroke spans [floor, floor+sf] and never drives below the floor.

BioCam and others added 3 commits June 20, 2026 16:43
Mixes in place with the rigid 96-head, built on the head96_experimental_aspirate /
head96_experimental_dispense primitives. Moves X/Y over a target (declared as an
explicit a1_coordinate or an aspirate96-style resource, plus offset), descends in two
stages (descent_speed to just above the well, then swap_speed in), runs mix.repetitions
symmetric surface-following aspirate/dispense cycles between the floor and the
surface-following distance above it, settles, then retracts at swap_speed before the
traverse out. v1 supports only LLDMode.OFF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reorder mix96 parameters (and the docstring Args to match) by the chronological
order in which they take effect as the command runs, so the signature mirrors the
execution flow. Reword the LLD guard message to drop the "v1" version tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two minimal orchestration tests for the highest-risk mix96 failure modes: the
experimental command's minimum_height is the resolved tip-bottom floor
(cavity_bottom + offset.z), and the careful descent starts surface following at
floor + surface_following_distance so the stroke spans [floor, floor+sf] and never
drives below the floor. Sub-primitives are mocked via a shared _stub_mix96_motion
helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BioCam BioCam requested a review from rickwierenga June 20, 2026 16:17
mix96 drives the 96-head over the deck with H0 direct-drive moves, which - unlike
the C0 core-96 commands - do not raise the single pipetting channels to safe Z at
firmware level. A channel left lowered would then sweep through the X/Y traverse, so
mix96 now clears all channels to safe Z (C0 ZA) before moving. The test stub mocks
that call alongside the other primitives.

Also correct the mix96 docstring's target-declaration list to a blank-line-delimited
RST bullet list, which the docs build (warnings-as-errors) was rejecting as an
unexpected indentation.

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