Skip to content

Implement pedestal for CarrierSite & MFXModule (fix PLR Plate location 1) #143

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/img/pedestal/measure.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ PyLabRobot provides a layer of general-purpose abstractions over robot functions
resources/introduction
resources/custom-resources
resources/hamilton_parse
resources/pedestal_size_z


.. toctree::
Expand Down
15 changes: 15 additions & 0 deletions docs/resources/pedestal_size_z.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Pedestal z height

> ValueError("pedestal_size_z must be provided. See https://docs.pylabrobot.org/pedestal_size_z for more information.")

Many plate carriers feature a "pedestal" or "platform" on the sites. Plates can sit on this pedestal, or directly on the bottom of the site. This depends on the pedestal _and_ plate geometry, so it is important that we know the height of the pedestal.

The pedestal information is not typically available in labware databases (like the VENUS or EVOware databases), and so we rely on users to measure and contribute this information.

Here's how you measure the pedestal height:

![Pedestal height measurement](/img/pedestal/measure.jpeg)

Once you have measured the pedestal height, you can contribute this information to the PyLabRobot Labware database. Here's a guide on contributing to the open-source project: https://docs.pylabrobot.org/how-to-open-source.html#quick-changes.

For background, see PR 143: https://github.com/PyLabRobot/pylabrobot/pull/143.
1 change: 1 addition & 0 deletions pylabrobot/resources/alpaqua/magnetic_racks.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ def Alpaqua_96_magnum_flx(name: str) -> PlateAdapter:
site_pedestal_z=6.2,
model="Alpaqua_96_magnum_flx",
)

41 changes: 36 additions & 5 deletions pylabrobot/resources/carrier.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import annotations

import logging
from typing import List, Optional, Type, TypeVar, Union
from typing import Generic, List, Optional, Type, TypeVar, Union

from .coordinate import Coordinate
from .plate import Plate
from .resource import Resource


Expand Down Expand Up @@ -36,7 +37,10 @@ def __eq__(self, other):
return super().__eq__(other) and self.resource == other.resource


class Carrier(Resource):
S = TypeVar("S", bound=Resource)


