PLR Carrier z-Height Issue

Hi everyone,

I was wondering whether there is a bug in the following PLR deck setup script:

from pylabrobot.resources import (
    PLT_CAR_L5AC_A00, Axy_24_DW_10ML,
)

plt_carrier_1 = PLT_CAR_L5AC_A00(name='plate_carrier_1')
lh.deck.assign_child_resource(plt_carrier_1, rails=13)
plt_carrier_1[4] = DWP_1 = Axy_24_DW_10ML(name = 'DWP_1')

# Checking labware position
plt_carrier_1.get_absolute_location(), plt_carrier_1.get_size_z()
# (Coordinate(x=370.0, y=63, z=100), 130.0) <=== ? z-coordiate + z-dimension = 230.0 mm ?

plt_carrier_1[0].get_absolute_location()
# Coordinate(x=374.0, y=71.5, z=186.15)

DWP_1.get_absolute_location()
# Coordinate(x=374.0, y=455.5, z=186.15)

The plate carrier is correctly stating its coordinate - starting at a z-height of 100mm.
But then it states that the carrier has a z-size of 130 mm.
This would mean that the top of the plate carrier is located at 230mm (absolute z-coordinate).

However, when looking at the carrier_sites associated with the carrier and the plate placed onto that carrier_site their absolute z origin is at 186.15 mm, not 230mm.

If this is a bug, I could imagine it hasn’t been causing any issues yet because the carrier is only used with its carrier_sites which are clearly working very well and are defined at 186.15mm z-coordinate.

Interested to have some more eyes on this :slight_smile:

Just testing some basic 3D-rendering ideas:

Resources do not necessarily exist on the top of another resource. In this case, the carrier sites are actually placed ‘inside’ the carrier:

Does this mean the blue line marked in your image is 43.85mm tall?
(I am back in the lab tomorrow to take some measurements as well)

230mm (z-coordinate of top of plate carrier) - 186.15 (z-coordinate of plate carrier site)
= equivalent to =
130mm (z-height of plate carrier) - 86.15 (z-coordinate of plate carrier site from z-height of deck)


Furthermore, I have asked this question before but haven’t found the time to look into this in more detail but this seems like a good place to bring it up again:
Is there a way to include automatic adjustment of the true plate position based on the carrier site skirt height?

If the dz of a plate is less than ~6mm, plates’ containers/wells will sit on the skirt and not on the plates ‘walls’. As a result, all wells’ absolute well location will higher than expected which endangers tips to crash when they are targeted for the bottom of a well.

I believe this would not have been a problem so far because only plate carriers with a skirt have been used and I’d assume everyone checks plate dimensions in the testing phase of a new automation run.
But with the integration of MFX modules, shakers and temp-control modules now, most of these don’t have a skirt, this skirt height becomes an issue: When moving a plate from a plate carrier with skirts to a module without a skirt, and performing liquid transfer actions on both sites the absolute well z locations will be different for the same plates.

It’s more like 2cm in this case. I’m using a PLT_CAR_L5_DWP.

For PLT_CAR_L5AC_A00, it’s about the same (75mm vs 95mm). The total height appears to be 95, even though Dim.Dz is 130 in the venus labware library (see PLT_CAR_L5AC_A00.tml).

Given the venus definition, 130-43.85=86.15 which is what I measure the skirt/surface on which the plate sits to be. This is the correct z-coordinate.

This is because the origin of the deck is actually 100mm above what you’d measure for the surface of the deck. :person_shrugging:

Very good point. I think we should examine a venus carrier file where this is the case to learn how these are calculated from ‘known’ parameters (known in the sense that they appear in venus labware definitions). I don’t think all information is stored in PLR definitions yet, but wouldn’t be hard to add once we know how this works.

allow me to link for future reference: https://forums.pylabrobot.org/t/labware-definitions-container-cross-section-carrier-type-plr/2739

edit: actually PLT_CAR_L5_DWP is an example. I’ll dig into it and share what I learn.

1 Like

Interesting. This image …

…is therefore evidence that this carrier/template definition is not physically accurate:

Because the total height of the carrier/template is not 130mm.
And I am not sure it makes a lot of sense that the “Template height” and the “Template clearance height” are the same, 130 mm.
It could be that VENUS has some requirements that we do not know about which could be why the “Template height” does not represent the true physical z height of the carrier.

