Modeling liquid handlers

this is the big one,

in remodeling the front ends into a composition of capabilities (Updating PLR API for machine interfaces discussion)

we have already have threads for

this one is for liquid handlers

The current liquid handler interface does all liquid handling work:

  • independent channels
  • fixed 8 channels
  • 96 head
  • (arms - see above)

These are actually separate capabilities, although related.

I propose splitting them into separate capabilities for:

  • independent channels (including single channel pipettes, n=1)
  • “fixed” 8 channel head
  • “fixed” 96 channel head
    • split into single/multi volume? anticipating Lynx?
  • “fixed” 384 channel head

the fixed heads will have some shared logic for offsets etc. The super parent of all of them will have methods for updating containers and volume tracking in channels etc.

Biggest question I have: what should “independent channels” be called?

like star.SOMETHING.aspirate(containers, volumes, use_channels=[...])?

for the heads, I was thinking star.head{8,96,384}.aspirate(containers, volume)

1 Like

“pip” is maximally short and descriptive of 1-16 individually articulated pipettes

I suspect we will want a separate liquid_displacement_pip capability so when reviewing protocol code, the different physical principles and constraints between air/liq disp pips never has to be inferred.

isn’t pip Hamilton specific?

Pressure Induced Pipetting (PIP)

no

however the problem is the other 8,96,384 channel heads are also Pressure Induced Pipetting

Pip is just a shortening for Pipette. It would be sufficient for describing OT-2 single channel and any other liquid handlers with single or independent single channels.

I like star.head.96, star.head.384, or tecan.head.8 to describe channels physically fixed together in space. we could also use star.fixed.96 to be more physically descriptive

A user would very easily know ot2.head.8 needs different list comprehension and pipetting logic than star.pip[range(8)]. Tecan calls it FCA, Biomek calls it span-8, all can be treated as multiple pipettes independent along the y/z axis like the STAR.

I am not familiar with the backend kwarg differences between air and liquid displacement Tecan models, so we can save that complexity for until a user needs it.

yes, the user would easily know the physical characteristics of their hardware

from the PLR perspective in the general case it’s a little bit more complex since we need to be able to reason and type these different machines. In my first post I argued “independent 8” (independent N) should be separate from “fixed 8”. For example @CamilloMoschner’s channel planning algorithm will only work on independent channels. Also fixed 8 will require the tip rack edges to be low for partial pickup. So some specific logic, and typing, are both important. The naming is difficult :sweat_smile:

star.pip[range(8)].

this is a very interesting syntax…

kind of like putting use_channels first and then calling the operation. This is very nice considering the other changes we have like .head96. and .iswap.

star.pip[0, 4, 7].aspirate :thinking:

all can be treated as multiple pipettes independent along the y/z axis like the STAR.

is what’s important. this is different from the OT or Prep fixed 8 heads

1 Like

star.ind_channels? For independent channels

star.ind_pip?

star.ichannels?

all sound kind of awkward but perhaps I am too used to lh at this point :sweat_smile:

I kinda like star.pip (.aspirate()) - simple and can be expanded using use_channels or another way to specify up the max number of pipettes. I’m not sure how it works if your star has 2 of the span-8 heads (16 total) - should you be able to do star.pip with channels 0-10 to use 11 pipette channels at once?

I also wonder - could some of the syntax is simplified if you can can optionally code your span-8 as a fixed head versus 8 independent channels (so vol=[100]*8 vs vol=100)

I go back and forth - I think reading tecan.head.8 would make me think of a span-8, not a fixed multichannel pipetting head. fixed.8 is also a little confusing but is more description for functionality. If we lean towards head, then having the number before it helps differentiate from independent pipettes. So star.pip vs star.8head

Definitely relate to this. It is nice having some defaults happening - like lh.aspirate already assuming independent channels and then calling lh.aspirate96 to specify a different head.

yes good point …

the second “head” can also have another iswap/96 head/etc.

