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, diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index 926ac89370..561184422f 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -1976,7 +1976,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" @@ -2009,13 +2009,26 @@ 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): + # 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 - 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") + 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( + 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..122c5ed218 100644 --- a/pylabrobot/resources/carrier.py +++ b/pylabrobot/resources/carrier.py @@ -234,8 +234,25 @@ def _update_resource_stack_location(self, resource: Resource): Args: resource: The Resource on the ResourceStack tht was assigned. """ - resource_stack = resource.parent - assert isinstance(resource_stack, ResourceStack) + + 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): + resource_stack = lid_parent.parent + 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 if resource_stack.children[0] == resource: resource_stack.location = self.get_default_child_location(resource)