But for PLR I think resource dimensions should represent the true dimensions of that resource to avoid unexpected behaviour, and I propose we change the values for carrier dimension to their real dimensions.

Agreed. Let’s replace it with the correct number, keeping in a comment where it differs from VENUS.

1 Like

In regards to plate carrier skirt integration I performed a small investigation, and things are actually quite different to what they seemed:

The skirt that is directly placed on top of the plate carrier actually has 3 different levels and looks like this:

I used the new probe_z_height function I created to acquire z-coordinates for all 3 levels of the skirt and calculate their mean across one carrier site:

  • top: 185.68 mm → pretty close to the VENUS-defined 186.15 mm (see “Template Sites” below); I guess for PLR, in general, sticking with this value has proven successful and deviations of 0.5mm can be expected with these injection-molded carrier sites.
  • middle: 180.95 mm → I’d say that can be rounded up to 181 mm
  • bottom: 179.26 mm

It seems like we were wrong: the carrier site is not the bottom of the skirt but the very top!

This is were things become interesting:

  1. Plates can either sit with their wells directly on the top of the skirt (carriers are then called “container-based”, or with their walls on the middle of the skirt (carriers are then called “rack-based”) - which one depends on the dz of the plate and whether the plate carrier site has a skirt or not.
  2. VENUS seems to check whether a plate’s dz is bigger than the distance between top-middle = 186.15 - 181 = 5.15 mm.
    If True then the plates’ z origin is set to 181 (the middle skirt level). If False then the wells are directly touching the top skirt.
  3. If False the z origin of wells is set to top of skirt + thickness of container base (I am not sure whether PLR definitions currently integrate this information?).

An illustration of if False, i.e. dz = 0.5 mm < 5.15 mm

It is exceedingly rare to find a plate whose dz is actually larger than 5.15 mm… except I recently added one to the PLR resource library: ProxiPlate 384-shallow well Plusdz ~ 8mm. (unfortunately I couldn’t find an accurate blue print of these plates to illustrate as above)

I am not sure how VENUS adjusts their plates’ z origin but it clearly can adjust them based on the same plate definitions for what Hamilton calls “container-based” as well as “rack-based” carriers, probably with the logic-based z origin adjustment mentioned above:

What do you guys think? How can we integrate this crucial information into PLR to ensure accurate positioning of plates of all different dzs?

1 Like

Scan of plate carrier using STAR’s inbuilt LLD, targeted for the carrier site:
download

Data (CSV-format):

x_list,y_list,lld_heights
490,155,179.2
490,146,179.2
490,137,180.9
490,125,179.3
490,115,179.2
490,105,179.3
490,92,181.0
490,75,179.6
520,155,179.1
520,146,185.7
520,137,185.6
520,125,185.7
520,115,185.7
520,105,185.8
520,92,185.7
520,75,179.4
545,155,179.0
545,146,185.6
545,137,185.6
545,125,185.7
545,115,185.7
545,105,185.7
545,92,185.7
545,75,179.4
575,155,179.1
575,146,185.7
575,137,185.6
575,125,185.7
575,115,185.7
575,105,185.7
575,92,185.7
575,75,179.5
610,155,179.2
610,146,179.2
610,137,180.9
610,125,179.2
610,115,179.1
610,105,179.2
610,92,181.0
610,75,179.6

1 Like

Great work on figuring this out and documenting this!

do you have an example of a number for a given plate?

wouldn’t you need to add the distance from the bottom of the plate to the bottom of wells? In your image it should be 15.5-15.1 I think.

exaggerated example:

I think it may be easier to find a plate with a low skirt, like the PLT_CAR_L5_DWP I have. I will investigate tonight or tomorrow.

Definitely something that should be done in assign_child_resource of CarrierSite.

In the example above:
image
…the thickness of container/well base is
15.5 mm (height plate top to bottom)
. - (15.1 mm [top of well to bottom of well] - 0.6 mm [elevation of well above plate top] =) 14.5
= 1.0 mm (distance of bottom of well to bottom of plate)
. - 0.5 mm (dz → distance of container/well shell bottom to plate bottom)
= 0.5 mm

Yep… a simple well is actually that complex :sweat_smile:

No, if the dz is indeed above 5.15mm then the plate wall simple sits on the middle level, making that level become the new z origin.
(Maybe I misunderstood the question tbh)

Interesting, I haven’t seen these carriers yet.

For the sake of unambiguity, should we call the last layer “step” of the skirt, the 5.15mm, in my example, the final_skirt_step which is the compared to the plate’s dz?
→ Different plates can have different dz and different plate sites can have different final_skirt_steps?

Are you sure? Users having to perform the calculation I showed above to find the correct position is quite a lot to ask.
Can we not integrate the thickness of container base into resource definitions just like in VENUS-based container definitions?

1 Like

The skirt vs non-skirt situation is becoming increasingly an issue:

I’m exposing MFX modules atm for PLR use.
A MFX module I have come across a lot is the MFX_DWP_module:

→ as can be seen here the MFX_DWP_module does not have a skirt, as opposed to the standard plate_carrier next to it.

Using the z-probing function I receive a mean z-height for this module of 178.73 mm.
The precise origin of the MFX_DWP_module’s MFXSite can then be easily tested out.

Next I placed a standard Cos_96_rd plate on the MFX_DWP_module’s MFXSite:

However, aspirating from the bottom of a well of this plate, i.e. lld_mode(0), crashes the tip into the bottom.

It appears that at least this plate’s PLR’s labware definitions has been using the wrong dz value:

To ensure we are talking about the same labware definitions, here is an infographic I made a while ago:

dz = marks the distance between the bottom of the plate and the bottom of a well/container

The PLR definition of Cos_96_Rd:

def Cos_96_Rd(name: str, with_lid: bool = False) -> Plate:
  """ Cos_96_Rd """
  return Plate(
    name=name,
    size_x=127.0,
    size_y=86.0,
    size_z=14.5,
    with_lid=with_lid,
    model="Cos_96_Rd",
    lid_height=10,
    items=create_equally_spaced(Well,
      num_items_x=12,
      num_items_y=8,
      dx=10.55,
      dy=8.05,
      dz=0.75,
      item_dx=9.0,
      item_dy=9.0,
      size_x=6.9,
      size_y=6.9,
      size_z=11.3,
      bottom_type=WellBottomType.U,
      cross_section_type=CrossSectionType.CIRCLE,
      compute_volume_from_height=_compute_volume_from_height_Cos_96_Rd,
    ),
  )

dz = 0.75 mm

Looking into the VENUS definition this arises from I tried to figure out where this number is coming from as it seems very low when looking at the plate:

Cos_96_Rd_container_def_page2a

→ the only number that corresponds with what PLR believes to be dz is actually the “Thickness of container base”.

Looking further into the definition of the entire rack:

  • plate_height = 14.5 mm
  • well/container_height_segment_1 = 10.7
  • well/container_height_segment_2 = 0.6

→ plate_height - container_height = 14.5 - (10.7+0.6) = 3.2 mm ← the actual dz value
(which includes the height/thickness_of_container_base (0.75mm) and a clearance underneath the well of 2.45 mm.

Can we please assess how many of the already defined PLR labware might be facing the same issue?


I also think I figured out why this hasn’t caused any issues so far:
Because so far only plate_carrier_sites with a skirt have been defined and used in PLR.
Looking at the same plate on one of those sites…

…the gap between the bottom of the plate and the carrier_site_level_1 becomes visible,
and now knowing that the plate_carrier_w_skirt top is actually the surface of the top layer, the dz = 0.75mm makes sense - only in this special circumstance - because there is indeed only the thickness of the plastic_bottom of the well in between the surface of the carrier_site and the bottom of the well.

This is not the true dz though as defined above, and hence all labware that uses its “thickness of container base” as its dz will cause crashes when placed on anything but a carrier_site with a skirt.

Changing the dz of Cos_96_Rd to 3.2mm leads to perfect aspirations/dispensation when placed on a flat surface such as the MFX_DWP_module.

I am not quite sure yet how to permanently correct this issue in PLR.
I do believe though that the dz value, defined as the distance between the bottom of the plate and the bottom of a well/container, is useful.
But if we keep it we have to add a true thickness_of_container_base variable to every labware that has been generated and add a logic function to plate_carriers_w_a_skirt to calculate the “skirt_adjusted_dz”.
Otherwise this issue will limit the use of PLR to skirted carrier_sites, and require specialised offsets for all other use cases because it will actually use wrong definitions for anything else.

1 Like

Thank you for bringing this up again and actually having an example of a carrier to base this off of. I was trying to get definitions for our PLT_CAR_L5_DWP carrier which is similar in that the skirt is very low. Unfortunately, I have no Hamilton definition for this to get the ground truth (tracking in thread https://forums.pylabrobot.org/t/plt-car-l5-dwp/3105).

Not surprising: the value we get when parsing Hamilton resources is actually named Cntr.1.base in the rck files/labware definitions. As you wrote later in your post, this is the dz between the carrier site skirt and well bottom in some cases but wrong in others. (Historical background / explanation why: I was looking at just these files and firmware commands, trying to figure out how these numbers mapped to each other and what a sensible model would be, and actually only learned about the labware editor recently!)

The value (10.7+0.6)=11.3 actually exists in PLR as the well.size_z.

This would be the case for all resources imported from venus. For Opentrons, we get the z for wells from whatever the "z" key in their resources corresponds to. I’m not sure about Tecan/evoware resources.

Let me start by making explicit that dz is only a paremeter of create_equally_spaced and is not tracked in this way in PLR. Instead, it is used in well.location.z: the well location wrt the plate. Currently, these values are wrong (as you pointed out) but let’s assume we fixed them (easy).

I still believe CarrierSite.assign_child_resource. In cases where the skirt is high enough, the plate is essentially ‘floating’ and the plate should not exist at 0,0,0 wrt CarrierSite, but at 0,0,z’ where z' = carrier_skirt_height - well.location.z. well.location.z may seem odd, but it’s actually the dz. This way, the plate ‘floats’ (z>0) a little above the carrier, and wells obviously float in the plate. Having the Plates float on the carriersite is a semantic choice, but in my opinion the obvious one. For plates sitting directly on the carrier site, the z=0, of course. In this way of thinking, you’d hope CarrierSite.assign_child_resource can automatically assign a Plate to this location based of its well.location.dz and plate.size_z.

Oddly, the carriersite z position (as seen in the labware editor and as imported in PLR) actually refers to the top of the skirt, I just measured that with a ruler. This makes sense when you think that plates are just assigned directly on top of sites right now, and wells are seen as starting 2mm (their thickness) above the plate. We’d have to move the carrier sites down by the carrier_skirt_height to make this all work correctly.

Now unfortunately, we don’t know the height of the skirt. I measure it to be 6.5mm for PLT_CAR_L5AC_A00, but this (approximate) value is not visible in the labware editor or the tml file as far as I can tell. However, I do see this:

which corresponds to what you wrote above: " Plates can either sit with their wells directly on the top of the skirt (carriers are then called “container-based”, or with their walls on the middle of the skirt (carriers are then called “rack-based”) - which one depends on the dz of the plate and whether the plate carrier site has a skirt or not."

It may be impossible for us to automatically determine whether a carrier is “rack” or “container based”, whether to assign a plate at z=0 or z=some other. This is obviously determined by the plate<>carrier combination used, but maybe not for Hamilton. :frowning:

We could 1) crowd-source the skirt-heights, and quickly reach all carriers people are actually using or 2) we could import the ‘z=0’ vs ‘z>0’ (“rack vs container based”) information from Hamilton files. How do you feel about 1?

Excellent, this will be very useful to solve this issue.


I think this is missing that the CarrierSite z-height is actually the top surface of the skirt:

→ This means, when using plate_carriers with a skirt, the plate is actually not floating in relationship to the carrier_site origin but it is “sunk” into it → which is what causes the crashes when placed on anything that doesn’t contain a skirt.

I agree with your idea here, just wanting to amend that we have to subtract, not add z in the case of carrier_sites with skirt to represent the “sunk”/“embedded”(?) not floating plate.

Just noticed that is what you mean here I believe:

I measured this precisely for PLT_CAR_L5AC_A00 using the z-probing function + the VENUS definitions in this thread a little while ago:


1 Like

I believe we can actually make PLR recognise whether a carrier_site is “rack” or “container”-based using your suggestion 1)!
But this is not enough.
In addition to…

  • adding a skirt_height class variable to all carrier_sites we have to
  • add the thickness_of_container_base as a class variable to every labware definition.

