Hi everyone,
In many automated protocols we (automators) perform liquid transfers between different plate formats, e.g.: from a 96-wellplate to 24-wellplates, or from 384-wellplates to 96-wellplates.
To facilitate these liquid transfers it is useful to have a set of pre-defined transfer patterns which map individual samples between the plates involved.
1st Transfer Pattern
One such transfer pattern has already been established in PLR:
- wellplate_format: 384
- transfer_pattern: 384<->96
- quadrant_type: “checkerboard”
- quadrant_internal_fill_order: “row-major” order
- number_of_parallel_channels_in_y_possible: 8 (with minimal y-distance=9mm)
- PLR-discussed use case: 96head-based transfers
It’s implementation can be found in pylabrobot/resources/plate.py
:
class Plate(ItemizedResource["Well"]):
...
# TODO: add quadrant definition for 96-well plates & specify current
# quadrant definition is only for 384-well plates
def get_quadrant(self, quadrant: int) -> List["Well"]:
"""Return the wells in the specified quadrant. Quadrants are overlapping and refer to
alternating rows and columns of the plate. Quadrant 1 contains A1, A3, C1, etc. Quadrant 2
contains A2, quadrant 3 contains B1, and quadrant 4 contains B2."""
if quadrant == 1:
return [
self.get_well((row, column))
for row in range(0, self.num_items_y, 2)
for column in range(0, self.num_items_x, 2)
]
elif quadrant == 2:
return [
self.get_well((row, column))
for row in range(0, self.num_items_y, 2)
for column in range(1, self.num_items_x, 2)
]
elif quadrant == 3:
return [
self.get_well((row, column))
for row in range(1, self.num_items_y, 2)
for column in range(0, self.num_items_x, 2)
]
elif quadrant == 4:
return [
self.get_well((row, column))
for row in range(1, self.num_items_y, 2)
for column in range(1, self.num_items_x, 2)
]
else:
raise ValueError(f"Invalid quadrant number: {quadrant}. Quadrant must be 1, 2, 3, or 4.")
This 384<->96 transfer pattern is also already defined in the PLR docs “Using the 96 head”:
There the indexing of the different quadrant is shown as…
…i.e. left-top (quadrant_1) > right-top (quadrant_2) > left-bottom (quadrant_3) > right-bottom (quadrant_4)
N.B.: At the moment, this is a bug when used for any plate_format besides 384-wellplates - but is missing error raising.
I believe it is time to revisit this definition, and expand a standardized quadrant definition scheme across different plate_formats and quadrant_types for all of PLR.
N.B.: This is all part of what I consider “sample management” > subcategory: “process management”.
PLR should not dictate how programmers perform sample management, but instead PLR must empower sample management systems programmers might want to use.
Proposal
I propose the establishment of a modified .get_quadrant()
method in Plate()
which returns the desired quadrant, regardless of plate_format (!), based on 4 attributes:
- quadrant: int
- quadrant_type: Literal[ “checkerboard”, “block”]
- quadrant_indexing_pattern: Literal[“N”, “reverse_N”, “Z”, “reverse_Z”]
- quadrant_internal_fill_order: Literal[ “column-major”, “row-major”] = “column-major”
Quick examples:
new_wellplate_obj = Thermo_TS_96_wellplate_1200ul_Rb(
name="new_96_wellplate",
quadrant_type="block",
quadrant_indexing_pattern="reverse_N",
quadrant_internal_fill_order="column-major"
)
new_wellplate_obj.get_quadrant(1)
# returns list of 24 well identifiers in column-major order,
# i.e. ["A1", "B1", "C1", "D1", "A2", "B2", ... , "D6"]
new_wellplate_obj.get_quadrant(2)
# returns list of 24 well identifiers in column-major order,
# i.e. ["E1", "F1", "G1", "H1", "E2", "F2", ... , "H6"]
Explanations
(I am going to use 96-wellplates and 384-wellplates as examples but all plate_formats with an even number of columns and an even number of rows can be partitioned into “quadrants” / 4 zones <== assumption for this method).
Fill Order
All matrices, including plates, can be filled in two different orders:
-
Row-major order (row-wise filling)
- Filling proceeds across a row before moving to the next row.
- Mostly used by manual handling of plates.
-
Column-major order (column-wise filling)
- Filling proceeds down a column before moving to the next column.
- Common standard in robotic handling of plates due to parallelization opportunity of liquid transfer actions in the same x-coordinate(–> massive temporal acceleration)
Quadrant Types
Depending on 1) manual vs fully-automated processing, and 2) y-channel spacing ability, two common definitions for “quadrants” are:
- “block”
- division of plate into 4 zones based on the plate’s 2 symmetric axes
- “checkerboard”
- division of plate into 4 zones based on using every other row + every other column
Quadrant Indexing Pattern
Finally, quadrants must have an unambiguous indexing pattern (regardless of quadrant type chosen).
Four different quadrant indexing patterns can be used based on the letter created when the first wells of each quadrant are connected:
- N
- reverse_N
- Z
- reverse_Z
Design Considerations
Specifying all these attributes / design factors for quadrant use should be possible at the level of calling a quadrant, i.e.
wellplate_obj.get_quadrant(
quadrant=3,
quadrant_type="block",
quadrant_indexing_pattern="reverse_N",
quadrant_internal_fill_order="column-major"
)
…but realistically this would be incredibly tedious.
Therefore, the .get_quadrant()
method should have defaults that are defined during at the Plate()
instantiation level (as shown in the quick example above).
This means that an automator decides during Plate
instantiation to use one transfer pattern for their Plate
object during that automated protocol.
This aims to empower any sample management scheme programmers might want to use.
Implementation Proposal
This means:
Plate
has to be updated to take these attributes, defaulting all of them toNone
→ this ensures all existingPlate
definitions won’t be broken in the process.
(Pragmatic Programmer’s mindset: “Do No Harm”)Plate
’s.get_quadrant()
method has to be updated- an error should be raised when
Plate.get_quadrant()
is called on aPlate
object that has no defined quadrant attributes. - an error should be raised when
Plate.get_quadrant()
is called on aPlate
if thePlate
contains uneven row-numbers and/or uneven plate-numbers.
Please let me know what you think about this development proposal.