PLR Feature Announcement: PlateAdapter

Hi everyone,

@rickwierenga and I have developed a new PLR feature, the creation of the resource class PlateAdapter, in pull request #152.
(Whenever possible we should write some brief announcements and documentation in this forum to explain what issue the new feature addresses, how it solves it, how to use it and what its current limitations are and how they might inform next steps, before adding a revised version to the docs.)


Summary of the current issue

Currently, whenever a resource (e.g. a carrier, plate, tiprack, …) is placed on a deck PLR gives you two options to declare the position of the new resource relative to its parent:

  1. hardcoding: i.e. you have to actively say location=Coordinate(x,y,z) to declare where the bottom-left / origin of your new resource is relative to the origin of the parent → this is suboptimal because every user has to figure our their specific coordinates for every resource before and after every movement.
  2. autoplacement: if location=None then PLR assumes you want to place the new resource directly on the parent, e.g. a Plate placed on a PlateCarrierSite will now share the same origin location. (there are some small differences to this rule for MFXModules)

The second mode is more convenient when moving plates during a run but fails when you want to place a plate on any of the following:

  • plate adapters
  • magnetic racks,
  • plate adapters for shakers and temperature control machines, and
  • on-deck thermocyclers (ODTCs).

As a consequence, PLR has so far not allowed the use of

  • semi-skirted plates, and
  • non-skirted plates
    because these plates cannot sit on a flat PlateCarrierSite by themselves.

The issue with placement on the above machines is that they are all representatives of PlateAdapters.
PlateAdapters are a block of metal or plastic in which holes are arranged in a grid that follows the ANSI SLAS 4-2004 (R2012) Well Position Standards.
This means the wells are spaced as follows: 96-well plate, 9x9mm^2; 384-well plate, 4.5x4.5mm^2; 1536-well plate, 2.25x2.25mm^2.

However, there are 2 complications to this standard:

  1. The well-grid is well standardized, the placement of the well-grid on the plate is not.
  2. PlateAdapters have a well standardized hole-grid to fit the well-grid, but PlateAdapters come in all shapes and sizes.

The positioning of the well-grid on a plate is given in PLR using Well.dx and Well.dy, denoting how far away the origin of the bottom-left well (“H1” for a 96-well plate) is from the origin of the plate. I have previously generated this infographic to visualise this and facilitate plate definition generation (though this shows a 24-well plate the same position arguments apply):

This means that Plate and PlateAdapter have their own dx and dy and Hole/Well.size_x.

The mission of a Plate PlateAdapter movement command is to align the 2D center of the bottom-left Plate.well with the 2D center of the bottom-left PlateAdapter.hole, despite their differences in relatve grid positioning.

Here is a visualisation of what happens if this is ignored and a Plate is moved onto a magnetic rack before this PR:


→ your run will fail


How PlateAdapter solves this issue

Issue 1: Complexity of automated placement of Plate onto PlateAdapter

  • We created a new PlateAdapter class in resources/plate_adapter.py.
  • When a new Plate is assigned to a PlateAdapter the PlateAdapter.assign_child_resource(Plate) method calculates the necessary location adjustment automatically.
  • PlateAdapter is therefore fundamentally different to other Resource subclasses because the location of a child that is assigned to it does not have to be hardcoded .

Issue 2: Missing Plate Support due to lack of PlateAdapter

  • With PlateAdapter working, semi- and non-skirted plates can now be used in PLR.

Examples

# Generate MFX carrier instances & assign to deck
shaker_carrier_1 = PLT_CAR_L4_SHAKER(name='shaker_carrier_1')
lh.deck.assign_child_resource(shaker_carrier_1, rails=1)

# Add heater-cooler
shaker_carrier_1[2] = hhc_1 = Hamilton_HC(name="hhc_1") # MFXModule in development

# Add PlateAdapter onto heater-cooler
hhc_adapter_188182_1 = Hamilton_96_adapter_188182(name="hhc_adapter_188182_1")
hhc_adapter_188182_1_location = hhc_1.child_resource_location + Coordinate(x=7.5, y=7.0, z=0.0)
hhc_1.assign_child_resource(hhc_adapter_188182_1 , location=hhc_adapter_188182_1_location)

With this deck setup any plate can now be moved onto the adapter on the heater-cooler even though different plates have their well-grid located differently on the plate. PlateAdapter automatically calculates the correct x-y position to place the plate.

await lh.move_plate(plate_x, hhc_adapter_188182_1) 

Limitations/Next Steps

At the moment only two PlateAdapters are defined in PLR:

  • Alpaqua_96_magnum_flx
  • Hamilton_96_adapter_188182

We encourage users to contribute more definitions to add to increase the number of PLR-supported PlateAdapters.

PlateAdapter only calculates the correct x-y position for placing the Plate at the moment.
The z-coordinate has to be empirically evaluated due to a.) complexities of Plate well-bottom geometries, and b.) a general issue regarding z-height definitions in Plate that is currently being solved.

Please use this thread to ask questions and add suggestions of how to make this feature more useful.

Happy automation :mechanical_arm:

4 Likes