class Carrier(Resource, Generic[S]):
""" Abstract base resource for carriers.

It is recommended to always use a resource carrier to store resources, because this ensures the
Expand Down Expand Up @@ -74,7 +78,7 @@ def __init__(
self,
name: str,
size_x: float, size_y: float, size_z: float,
sites: Optional[List[CarrierSite]] = None,
sites: Optional[List[S]] = None,
category: Optional[str] = "carrier",
model: Optional[str] = None):
super().__init__(name=name, size_x=size_x, size_y=size_y, size_z=size_z, category=category,
Expand Down Expand Up @@ -182,6 +186,33 @@ def __init__(
sites,category=category, model=model)


class PlateCarrierSite(CarrierSite):
""" A single site within a plate carrier. """
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
pedestal_size_z: float = None, # type: ignore
category="plate_carrier_site", model: Optional[str] = None):
super().__init__(name, size_x, size_y, size_z, category=category, model=model)
if pedestal_size_z is None:
raise ValueError("pedestal_size_z must be provided. See "
"https://docs.pylabrobot.org/pedestal_size_z for more information.")

self.pedestal_size_z = pedestal_size_z
self.resource: Optional[Plate] = None # fix type
# TODO: add self.pedestal_2D_offset if necessary in the future

def assign_child_resource(self, resource: Resource, location: Coordinate = Coordinate.zero(),
reassign: bool = True):
if not isinstance(resource, Plate):
raise TypeError(f"PlateCarrierSite can only store Plate resources, not {type(resource)}")

# TODO: add conditional logic to modify Plate position based on whether
# pedestal_size_z>plate_true_dz OR pedestal_z<pedestal_size_z IF child.category == 'plate'
return super().assign_child_resource(resource, location, reassign)

def serialize(self) -> dict:
return { **super().serialize(), "pedestal_size_z": self.pedestal_size_z, }


class PlateCarrier(Carrier):
""" Base class for plate carriers. """
def __init__(
Expand All @@ -190,7 +221,7 @@ def __init__(
size_x: float,
size_y: float,
size_z: float,
sites: Optional[List[CarrierSite]] = None,
sites: Optional[List[PlateCarrierSite]] = None,
category="plate_carrier",
model: Optional[str] = None):
super().__init__(name, size_x, size_y, size_z,
Expand Down Expand Up @@ -272,7 +303,7 @@ def create_homogeneous_carrier_sites(
site_size_x: float,
site_size_y: float,
**kwargs
) -> List[T]:
) -> List[T]:
""" Create a list of carrier sites with the same size. """

n = len(locations)
Expand Down
6 changes: 4 additions & 2 deletions pylabrobot/resources/deck_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Deck,
Plate,
PlateCarrier,
PlateCarrierSite,
Resource,
TipCarrier,
TipRack,
Expand Down Expand Up @@ -64,8 +65,9 @@ def test_json_serialization_standard(self):
item_dx=1, item_dy=1,
size_x=1, size_y=1,
make_tip=standard_volume_tip_with_filter))
pc = PlateCarrier("pc", 100, 100, 100, sites=create_homogeneous_carrier_sites(klass=CarrierSite,
locations=[Coordinate(10, 20, 30)], site_size_x=10, site_size_y=10))
pc = PlateCarrier("pc", 100, 100, 100, sites=create_homogeneous_carrier_sites(
klass=PlateCarrierSite, locations=[Coordinate(10, 20, 30)], site_size_x=10, site_size_y=10,
pedestal_size_z=0))
pc[0] = Plate("plate", 10, 20, 30,
items=create_equally_spaced_2d(Well,
num_items_x=1, num_items_y=1,
Expand Down
3 changes: 2 additions & 1 deletion pylabrobot/resources/hamilton/hamilton_decks.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ def summary(self) -> str:
"Build a layout first by calling `assign_child_resource()`. "
)

exclude_categories = {"well", "tube", "tip_spot", "carrier_site"} # don't print these
# don't print these
exclude_categories = {"well", "tube", "tip_spot", "carrier_site", "plate_carrier_site"}

def find_longest_child_name(resource: Resource, depth=0):
""" DFS to find longest child name, and depth of that child, excluding excluded categories """
Expand Down
3 changes: 2 additions & 1 deletion pylabrobot/resources/hamilton_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
MFXCarrier,
Plate,
PlateCarrier,
PlateCarrierSite,
TipCarrier,
TipRack,
TipSpot,
Expand Down Expand Up @@ -312,7 +313,7 @@ def create_plate_carrier_for_writing(filepath: str) -> Tuple[PlateCarrier, Optio
size_x=size_x,
size_y=size_y,
size_z=size_z,
sites=create_homogeneous_carrier_sites(klass=CarrierSite, locations=sites,
sites=create_homogeneous_carrier_sites(klass=PlateCarrierSite, locations=sites,
site_size_x=site_width, site_size_y=site_height),
model=cname
)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 20 additions & 13 deletions pylabrobot/resources/ml_star/mfx_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,42 @@ class MFXModule(Resource):

Examples:
1. Creating MFX module for tips:
Creating a `MFXCarrier`,
Creating a `MFXModule` for tips,
Assigning the `MFXModule` for tips to a carrier_site on the `MFXCarrier`,
Creating and assigning a tip_rack to the MFXsite on the MFXModule:

Create a `MFXCarrier`,
Create `MFXModule` for tips,
Assign the `MFXModule` for tips to a carrier_site on the `MFXCarrier`,
Create and assign a tip_rack to the MFXModule:
>>> mfx_carrier_1 = MFX_CAR_L5_base(name='mfx_carrier_1')
>>> mfx_carrier_1[0] = mfx_tip_module_1 = MFX_TIP_module(name="mfx_tip_module_1")
>>> mfx_tip_module_1[0] = tip_50ul_rack = TIP_50ul_L(name="tip_50ul_rack")
>>> tip_50ul_rack = TIP_50ul_L(name="tip_50ul_rack")
>>> mfx_tip_module_1.assign_child_resource(tip_50ul_rack)

2. Creating MFX module for plates:
Use the same `MFXCarrier` instance,
Creating a `MFXModule` for plates,
Assigning the `MFXModule` for plates to a carrier_site on the `MFXCarrier`,
Creating and assigning a plate to the MFXsite on the MFXModule:
Create a `MFXModule` for plates,
Assign the `MFXModule` for plates to a carrier_site on the `MFXCarrier`,
Create and assign a plate directly to the MFXModule:

>>> mfx_carrier_1[1] = mfx_dwp_module_1 = MFX_DWP_module(name="mfx_dwp_module_1")
>>> mfx_dwp_module_1[0] = Cos96_plate_1 = Cos_96_Rd(name='Cos96_plate_1')
>>> mfx_carrier_1[1] = mfx_dwp_module_1 = MFX_DWP_rackbased_module(name="mfx_dwp_module_1")
>>> Cos96_plate_1 = Cos_96_Rd(name='cos96_plate_1')
>>> mfx_dwp_module_1.assign_child_resource(Cos96_plate_1)
"""

def __init__(
self,
name: str,
size_x: float, size_y: float, size_z: float,
child_resource_location: Coordinate,
skirt_height: float = 0,
category: Optional[str] = "mfx_module",
pedestal_size_z: Optional[float] = None,
model: Optional[str] = None):
super().__init__(name=name, size_x=size_x, size_y=size_y, size_z=size_z, category=category,
model=model)
# site where resources will be placed on this module
self._child_resource_location = child_resource_location
self._child_resource: Optional[Resource] = None
self.skirt_height = skirt_height
self.pedestal_size_z: Optional[float] = pedestal_size_z
# TODO: add self.pedestal_2D_offset if necessary in the future

@property
def child_resource_location(self) -> Coordinate:
Expand All @@ -65,6 +68,10 @@ def assign_child_resource(
):
""" Assign a resource to a site on this module. If `location` is not provided, the resource
will be placed at `self._child_resource_location` (wrt this module's left front bottom). """

# TODO: add conditional logic to modify Plate position based on whether
# pedestal_size_z>plate_true_dz OR pedestal_z<pedestal_size_z IF child.category == 'plate'

if self._child_resource is not None and not reassign:
raise ValueError(f"{self.name} already has a child resource assigned")
super().assign_child_resource(
Expand Down
Loading
Loading