Are PLR Resource Definitions Different for Opentrons Machines?

Hi everyone,

PLR normally uses a consistent resource definition scheme, assigning child resources to parent resources at specific locations.
These locations mark the left-front-bottom of a cuboid that encompasses the entire child resource.
This is what I found for all Hamilton-specific and general resource definitions in the PLR Resource Library.

However, this does not appear to be the case for resources in the pylabrobot/resources/opentrons/ folder (or at least the couple I looked into).


Everything on pylabrobot:main

Example 1

from pylabrobot.resources import (
    Porvair_6_reservoir_47ml_Vb, # Plate contributed by me
    agilent_1_reservoir_290ml # Plate from Opentrons folder
)

Porvair_6_reservoir_47ml_Vb(name="test")
>>> Plate(name=test, size_x=127.0, size_y=86.0, size_z=44, location=None)

Porvair_6_reservoir_47ml_Vb(name="test").children
>>> [Well(name=test_well_0_0, location=(009.300, 005.700, 002.240), size_x=16.8, size_y=70.8, size_z=42.5, category=well),
>>>  Well(name=test_well_1_0, location=(027.800, 005.700, 002.240), size_x=16.8, size_y=70.8, size_z=42.5, category=well),
>>>  Well(name=test_well_2_0, location=(046.300, 005.700, 002.240), size_x=16.8, size_y=70.8, size_z=42.5, category=well),
>>>  Well(name=test_well_3_0, location=(064.800, 005.700, 002.240), size_x=16.8, size_y=70.8, size_z=42.5, category=well),
>>>  Well(name=test_well_4_0, location=(083.300, 005.700, 002.240), size_x=16.8, size_y=70.8, size_z=42.5, category=well),
>>>  Well(name=test_well_5_0, location=(101.800, 005.700, 002.240), size_x=16.8, size_y=70.8, size_z=42.5, category=well)]

agilent_1_reservoir_290ml(name="test")
>>>  Plate(name=test, size_x=127.76, size_y=85.57, size_z=44.04, location=None)

agilent_1_reservoir_290ml(name="test").children
>>>  [Well(name=test_A1, location=(063.880, 042.785, 004.820), size_x=108, size_y=72, size_z=39.22, category=well)]

→ The Opentrons-defined and -extracted PLR definition believes that the well/reservoir starts in the center of the plate.
→ All standard commands without synthetic offsets will crash.


Example 2

I thought that maybe this is just a single wrongly defined import from the Opentrons library and looked further.

To properly compare I looked for Opentrons library plates that have a similar counterpart in the remaining PLR Resource Library:

  • I recently contributed the definition to Cos_6_MWP_16800ul_Fb, Costar cat no.: 3516
  • The Opentrons library has a similar predefined plate: corning_6_wellplate_16point8ml_flat, Costar cat no.: 3335
from pylabrobot.resources import ( 
    Cos_6_MWP_16800ul_Fb, # Plate contributed by me
    corning_6_wellplate_16point8ml_flat # Plate from Opentrons folder
)

Cos_6_MWP_16800ul_Fb(name="test")
>>>  Plate(name=test, size_x=127.0, size_y=86.0, >>>  size_z=19.85, location=None)

Cos_6_MWP_16800ul_Fb(name="test").children
>>>  [Well(name=test_well_0_0, location=(007.000, 043.900, 000.300), size_x=35.0, size_y=35.0, size_z=17.5, category=well),
>>>  Well(name=test_well_0_1, location=(007.000, 005.450, 000.300), size_x=35.0, size_y=35.0, size_z=17.5, category=well),
>>>  Well(name=test_well_1_0, location=(045.450, 043.900, 000.300), size_x=35.0, size_y=35.0, size_z=17.5, category=well),
>>>  Well(name=test_well_1_1, location=(045.450, 005.450, 000.300), size_x=35.0, size_y=35.0, size_z=17.5, category=well),
>>>  Well(name=test_well_2_0, location=(083.900, 043.900, 000.300), size_x=35.0, size_y=35.0, size_z=17.5, category=well),
>>>  Well(name=test_well_2_1, location=(083.900, 005.450, 000.300), size_x=35.0, size_y=35.0, size_z=17.5, category=well),
>>>  Lid(name=test_lid, location=(000.000, 000.000, 017.850), size_x=127.0, size_y=86.0, size_z=2, category=lid)]