The reason is that we need both pieces of information to correctly adjust the position of the z-origin of the plate if dz - thickness_of_container_base < skirt height.

→ If there is no skirt the skirt_height == 0 and this conditional adjustment of the plate_z_origin will never be triggered.


Proposal

Step 1: Always calculate true dz

Instead of calculating the well.location.z from the dz (distance between the bottom of the plate and the bottom of a well/container),
I believe we can calculate the well.location.z from plate.size_z - well.size_z as we discussed here:

Step 2: Add skirt_height & thickness_of_container_base class variables

Imagine a new class variable called skirt_height associated to all plate_carriers (easily added through the base class PlateCarrier().
We can modify the create_carrier_sites and create_homogeneous_carrier_sites functions to also take and assign skirt_height to each carrier_site.
→ This is how PLR can distinguish between “rack” and “container”-based carriers!
→ If skirt_height == 0 → it is a “container”-based carrier.

Furthermore, we should add a thickness_of_container_base to all labware definitions because this is what is needed to correctly adjust the z-position as shown in my infographic above.
A default value of 0.8 mm seems reasonable to me and we can adjust it over time for labware that people are using - occasionally manufacturers provide this information in their spec sheets as well.

I believe that making these 3 modifications will allow correct pipetting from plates from any position they are placed on (which is very important for the future integration of heater-shakers, temperature-controllers, ODTCs, tilt-modules, …).
Otherwise moving plates around - although possible with the iSWAP and CO-RE grippers - will result in crashes when these plates are used in their different locations.


1 Like

In the current model, yes. What I described was the hypothetical updated model.

Sinking is an alternative to floating. I like that it works without knowing the carrier_skirt_height.

We essentially have both: thickness_of_container_base is currently well.location.z. And the actual dz = plate.size_z - well.size_z.

We should probably do this in the resource writer tool. This makes the most sense, and we should do this anyway. Then the question is how we update carriers: sinking or floating plates?

Exactly. The problem is we don’t really know skirt_height, right? Sure we can measure it, but that’s a choice we’d have to make (as I put option 1 above). (option 2 is to import ‘rack’/‘carrier’ based as a bool.) The third approach which I did not mention is sinking plates instead of floating them. This approach is growing on me because it’s easy to implement with current data and allows us to improve Plate definitions simultaneously.

1 Like

ahhhh, I see now what you mean → floating means changing the carrier_site z-height to the middle layer of the skirt, i.e. the layer that the plate would sit on if the clearance_height below wells is bigger than the skirt.

…which is of course what you wrote here:

…which means this approach would still require knowledge of the carrier_skirt_height too?

I am not sure how this ‘floating’ approach would work for carrier_sites with carrier_skirt_height=0?

Of course! One of the problems is that the dz is currently set to the thickness_of_container_base → hence we already have that information. I completely overlooked that; thank you for pointing that out, Rick.

…which means we actually already have 2 / 3 variables we need:
We have…

  • thickness_of_container_base → currently wrongly named dz
  • dz → easily calculatable with plate.size_z - well.size_z (assuming that plate_top and well_top are flush with one another in the plate_definition… :grimacing: )

…and we only need…

  • carrier_skirt_height → this is the difference between skirt_top_layer - skirt_middle_layer → but, to make this easier we can look up what the skirt_top_layer is defined as in VENUS, i.e. its current PLR z-height:

…verify it using z-probing, and then carefully z-probe the skirt_middle_layer?

I am in favour of the sinking model + option 1:
this is how VENUS appears to adjust its position + we would not have to change VENUS-based definitions of plate_carriers, we’d just add a carrier_skirt_height class variable instead, which I hope will make it easier for future users (including future us) to look back at PLR and VENUS definitions and see how they are connected.

Plus what you said:

This means plate_origins are adjusted when assigning plates to plate_carrier_sites based on the their combined plate-carrier_site properties, which I think will make this easily adjustable - and the majority of future users hopefully will never have to worry about this because PLR solves it all in the background for them/us :slight_smile: