I can’t find anything about this already discussed. Is it currently not possible to aspirate/dispense in a 384-well plate with the CO-RE 96 head using PLR? I’ve tried a very simple test method and got this:
NotImplementedError: It is not possible to plate aspirate from an 24 by 16 plate
Is anybody working on this or done a hard-coded work-around? I figure it could be possible if I create four artificial “96-well” labware definitions where I define the wells as being in the positions of the four 384-well quadrants for the time being until it can be implemented properly…
Sorry Rick, I’m using the 96-well head, but trying to do a whole-plate aspirate/dispense from/to a 384-well plate.
This would only aspirate from one “quadrant” (my terminology, hopefully not confusing) of the 384-well plate. In other words, if aspirating from the A1 quadrant, it would be aspirating from A1, A3, …, C1, C3, …, P21, P23, etc (skipping wells.)
I’m happy to contribute to this, but didn’t know if anyone else had already made a workaround for what I think is not an uncommon scenario.
I’m afraid this is also not supported yet. Perhaps we can add a .quadrant(1,2,3,4) method to ItemizedResource/Plate that returns a list of wells to pass to aspirate_plate?
Here’s my code for targeting specific quadrants with the 96 channel. The first function maps from a 4-well cell (i.e. where a single well in a 96-well plate would be) and a quadrant index in that cell to a well index in a 384 well plate. The second function gives a list of 384wp positions for a given quadrant across all 96 cells.
def cells_96_to_384(well, quadrant):
return well*2+quadrant%2+(quadrant//2)*16+16*(well//8)
def get_384w_quadrant(quadrant):
return [cells_96_to_384(idx, quadrant) for idx in range(96)]
This was for PyHamilton but I think it’s the same in PLR
Q1
As someone without a MPH/96-channel atm I am asking more out of curiosity and for the future:
How does the coordinate system of MPH-commands work, i.e. what is the commands reference point?
Is it similar to the resource coordinate system, i.e. does the front-left tip define the movement of the entire MPH?
In other words, can the MPH aspirate/dispense commands simply be given the well_location for "O1" (marked with a yellow “X”)
and it will go with its front-left tip to this well? Or is there a different reference point?
Q2
Associated to this question:
How do we want to define “Quadrants” in PLR? (the concept is incredibly useful but I found different labs define quadrants differently, either on a ‘go-right-first’ or a ‘go-down-first’ approach)
Impact
If Q1 is indeed referencing the front-left position, and we adapt the ‘go-right-first’ approach from Q2 then aspirating/dispensing from/into a 384-well plate with a 96-MPH should be as simple as:
[await lh.dispense96(lh.deck.get_resource(<384_well_plate_name>)[ref_well], dispense_volume=20)
for ref_well in ["O1", "O13", "P1", "P13"]]
ups… I just looked into STAR.py to answer my question:
definition of dispense_core_96, STAR.py:5064:
""" dispense CoRe 96
Dispensing of liquid using CoRe 96
Args:
dispensing_mode: Type of dispensing mode 0 = Partial volume in jet mode 1 = Blow out
in jet mode 2 = Partial volume at surface 3 = Blow out at surface 4 = Empty tip at fix
position. Must be between 0 and 4. Default 0.
x_position: X-Position [0.1mm] of well A1. Must be between 0 and 30000. Default 0.
x_direction: X-direction. 0 = positive 1 = negative. Must be between 0 and 1. Default 0.
y_position: Y-Position [0.1mm] of well A1. Must be between 1080 and 5600. Default 0.
...
→ so the reference well is not the front-left one, "O1", but it is the back-left one, "A1"!
Makes sense, even if it doesn’t comply with PLR’s resource coordinate referencing system - nothing we can do here though → looks like this is intrinsic to firmware command "C0ED".
Well, this means that correcting my suggestion from above, the dispensing command for a full 384-well plate using a 96-MPH should be:
quadrant_ref_coordinates = [(loc.get_absolute_location().x, loc.get_absolute_location().y) for loc in prep_plate_1["A1", "A13", "B1", "B13"]]
[await lh.backend.dispense_core_96(x_position=ref_well[0], y_position=ref_well[1], dispense_volume=20)
for ref_well in quadrant_ref_coordinates]
…however, there is a bit of a problem here:
→ x_position and y_position type hint to integers here, which is because their units are 0.1mm. But the positions given by .get_absolute_location() are given as floats in units 1.0mm.
So, you’d have to multiply them by 10 first?
Hey all, I’m going to work on implementing some of the above suggestions today just to get a prototype method working in time for a specific purpose tomorrow, but I am looking forward to continuing this to get a “native” implementation built.
@CamilloMoschner I think I’m following you, but there is one important thing to note regarding your notes: The dimensions of 96 SBS are such that the head (MPH = multi-pipette head?) not only skips 384 wells in the vertical/y dimension (A, C, E… and B, D, F, …) but also in the horizontal/x direction (1, 3, 5, … and 2, 4, 6, …). I think that means where you have noted A13, B13, P13 etc should actually read A23, B23, P23, etc, right? I haven’t read through it close yet so maybe I’m missing something there with what you’re doing.
To further elaborate: If you took your colored diagrams and spread them so they also skipped columns horizontally, this would match my definition of 384 quadrants, and match how the 96 head maps onto a 384 well plate:
Ok I read this a little closer, and I think this should actually be A1, A2, B1, B2, instead of A1, A13, B1, B13. These are the four reference “A1” cells for the four quadrants.
Now onto actually trying your strategy @CamilloMoschner. I tried this:
quadrant_ref_coordinates = [(loc.get_absolute_location().x, loc.get_absolute_location().y) for loc in lh.deck.get_resource("reaction_plate")["A1", "A2", "B1", "B2"]]
quadrant_ref_coordinates
Yes, you are absolutely correct. I didn’t think about the jumping of the columns in addition to the jumping of the rows. (I’ll make some updated infographics in a bit)
Your error message is interesting:
Rick cleverly built in assertion checks so that a STAR-command is first checked for having correct values for all function arguments by the Python backend.
If a value is not in the range Python should give an error, and Python should tell you what argument is out of range.
However, in your case the error does not appear to arise during any of the assertion checks but it looks to me like the error actually comes directly from your STAR, i.e. Python hands the STAR a firmware command whose “Parameter is out of range” → that is what error 32 from C0EDid0012er99/00 H001/32 means (to my knowledge).
The difficult part is that we don’t know which parameter is out of range…
Your x and y values…
…and your volume (though a bit low 0.2 ul) are in the correct range:
assert 0 <= x_position <= 30000, "x_position must be between 0 and 30000"
...
assert 1080 <= y_position <= 5600, "y_position must be between 1080 and 5600"
...
assert 0 <= dispense_volume <= 11500, "dispense_volume must be between 0 and 11500"
I’d need to look at the STAR method definition in more detail and compare it with its default values.
@Stefan thanks for this. It works to get a list of wells, but forgive me if I don’t fully understand:
This would work well for the 8-span, for example, which takes a list of wells as input for aspirating/dispensing, but I don’t know how it would work for the 96 head, as the PLR aspirate_plate command takes a plate as input, and not a list.
I did one more thing that may help us dig into this, though as I mentioned I am very new the firmware side of things so I’ll need some help to parse this. I ran a method with VENUS that recapitulates what I’m trying to do here:
Aspirate with the 96 head from a 96 well plate
Dispense to a 384 well plate in one quadrant
In this case I used the 50 uL tips, but below is the firmware trace for this method. It executed successfully except for the following:
It threw an LLD error when first trying to aspirate for “not enough liquid”. I don’t know why this happened, as I asked for 20 uL aspirate, and there was 50 uL water in the wells.
When dispensing in the 384, it does go by default to the A1 quadrant, but it dispensed from way too high (I’m using the Cos_384_PCR labware, which is essentially identical to the actual 384 PCR plate I’m using). It was probably 3-5 mm above the top of the plate.
For plate based 96 channel references like what PLR uses you can use XY offsets. There might be other ways to do it but that is the most obvious to me.
(I saw some of what I wrote below was already written in other posts)
Center of well A1, or rather the channel that would usually go to A1 (left back as viewed from the cover).
I think for the quadrants you showed, you’d have to skip alternating columns. The wells are too close otherwise for the 96 head to reach simultaneously. Stefan’s code gives intuition for this.
This is very Hamilton specific. I think PLR should take a Plate/List[Well], and then STAR can be responsible for translating this.
I don’t think this is very clean tbh. It’s not clear what’s happening when you aspirate_plate with offset=something or even offset=(well.location.x, …). This might work quickly though, so I’d recommend people play with it.
In PLR, I think we should do the following:
Rename lh.aspirate_plate back to lh.aspriate96: in the 384 well plate, we are using the 96 channels but not doing a plate at a time.
Have lh.aspirate96 take either a Plate or List[Well] as an argument. If Plate, take all of its wells. (after that make sure len(wells)==96 to cover both cases.)
We can do some checking to ensure these plates are in the right order: same plate, good offset, good ordering, etc.
The wells-case of lh.aspirate96 can take eg plate.quadrant(1) = [Well, Well, ...] so: lh.aspirate96(plate.quadrant(1)). This would only work for 384 well plates on the 96 head, although the behavior will work for 96-well plates as well to be future proof.
Then, STAR will just take well[0] as the reference point. Other backends may treat it differently.
I can push this probably tonight or tomorrow, and use @jonlaurent’s firmware log as a test case, in addition to a local test on one of our plates.
hehe yeah… I have been using the quadrants I showed for 8-channel processing of 384-well plates and haven’t thought about the column-jumping of the 96-MPH.
Here are the correct quadrants for the 384-well plate using a 96-MPH:
I completely agree with Rick. Offsets are “quick & dirty” solutions and very good if you have to perform an experiment or have a showcase tomorrow but otherwise they are too hacky and lead to confusion in the long-term.
We should figure out a more permanent solution now that the opportunity has arisen.
A general question regarding the 96-MPH:
How can we use this problem to enable picking up and using non-96 position tip pickups and aspiration dispensations?
E.g. how can the 96-MPH pick up only 4 columns of tips? or 4-columns of tips using only 6 of the 8 possible rows?
I don’t think this is physically supported on the machine. What you could do is just the 8 channel head to remove the tips from the tip rack that you don’t want picked up.