corning_6_wellplate_16point8ml_flat(name="test").children
>>> Plate(name=test, size_x=127.76, size_y=85.47, size_z=20.27, location=None)

corning_6_wellplate_16point8ml_flat(name="test").children
>>> [Well(name=test_A1, location=(024.760, 062.280, 002.870), size_x=25.053, size_y=25.053, size_z=17.4, category=well),
>>>  Well(name=test_B1, location=(024.760, 023.160, 002.870), size_x=25.053, size_y=25.053, size_z=17.4, category=well),
>>>  Well(name=test_A2, location=(063.880, 062.280, 002.870), size_x=25.053, size_y=25.053, size_z=17.4, category=well),
>>>  Well(name=test_B2, location=(063.880, 023.160, 002.870), size_x=25.053, size_y=25.053, size_z=17.4, category=well),
>>>  Well(name=test_A3, location=(103.000, 062.280, 002.870), size_x=25.053, size_y=25.053, size_z=17.4, category=well),
>>>  Well(name=test_B3, location=(103.000, 023.160, 002.870), size_x=25.053, size_y=25.053, size_z=17.4, category=well)]

→ It is now clear that multiple plates that have been imported from the Opentrons library are not correctly defined in PLR.

There appear to be multiple discrepancies also when comparing the PLR Opentrons definition to the Opentrons library website entry itself:

  • The wells’ diameters have not been correctly transferred into the PLR_well.size_x / y
  • Opentrons uses as different grid-offset system - based on the first well’s center (see Opentrons Labware Creator), while PLR uses one based on the first well’s edges - which can explain some of these differences.

I wondered whether this was on purpose?
Does PLR require resources to be defined differently for Opentrons machines?

1 Like

Thanks for pointing this out. This is a totally fixable situation.

Background: I presume people mostly use opentrons resources with the OT robots. This is why this has not turned out to be an issue (although I’m pretty sure I checked using a venus resource on the OT once, and that worked fine). But: resource definitions in PLR should be hardware agnostic and as you point out, this is an error.

Defining resources on OT requires us to “define” these resources on the Opentrons onboard raspberry Pi, and then to “add” a defined resource to the deck. The lifetime of definitions is the same as the “protocol”/“run” (OT terms), which is the period between lh.setup and lh.stop in PLR. The reason you have to mirror resources on the OT is because commands like aspirate expect us to specify eg a well by name instead of simply expecting coordinates (like Hamilton). It is a bit cumbersome, but that’s what we have to work with (until we run PLR software on the onboard OT rpi). The OT resource definition format is quite extensive, and thus far it has been possible to get pretty good definitions uploaded to the OT.

The deck layout does work slightly differently. On OT, the tree is generally only three layers: the deck, a high-level resource like plate or tip rack, and then children (children are always known as “wells”, even for tip racks). The tree has to be three layers though, because you can only use third-layer resources for pipetting operations. For example, you want to put a single beaker on the deck, you have to make sure this beaker has a parent. See en example here: Lennart Justen - Ten-fold serial dilution (linked the specific section). Exceptions to the three-layer requirement as specialized machines like the heater module.

Fix: on loading OT resources, we should convert them to the PLR format. On assignment, we should convert PLR-format back to OT format. I created an issue assigned to myself: Make OT resource loading uniform with the rest · Issue #164 · PyLabRobot/pylabrobot · GitHub.

1 Like

Thank you @rickwierenga for the detailed explanation!

So in summary, the answer to the question
“Are PLR Resource Definitions Different for Opentrons Machines?” is
“Yes, at the moment, but it is a tradeoff that we want to change, i.e. we want to make all PLR resources uniformly defined to enable any resource to be used on any machine, which is currently not the case”?