so the most rigorous would be star.heads[0].pip and star.heads[1].pip and star.heads[0].head96 and star.heads[1].head96. This is rigorous but also annoying. Also then what is “a head”? Should we say star.arms[0]? Then what is an arm? :upside_down_face:

Numbering wise, definitely one numbering system per “pip” instance (independent channels). So if there are two of these systems on one star, there will be two channels with index 0.

the apis for fixed 8 and variable 8 would be similar I imagine, but variable can have use_channels. Depending on what we decide, either command can accept Union[float, List[float]] for volumes.

yes, unfortunately it does not scale nicely as we continue to grow PLR

Nice to see pip and head{n} land, nice split.

That Lynx 96VVP is very nice, 96 independent volumes on a fixed head, per-channel LLD, real-time flow verification on every channel. Some of us are jealous over here with our Flex… But for the naming: I wouldn’t split it into a separate type. It’s still a head96, channels are fixed in space. The independent volume is a property of that specific head. Same name, different configuration:

Maybe I’m stating the obvious here, but it helped me to think about it this way: pip and head{n} aren’t really capabilities they’re sub-devices. The capability is the method: aspirate(), dispense(), transfer(). That gives us a pattern:

device.attribute.method()

Where the attribute can be:

  • a physical sub-device: pip, head96, iswap, arm[0]
  • a measurement domain: ph, absorbance, temperature
  • a coordinator: transport, lh
star.pip.aspirate(...)
star.head96.aspirate(...)
star.iswap.pick_up_plate(...)
cytation.absorbance.read(plate, wavelength=450)
titronic.ph.read()

When there are two arms, just add a subdevice level:

star.arm[0].pip.aspirate(...)
star.arm[0].head96.aspirate(...)
star.arm[1].pip.aspirate(...)

Single arm? Skip it: star.pip.aspirate(...).

Scales to a workcell too:

workcell.star.pip.aspirate(plate["A1"], volume=50)
workcell.cytation.absorbance.read(plate, wavelength=450)

workcell.transport.transfer_plate(
    source=star.deck["pos1"],
    target=cytation["tray"]
)

So star.pip is a sub-device backed by a PipettingCapability, and aspirate() is one of its methods. star.head96 is backed by a HeadPipettingCapability. echo.dispenser by an AcousticDispenserCapability. cytation.absorbance by an AbsorbanceReadingCapability. Same pattern: device → attribute (with Capability assigned) → method

1 Like

^ This is very easy to follow! And I like that it applies to other types of devices too, like the plate reader modelling discussion. So a single device could have multiple measurement types/domains, like absorbance and fluorescence

FWIW, looking at Opentrons API, they have their most current pipetting heads described just as number of channels

Pipette Model Volume (µL) API Load Name
Flex 1-Channel Pipette 1–50 flex_1channel_50
5–1000 flex_1channel_1000
Flex 8-Channel Pipette 1–50 flex_8channel_50
5–1000 flex_8channel_1000
Flex 96-Channel Pipette 1-200 flex_96channel_200
5–1000 flex_96channel_1000

So 8channel (or channel8) could be applicable?

ohh I like that!

@property
def pip(self):
  if len(self.heads) == 1:
    return self.heads[0].pip
  raise ValueError

self.heads might be a horrible name, but just s//g that. same for pip.

but the general idea of providing convenience methods that work in 99% of cases while keeping the underlying structure is neat

staying accurate here: we ~decided in the other thread that the objects/front ends would be called capabilities. The methods are just methods. Changing that is still possible of course but is a separate conversation.

reasonable names for the individual pipettes, which of course is easy (from our perspective). what we need is the GENERAL name for what to call a system of N independent channels :slight_smile:

my problem with “channel 8” is that 'fixed 8" are also 8 pipetting channels

(@CamilloMoschner?) do you know what hamilton calls the gantry/head/arm? (C0 and possibly C1)

Agreed - this would not be for naming the span-8 style head with independent pipettes!

Hamilton calls it “X-arm”. You can have any number 1-16 channels on a x-arm. Heck we had 8 1000ul and 4 5ml at one point on a single x-arm.

2 Likes