diff --git a/pylabrobot/resources/plate.py b/pylabrobot/resources/plate.py index a05f47f95f0..bb62a598e13 100644 --- a/pylabrobot/resources/plate.py +++ b/pylabrobot/resources/plate.py @@ -80,6 +80,7 @@ def __init__( lid: Optional[Lid] = None, model: Optional[str] = None, plate_type: Literal["skirted", "semi-skirted", "non-skirted"] = "skirted", + stacking_z_height: Optional[float] = None, ): """Initialize a Plate resource. @@ -89,6 +90,10 @@ def __init__( lid: Immediately assign a lid to the plate. plate_type: Type of the plate. One of "skirted", "semi-skirted", or "non-skirted". A more complete description is still pending. + stacking_z_height: The vertical pitch in mm between two identical plates stacked directly on + top of each other (i.e. the height a plate adds to a stack, equal to ``size_z`` minus the + overlap with the plate below). Required by some stacker devices (e.g. the Agilent BenchCel) + and left as ``None`` when unknown. """ super().__init__( @@ -103,6 +108,7 @@ def __init__( ) self._lid: Optional[Lid] = None self.plate_type = plate_type + self.stacking_z_height = stacking_z_height if lid is not None: self.assign_child_resource(lid) @@ -144,10 +150,31 @@ def unassign_child_resource(self, resource): self._lid = None return super().unassign_child_resource(resource) + def serialize(self) -> dict: + return { + **super().serialize(), + "plate_type": self.plate_type, + "stacking_z_height": self.stacking_z_height, + } + + def __eq__(self, other) -> bool: + # `stacking_z_height` is a physical dimension (the pitch a plate adds to a stack), so plates + # that differ in it are different labware and must not compare equal. + return ( + super().__eq__(other) + and isinstance(other, Plate) + and self.stacking_z_height == other.stacking_z_height + ) + + # Defining `__eq__` sets `__hash__` to None; restore the `Resource` (repr-based) hash. Equal + # plates share the same repr, so the hash/eq invariant holds. + __hash__ = Resource.__hash__ + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(name={self.name!r}, size_x={self._size_x}, " - f"size_y={self._size_y}, size_z={self._size_z}, location={self.location})" + f"size_y={self._size_y}, size_z={self._size_z}, " + f"stacking_z_height={self.stacking_z_height}, location={self.location})" ) def get_well(self, identifier: Union[str, int, Tuple[int, int]]) -> "Well": diff --git a/pylabrobot/resources/plate_tests.py b/pylabrobot/resources/plate_tests.py index 2139152d37e..fdfd43506b4 100644 --- a/pylabrobot/resources/plate_tests.py +++ b/pylabrobot/resources/plate_tests.py @@ -63,6 +63,57 @@ def test_add_lid_with_existing_lid(self): self.assertIsNone(plate.lid) +class TestStackingZHeight(unittest.TestCase): + def test_default_is_none(self): + plate = Plate("plate", size_x=1, size_y=1, size_z=15, ordered_items={}) + self.assertIsNone(plate.stacking_z_height) + + def test_stored(self): + plate = Plate("plate", size_x=1, size_y=1, size_z=15, ordered_items={}, stacking_z_height=12.5) + self.assertEqual(plate.stacking_z_height, 12.5) + + def test_serialize_round_trip(self): + plate = Plate( + "plate", + size_x=1, + size_y=1, + size_z=15, + ordered_items={}, + stacking_z_height=12.5, + plate_type="non-skirted", + ) + serialized = plate.serialize() + self.assertEqual(serialized["stacking_z_height"], 12.5) + self.assertEqual(serialized["plate_type"], "non-skirted") + + restored = Plate.deserialize(serialized) + self.assertEqual(restored.stacking_z_height, 12.5) + self.assertEqual(restored.plate_type, "non-skirted") + + def test_differs_in_equality(self): + # `stacking_z_height` is a physical dimension, so plates that differ in it are not equal. + def make(stacking_z_height): + return Plate( + "plate", + size_x=1, + size_y=1, + size_z=15, + ordered_items={}, + stacking_z_height=stacking_z_height, + ) + + self.assertEqual(make(12.5), make(12.5)) + self.assertNotEqual(make(12.5), make(13.0)) + self.assertNotEqual(make(12.5), make(None)) + + def test_remains_hashable(self): + plate = Plate("plate", size_x=1, size_y=1, size_z=15, ordered_items={}, stacking_z_height=12.5) + # equal plates hash equally; the object stays usable in sets/dicts. + other = Plate("plate", size_x=1, size_y=1, size_z=15, ordered_items={}, stacking_z_height=12.5) + self.assertEqual(hash(plate), hash(other)) + self.assertIn(plate, {plate}) + + class TestQuadrants(unittest.TestCase): def setUp(self): self.example_6_wellplate = Cor_Cos_6_wellplate_16800ul_Fb(name="example_6_wellplate")