Ohh my, I believe this plus the fact that the RPi isn’t that powerful could explain why I always found the OT-2 to take a while to process and then execute a command, i.e. it is very slow.

Do you think a generic convert_PLR_to_OT_definition(resource: Resource, category="plate") function could do the job?
I imagine it would first focus on plates and can later be supplemented with definition conversions of other resources?

I imagine this could be called at instantiation of the OTDeck to convert any PLR resource that is placed upon it (and its deck tree) into its corresponding OT definition?

Do we know why this is the case?
One of the most powerful features of PLR, imo, is the “Lego building” it provides on Hamilton machines, i.e. simple, fast and unlimited resource on top of resource creation and movement. It seems like quite a handicap not to have that.
(I am specifically thinking about people wanting to place a scale on the OT-2 to use PLR to actually test liquid transfer parameters / create liquid classes, a feature that to my knowledge Opentrons is currently not talking about. Of course, there is a way to do this with 3 layers but why not make it easier?)

(Note: I am not really interested in development of the OT-2 for PLR / cannot test anything, which means that someone with an OT-2 would ideally voice what they think would be best for their system and workflows.
I am mostly interested in upgrading the PLR Resource Library.)

I don’t think there is a tradeoff. Otherwise, good summary.

I think I will keep doing this in the OpentronsBackend.assigned_resource_callback. I already do a lot of this conversion. I just need to change the location of wells to be lfb instead of ccb. It’s a just-in-time conversion, just after resource assignment.

Tip racks and plates are what are currently supported. For other resources, it’s a bit harder and requires some hacky things in PLR as Lenni showed above.

It is simply a quirk of the OT api.

(I was thinking about building my own OT-rpi side server/controller for use with PLR, in the style of Hamilton where it expects the bare minimum like xyz coordinates, but I don’t think it’s worth it because not a lot of people use OT with PLR. PLR’s benefit is smaller given they already have a Python API and I think their hardware inaccuracies are limiting, not software. The current software was too complicated for me to quickly hack this together.)

I figured. Assume this will be solved soon-ish (<1w), and that we are uniform across all robots. We can always do a PLR->anything conversion on the backend.

Thank you, @rickwierenga!

In this regard, how can the visualizer still correctly visualize resources with PLR-definitions and Opentrons-definitions even though they are, at the moment, different?

I am asking because it seems like the visualizer is incorrectly displaying the position of wells if

  • the plate is not defined in the opentrons folder AND
  • cross_section_type=CrossSectionType.RECTANGLE, e.g.:
plt_car = PLT_CAR_L5AC_A00(name="plate carrier")

# Test standard plate - CrossSectionType.CIRCLE
plt_car[0] = plate_1 = Cos_96_DW_1mL(name="aspiration plate")

# Test user-defined plate - CrossSectionType.CIRCLE
plt_car[1] = plate_2 = Cos_6_wellplate_16800ul_Fb(name="dispense plate")

# Test user-defined plate - CrossSectionType.RECTANGLE #1
plt_car[2] = Axy_24_DW_10ML_1 = Axy_24_DW_10ML(name="Axy_24_DW_10ML_1")

# Test user-defined plate - CrossSectionType.RECTANGLE #2
plt_car[3] = Porvair_6_reservoir_47ml_Vb_1 = Porvair_6_reservoir_47ml_Vb(name="Porvair_6_reservoir_47ml_Vb_1")

# Test PLR-Opentrons-defined plate - CrossSectionType.RECTANGLE
plt_car[4] = agilent_1_reservoir_290ml_1 = agilent_1_reservoir_290ml(name="agilent_1_reservoir_290ml_1")

lh.deck.assign_child_resource(plt_car, rails=1)

The issue is indeed with cells being drawn from the center and rectangles from the left bottom.


gives

next, I will fix the OT definition agilent_1_reservoir_290ml_1 (along with other OT definitions)

1 Like

1 Like