From 8307b3493af63e84a353bf4f7676b2087102fb11 Mon Sep 17 00:00:00 2001 From: Eric <113262615+ericguan04@users.noreply.github.com> Date: Tue, 3 Jun 2025 16:13:09 -0400 Subject: [PATCH 1/6] fix: resource stack handling for Lid in LiquidHandler and PlateHolder. Specifically, Lid will now be assigned to parent Plate if top item of ResourceStack is Plate. Change to carrier.py ensures that ResourceStack is inferred correctly (up to two levels of parent above, since Lid may be child of plate, or directly a child of ResourceStack). Future stability improvements: Geometric Plate comptability check before assigining as child. If the plate will not slot along skirting, it should not register as a child. --- .vscode/settings.json | 3 ++- pylabrobot/liquid_handling/liquid_handler.py | 23 ++++++++++++++------ pylabrobot/resources/carrier.py | 12 +++++++++- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 714c508189..df11ff2a8d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,5 +45,6 @@ "editor.wordWrap": "bounded", "editor.wordWrapColumn": 100 }, - "mypy.runUsingActiveInterpreter": true + "mypy.runUsingActiveInterpreter": true, + "ros.distro": "humble" } diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index 0224803212..80fd9abb17 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -1813,7 +1813,7 @@ async def drop_resource( ) # get the location of the destination - if isinstance(destination, ResourceStack): + if isinstance(destination, ResourceStack) and not isinstance(resource, Lid): assert ( destination.direction == "z" ), "Only ResourceStacks with direction 'z' are currently supported" @@ -1835,13 +1835,22 @@ async def drop_resource( resource.rotated(z=resource_rotation_wrt_destination_wrt_local) ).rotated(destination.get_absolute_rotation()) to_location = destination.get_absolute_location() + adjusted_plate_anchor - elif isinstance(destination, Plate) and isinstance(resource, Lid): + elif isinstance(destination, (Plate, ResourceStack)) and isinstance(resource, Lid): lid = resource - plate_location = destination.get_absolute_location() - child_wrt_parent = destination.get_lid_location( - lid.rotated(z=resource_rotation_wrt_destination_wrt_local) - ).rotated(destination.get_absolute_rotation()) - to_location = plate_location + child_wrt_parent + if isinstance (destination, ResourceStack): + if destination.direction != "z": + raise ValueError("Only ResourceStacks with direction 'z' are currently supported") + top_item = destination.get_top_item() + if isinstance(top_item, Plate): + destination = top_item + else: + to_location = destination.get_absolute_location(z="top") + if isinstance(destination, Plate): + plate_location = destination.get_absolute_location() + child_wrt_parent = destination.get_lid_location( + lid.rotated(z=resource_rotation_wrt_destination_wrt_local) + ).rotated(destination.get_absolute_rotation()) + to_location = plate_location + child_wrt_parent else: to_location = destination.get_absolute_location() diff --git a/pylabrobot/resources/carrier.py b/pylabrobot/resources/carrier.py index 6eabb1de3c..edf7055100 100644 --- a/pylabrobot/resources/carrier.py +++ b/pylabrobot/resources/carrier.py @@ -234,7 +234,17 @@ def _update_resource_stack_location(self, resource: Resource): Args: resource: The Resource on the ResourceStack tht was assigned. """ - resource_stack = resource.parent + + if isinstance(resource, Lid): + lid_parent = resource.parent + if isinstance(lid_parent, ResourceStack): + resource_stack = lid_parent + elif isinstance(lid_parent.parent, ResourceStack): + resource_stack = lid_parent.parent + else: + raise ValueError("ResourceStack not found for Lid") + else: + resource_stack = resource.parent assert isinstance(resource_stack, ResourceStack) if resource_stack.children[0] == resource: resource_stack.location = self.get_default_child_location(resource) From 5acdc82ae5fb6ebae395c624bd5a4c9926b614cd Mon Sep 17 00:00:00 2001 From: Eric <113262615+ericguan04@users.noreply.github.com> Date: Tue, 3 Jun 2025 16:43:56 -0400 Subject: [PATCH 2/6] fix: improve formatting in LiquidHandler class for better readability with ruff --- pylabrobot/liquid_handling/liquid_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index 80fd9abb17..ababe48504 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -1837,7 +1837,7 @@ async def drop_resource( to_location = destination.get_absolute_location() + adjusted_plate_anchor elif isinstance(destination, (Plate, ResourceStack)) and isinstance(resource, Lid): lid = resource - if isinstance (destination, ResourceStack): + if isinstance(destination, ResourceStack): if destination.direction != "z": raise ValueError("Only ResourceStacks with direction 'z' are currently supported") top_item = destination.get_top_item() From 0a4eeef269a61e5631791666763a497e60bcba73 Mon Sep 17 00:00:00 2001 From: Eric <113262615+ericguan04@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:21:19 -0400 Subject: [PATCH 3/6] Fixed linting, formatting, and type inspection issues --- pylabrobot/liquid_handling/liquid_handler.py | 11 +++++++---- pylabrobot/resources/carrier.py | 9 ++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index ababe48504..a133cc6838 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -1840,11 +1840,14 @@ async def drop_resource( if isinstance(destination, ResourceStack): if destination.direction != "z": raise ValueError("Only ResourceStacks with direction 'z' are currently supported") - top_item = destination.get_top_item() - if isinstance(top_item, Plate): - destination = top_item - else: + if len(destination.children) == 0: to_location = destination.get_absolute_location(z="top") + else: + top_item = destination.get_top_item() + if isinstance(top_item, Plate): + destination = top_item + else: + to_location = destination.get_absolute_location(z="top") if isinstance(destination, Plate): plate_location = destination.get_absolute_location() child_wrt_parent = destination.get_lid_location( diff --git a/pylabrobot/resources/carrier.py b/pylabrobot/resources/carrier.py index edf7055100..122c5ed218 100644 --- a/pylabrobot/resources/carrier.py +++ b/pylabrobot/resources/carrier.py @@ -237,6 +237,8 @@ def _update_resource_stack_location(self, resource: Resource): if isinstance(resource, Lid): lid_parent = resource.parent + if lid_parent is None: + raise ValueError("Lid has no parent. ResourceStack not found for Lid") if isinstance(lid_parent, ResourceStack): resource_stack = lid_parent elif isinstance(lid_parent.parent, ResourceStack): @@ -244,8 +246,13 @@ def _update_resource_stack_location(self, resource: Resource): else: raise ValueError("ResourceStack not found for Lid") else: + if resource.parent is None: + raise ValueError("ResourceStack not found for resource") + if not isinstance(resource.parent, ResourceStack): + raise TypeError( + f"Resource {resource} is not a child of a ResourceStack, but of {type(resource.parent)}" + ) resource_stack = resource.parent - assert isinstance(resource_stack, ResourceStack) if resource_stack.children[0] == resource: resource_stack.location = self.get_default_child_location(resource) From 8bb93da3b98468f489708de3e426d50826cf774c Mon Sep 17 00:00:00 2001 From: Eric <113262615+ericguan04@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:31:26 -0400 Subject: [PATCH 4/6] Remove ROS line in .vscode/settings.json --- .vscode/settings.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index df11ff2a8d..714c508189 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,6 +45,5 @@ "editor.wordWrap": "bounded", "editor.wordWrapColumn": 100 }, - "mypy.runUsingActiveInterpreter": true, - "ros.distro": "humble" + "mypy.runUsingActiveInterpreter": true } From 3ead5b141ba6c4ace2372e24ccea6a4f2c7f63b6 Mon Sep 17 00:00:00 2001 From: Eric <113262615+ericguan04@users.noreply.github.com> Date: Sat, 7 Jun 2025 23:33:00 -0400 Subject: [PATCH 5/6] Add comment --- pylabrobot/liquid_handling/liquid_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index a133cc6838..29971517a2 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -1835,6 +1835,7 @@ async def drop_resource( resource.rotated(z=resource_rotation_wrt_destination_wrt_local) ).rotated(destination.get_absolute_rotation()) to_location = destination.get_absolute_location() + adjusted_plate_anchor + # Automatically assign lid to plate as child when dropping a lid to a plate (on or off resource stack) elif isinstance(destination, (Plate, ResourceStack)) and isinstance(resource, Lid): lid = resource if isinstance(destination, ResourceStack): From 665247b0fb67873f46d0a8666c164f0404ec2fcb Mon Sep 17 00:00:00 2001 From: Eric <113262615+ericguan04@users.noreply.github.com> Date: Wed, 18 Jun 2025 19:11:03 -0400 Subject: [PATCH 6/6] Add additional documentation for ResourceStack for this particular pull request --- .../resource-stack/resource-stack.ipynb | 287 +++++++++++++++++- 1 file changed, 273 insertions(+), 14 deletions(-) diff --git a/docs/resources/resource-stack/resource-stack.ipynb b/docs/resources/resource-stack/resource-stack.ipynb index 76f6faf9c0..024c779f3f 100644 --- a/docs/resources/resource-stack/resource-stack.ipynb +++ b/docs/resources/resource-stack/resource-stack.ipynb @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 29, "id": "2ebfc327", "metadata": {}, "outputs": [], @@ -42,7 +42,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 30, "id": "1737611f", "metadata": {}, "outputs": [ @@ -52,7 +52,7 @@ "([], [], [])" ] }, - "execution_count": 2, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -75,7 +75,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 31, "id": "255c656a", "metadata": {}, "outputs": [ @@ -85,7 +85,7 @@ "(['A', 'B'], 20)" ] }, - "execution_count": 3, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } @@ -112,7 +112,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 32, "id": "c6479d0b", "metadata": {}, "outputs": [ @@ -122,7 +122,7 @@ "20" ] }, - "execution_count": 4, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -152,7 +152,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 33, "id": "e35e3a75", "metadata": {}, "outputs": [ @@ -162,7 +162,7 @@ "'L2'" ] }, - "execution_count": 5, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } @@ -177,7 +177,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 34, "id": "7c7c11d5", "metadata": {}, "outputs": [ @@ -187,7 +187,7 @@ "'L1'" ] }, - "execution_count": 6, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } @@ -218,7 +218,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 35, "id": "7b92cac5", "metadata": {}, "outputs": [ @@ -228,7 +228,7 @@ "True" ] }, - "execution_count": 7, + "execution_count": 35, "metadata": {}, "output_type": "execute_result" } @@ -256,6 +256,265 @@ "\n", "This allows temporary storage of plates or lids during automated workflows." ] + }, + { + "cell_type": "markdown", + "id": "d3d4b23b", + "metadata": {}, + "source": [ + "## Moving plates and lids to the stacking area\n", + "The functions `move_lid()` and `move_plate()` can be used to move plates and lids to a ResourceStack during robot runtime.\n", + "\n", + "Below is an example on the STARBackend:" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "f421a8be", + "metadata": {}, + "outputs": [], + "source": [ + "from pylabrobot.liquid_handling import LiquidHandler\n", + "from pylabrobot.liquid_handling.backends import LiquidHandlerChatterboxBackend, STARBackend\n", + "from pylabrobot.resources.hamilton import STARLetDeck\n", + "\n", + "from pylabrobot.resources import (\n", + " TIP_CAR_480_A00, \n", + " STF,\n", + " PLT_CAR_L5AC_A00,\n", + " Cor_96_wellplate_360ul_Fb,\n", + " Cor_96_wellplate_2mL_Vb,\n", + " ResourceStack,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2c24b77d", + "metadata": {}, + "source": [ + "Setup liquid handler and deck" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "0dda3ff5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Setting up the liquid handler.\n", + "Resource deck was assigned to the liquid handler.\n", + "Resource trash was assigned to the liquid handler.\n", + "Resource trash_core96 was assigned to the liquid handler.\n", + "Resource waste_block was assigned to the liquid handler.\n" + ] + } + ], + "source": [ + "lh = LiquidHandler(backend=LiquidHandlerChatterboxBackend(), deck=STARLetDeck())\n", + "await lh.setup()" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "1ee5adf5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resource tip_carrier was assigned to the liquid handler.\n" + ] + } + ], + "source": [ + "tip_car = TIP_CAR_480_A00(name=\"tip_carrier\")\n", + "tip_rack = STF(name=\"tip_rack\")\n", + "tip_car[0] = tip_rack\n", + "lh.deck.assign_child_resource(tip_car, rails=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "7e139df0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resource plate_carrier was assigned to the liquid handler.\n" + ] + } + ], + "source": [ + "plate_stack = ResourceStack(\"plate_stack\", \"z\", [\n", + " Cor_96_wellplate_2mL_Vb(name='stack_plate_1')\n", + "])\n", + "\n", + "plt_car = PLT_CAR_L5AC_A00(name=\"plate_carrier\")\n", + "plt_car[0] = plate_stack\n", + "plt_car[1] = plate_1 = Cor_96_wellplate_360ul_Fb(name=\"plate_1\", with_lid=True)\n", + "plt_car[2] = plate_2 = Cor_96_wellplate_360ul_Fb(name=\"plate_2\", with_lid=True)\n", + "lh.deck.assign_child_resource(plt_car, rails=8)\n", + "\n", + "plate_1_lid = plate_1.lid\n", + "plate_2_lid = plate_2.lid" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "c0811184", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Rail Resource Type Coordinates (mm)\n", + "=================================================================================\n", + "(-6) ├── trash_core96 Trash (-58.200, 106.000, 229.000)\n", + " │\n", + "(1) ├── tip_carrier TipCarrier (100.000, 063.000, 100.000)\n", + " │ ├── tip_rack TipRack (106.200, 073.000, 214.950)\n", + " │ ├── \n", + " │ ├── \n", + " │ ├── \n", + " │ ├── \n", + " │\n", + "(8) ├── plate_carrier PlateCarrier (257.500, 063.000, 100.000)\n", + " │ ├── plate_stack ResourceStack (261.500, 071.500, 184.950)\n", + " │ │ ├── stack_plate_1 Plate (261.500, 071.500, 184.950)\n", + " │ ├── plate_1 Plate (261.500, 167.500, 183.120)\n", + " │ │ ├── plate_1_lid Lid (261.500, 167.500, 189.720)\n", + " │ ├── plate_2 Plate (261.500, 263.500, 183.120)\n", + " │ │ ├── plate_2_lid Lid (261.500, 263.500, 189.720)\n", + " │ ├── \n", + " │ ├── \n", + " │\n", + "(31) ├── waste_block Resource (775.000, 115.000, 100.000)\n", + " │ ├── teaching_tip_rack TipRack (780.900, 461.100, 100.000)\n", + " │\n", + "(32) ├── trash Trash (800.000, 190.600, 137.100)\n", + "\n" + ] + } + ], + "source": [ + "lh.summary()" + ] + }, + { + "cell_type": "markdown", + "id": "a34006a6", + "metadata": {}, + "source": [ + "If the top of the stack is a plate without a lid, a lid moved with `move_lid()` to the stack will automatically become a child of the top plate.\n", + "\n", + "Moving a plate with a lid with `move_plate()` will move both the plate and lid to the top of the stack." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5ac7d15", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Picking up resource: ResourcePickup(resource=Lid(name=plate_1_lid, location=Coordinate(000.000, 000.000, 006.600), size_x=127.76, size_y=85.48, size_z=8.9, category=lid), offset=Coordinate(x=0, y=0, z=0), pickup_distance_from_top=2.37, direction=)\n", + "Dropping resource: ResourceDrop(resource=Lid(name=plate_1_lid, location=Coordinate(000.000, 000.000, 006.600), size_x=127.76, size_y=85.48, size_z=8.9, category=lid), destination=Coordinate(x=261.5, y=71.5, z=220.85), destination_absolute_rotation=Rotation(x=0, y=0, z=0), offset=Coordinate(x=0, y=0, z=0), pickup_distance_from_top=2.37, pickup_direction=, drop_direction=, rotation=0)\n", + "Resource plate_1_lid was unassigned from the liquid handler.\n", + "Resource plate_1_lid was assigned to the liquid handler.\n", + "Picking up resource: ResourcePickup(resource=Plate(name=plate_2, size_x=127.76, size_y=85.48, size_z=14.2, location=Coordinate(000.000, 000.000, -03.030)), offset=Coordinate(x=0, y=0, z=0), pickup_distance_from_top=9.87, direction=)\n", + "Dropping resource: ResourceDrop(resource=Plate(name=plate_2, size_x=127.76, size_y=85.48, size_z=14.2, location=Coordinate(000.000, 000.000, -03.030)), destination=Coordinate(x=261.5, y=71.5, z=229.75), destination_absolute_rotation=Rotation(x=0, y=0, z=0), offset=Coordinate(x=0, y=0, z=0), pickup_distance_from_top=9.87, pickup_direction=, drop_direction=, rotation=0)\n", + "Resource plate_2 was unassigned from the liquid handler.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Resource 'plate_2_lid' is very high on the deck: 245.25 mm. Be careful when traversing the deck.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resource plate_2 was assigned to the liquid handler.\n" + ] + } + ], + "source": [ + "await lh.move_lid(plate_1_lid, plate_stack)\n", + "await lh.move_plate(plate_2, plate_stack)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "38f8e056", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Rail Resource Type Coordinates (mm)\n", + "==================================================================================\n", + "(-6) ├── trash_core96 Trash (-58.200, 106.000, 229.000)\n", + " │\n", + "(1) ├── tip_carrier TipCarrier (100.000, 063.000, 100.000)\n", + " │ ├── tip_rack TipRack (106.200, 073.000, 214.950)\n", + " │ ├── \n", + " │ ├── \n", + " │ ├── \n", + " │ ├── \n", + " │\n", + "(8) ├── plate_carrier PlateCarrier (257.500, 063.000, 100.000)\n", + " │ ├── plate_stack ResourceStack (261.500, 071.500, 184.950)\n", + " │ │ ├── stack_plate_1 Plate (261.500, 071.500, 184.950)\n", + " │ │ │ ├── plate_1_lid Lid (261.500, 071.500, 220.850)\n", + " │ │ ├── plate_2 Plate (261.500, 071.500, 229.750)\n", + " │ │ │ ├── plate_2_lid Lid (261.500, 071.500, 236.350)\n", + " │ ├── plate_1 Plate (261.500, 167.500, 183.120)\n", + " │ ├── \n", + " │ ├── \n", + " │ ├── \n", + " │\n", + "(31) ├── waste_block Resource (775.000, 115.000, 100.000)\n", + " │ ├── teaching_tip_rack TipRack (780.900, 461.100, 100.000)\n", + " │\n", + "(32) ├── trash Trash (800.000, 190.600, 137.100)\n", + "\n" + ] + } + ], + "source": [ + "lh.summary()" + ] + }, + { + "cell_type": "markdown", + "id": "7f41abdb", + "metadata": {}, + "source": [ + "**Warning:** Currently there are no checks in PyLabRobot for plate and lid compatibility when moving a lid to a ResourceStack. It is possible that the lid from one plate could be added to a plate of a different type on the ResourceStack with `move_lid()`. Users are responsible for making sure the lids and plates are compatible and will stack correctly.\n", + "\n", + "TODO: Create a more permanent and robust fix: https://github.com/PyLabRobot/pylabrobot/pull/546#issuecomment-2945105532" + ] } ], "metadata": { @@ -279,7 +538,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.15" + "version": "3.10.12" } }, "nbformat": 4,