diff --git a/docs/cookbook/assets/star_semiautomated_plate_definition/biorad_96_wellplate_200uL_Vb.png b/docs/cookbook/assets/star_semiautomated_plate_definition/biorad_96_wellplate_200uL_Vb.png new file mode 100644 index 00000000000..4de1fb7f449 Binary files /dev/null and b/docs/cookbook/assets/star_semiautomated_plate_definition/biorad_96_wellplate_200uL_Vb.png differ diff --git a/docs/cookbook/star_semiautomated_plate_definition.ipynb b/docs/cookbook/star_semiautomated_plate_definition.ipynb new file mode 100644 index 00000000000..62c0fc2debe --- /dev/null +++ b/docs/cookbook/star_semiautomated_plate_definition.ipynb @@ -0,0 +1,1989 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Semi-Automated Plate Definition Generation\n", + "\n", + "- tags: #platedefinition #resourcemovement #plateadapter #hamiltonstar\n", + "- Last updated: 2026-01-29" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "- Machines used:\n", + " - Hamilton STAR\n", + "- Non-PLR dependencies: None \n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Preview of Machine Behvaiour" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Master Information\n", + "\n", + "i.e. declare parameters that might change from run to run" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "protocol_mode = \"execution\" # \"execution\" or \"simulation\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Choose how many channels you want to be involed in probing actions (1-6 is acceptable)\n", + "n_channels = 3" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Import Statements" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Non-PLR Dependencies\n", + "\n", + "None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Machine & Visualizer" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + " \n", + "import random \n", + "import time\n", + "import asyncio\n", + " \n", + "from pylabrobot.liquid_handling import LiquidHandler\n", + "from pylabrobot.resources.hamilton import STARLetDeck, STARDeck\n", + "from pylabrobot.visualizer.visualizer import Visualizer\n", + "from pylabrobot.utils import chunk_list\n", + "\n", + "if protocol_mode == \"execution\":\n", + "\n", + " from pylabrobot.liquid_handling.backends import STARBackend\n", + "\n", + " star = STARBackend()\n", + "\n", + "elif protocol_mode == \"simulation\":\n", + "\n", + " from pylabrobot.liquid_handling.backends.hamilton.STAR_chatterbox import STARChatterboxBackend\n", + " \n", + " star = STARChatterboxBackend()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Required Resources" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from pylabrobot.resources import (\n", + " hamilton_mfx_carrier_L5_base,\n", + " hamilton_mfx_plateholder_DWP_metal_tapped,\n", + " hamilton_mfx_plateholder_DWP_flat,\n", + " Coordinate\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Create Initial Plate Definition\n", + "\n", + "Ideally, the manufactuer publishes technical drawings which act as a starting material that requires verification in physical reality + measurements of missing parameters:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![sd](./assets/star_semiautomated_plate_definition/biorad_96_wellplate_200uL_Vb.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{note}\n", + "PyLabRobot always uses \"front-left-bottom\" references!\n", + "This means that every technical drawing measurement has to be converted accordingly:\n", + "e.g. `dx = 14.38 (left to center_x) - 5.46 / 2 (radius of well)`.\n", + "\n", + "Furthermore, this is a special plate: its well spacing is symmetric in both the x and y dimensions!\n", + "This is not common, and you should always perform a multiple sanity checks to ensure you are not taking measurements from the incorrect direction: e.g. `127.76 - (14.38)*2 = 99.0` mm -> 1st column center_x and 12th column center_x are exactly 99.0 mm apart as expected based on the ANSI/SLAS 4-2004 standard and an 11 column distance -> the plate is symmetric across its welll spacing in the `x` dimension\n", + "```\n", + "\n", + "```{warning}\n", + "It is now clear whether `16.06-14.81 = 1.25` refers to the `dz` with or without the `material_z_thickness`.\n", + "This means without measurement we risk crashing pipette heads into the well cavity bottom - if a plate holder with a pedestal (i.e. central elevation) is used.\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from pylabrobot.resources.plate import Plate\n", + "from pylabrobot.resources.utils import create_ordered_items_2d\n", + "from pylabrobot.resources.well import (\n", + " CrossSectionType,\n", + " Well,\n", + " WellBottomType,\n", + ")\n", + "\n", + "def biorad_96_wellplate_200uL_Vb(name: str, with_lid: bool = False) -> Plate:\n", + " \"\"\"Bio-rad cat. no.: HSP9601 (50/package), HSP9601B (8*50/package).\n", + "\n", + " Bio-rad plate with 96-wells (V-bottoms).\n", + " \"The patented rigid 2-component design is specifically engineered to\n", + " withstand the stresses of thermal cycling.\" -> excellent for automation!\n", + "\n", + " - Colour: white shell/clear well\n", + " - alternative cat. no.:\n", + " - red shell (HSP-9611)\n", + " - yellow shell (HSP-9621)\n", + " - blue shell (HSP-9631)\n", + " - green shell (HSP-9641)\n", + " - black shell (HSP-9661)\n", + " - Material: Polypropylene\n", + " - Cleanliness: \"Certified to be free of DNase, RNase, and human genomic DNA\"\n", + " - Total volume/well = 200 uL\n", + " - URL: https://www.bio-rad.com/en-uk/sku/HSP9601-hard-shell-96-well-pcr-plates-\n", + " low-profile-thin-wall-skirted-white-clear?ID=HSP9601\n", + " - technical drawing: ./assets/star_semiautomated_plate_definition/biorad_96_wellplate_200uL_Vb.png\n", + " \"\"\"\n", + " well_diameter = 5.46 # mm\n", + " return Plate(\n", + " name=name,\n", + " size_x=127.76,\n", + " size_y=85.48,\n", + " size_z=16.06,\n", + " lid=None,\n", + " model=biorad_96_wellplate_200uL_Vb.__name__,\n", + " ordered_items=create_ordered_items_2d(\n", + " Well,\n", + " num_items_x=12,\n", + " num_items_y=8,\n", + " dx=round(14.38-well_diameter/2, 2), # symmetric well spacing across X centerline\n", + " dy=round(11.24-well_diameter/2, 2), # symmetric well spacing across Y centerline\n", + " dz=1.1, # initial guess\n", + " item_dx=9.0,\n", + " item_dy=9.0,\n", + " size_x=well_diameter,\n", + " size_y=well_diameter,\n", + " size_z=14.4,\n", + " bottom_type=WellBottomType.V,\n", + " material_z_thickness=0.65, # initial guess\n", + " cross_section_type=CrossSectionType.CIRCLE,\n", + " compute_volume_from_height=None,\n", + " compute_height_from_volume=None,\n", + " ),\n", + " )\n", + "\n", + "# Modify the above if you want to change/create your own plate definition\n", + "# then simply change this variable which is used for the rest of this recipe\n", + "plate_definition_function = biorad_96_wellplate_200uL_Vb" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "11.65" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "14.38-5.46/2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Utility Functions\n", + "\n", + "Inevitably we have to create specialised solutions for specialised problems.\n", + "Python makes this easy by quickly generating utility functions.\n", + "These can be stored with you 'automated Protocol' script or, if used repeadetly be added to your internal repository, or even added to the `pylabrobot.utils` package." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def generate_channel_wells(plate: Plate, channels_used: int):\n", + " \"\"\"\n", + " Generate a list of wells for multi-channel pipetting with evenly spaced rows.\n", + " \n", + " Returns:\n", + " List of well names:\n", + " e.g., generate_channel_wells(test_plate_0, 4) -> [\"A1\", \"C1\", \"E1\", \"H1\", \"A2\", \"C2\", ...])\n", + " \"\"\"\n", + " num_rows, num_cols = plate.num_items_y, plate.num_items_x\n", + " \n", + " # Generate evenly spaced row indices\n", + " row_indices = [round(i * (num_rows - 1) / (channels_used - 1)) for i in range(channels_used)]\n", + " \n", + " # Generate well names: all columns, with selected rows per column\n", + " return [f\"{chr(ord('A') + row)}{col}\" \n", + " for col in range(1, num_cols + 1) \n", + " for row in row_indices]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Instantiate Frontend & Connect to Machine" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-01-28 18:23:49,585 - pylabrobot.io.usb - INFO - Finding USB device...\n", + "2026-01-28 18:23:49,618 - pylabrobot.io.usb - INFO - Found USB device.\n", + "2026-01-28 18:23:49,621 - pylabrobot.io.usb - INFO - Found endpoints. \n", + "Write:\n", + " ENDPOINT 0x2: Bulk OUT ===============================\n", + " bLength : 0x7 (7 bytes)\n", + " bDescriptorType : 0x5 Endpoint\n", + " bEndpointAddress : 0x2 OUT\n", + " bmAttributes : 0x2 Bulk\n", + " wMaxPacketSize : 0x40 (64 bytes)\n", + " bInterval : 0x0 \n", + "Read:\n", + " ENDPOINT 0x81: Bulk IN ===============================\n", + " bLength : 0x7 (7 bytes)\n", + " bDescriptorType : 0x5 Endpoint\n", + " bEndpointAddress : 0x81 IN\n", + " bmAttributes : 0x2 Bulk\n", + " wMaxPacketSize : 0x40 (64 bytes)\n", + " bInterval : 0x0\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Websocket server started at http://127.0.0.1:2121\n", + "File server started at http://127.0.0.1:1337 . Open this URL in your browser.\n" + ] + }, + { + "data": { + "text/plain": [ + "'C0CDid0016er00/00'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "deck = STARDeck()\n", + "lh = LiquidHandler(backend=star, deck=deck)\n", + "\n", + "await lh.setup()\n", + "\n", + "vis = Visualizer(resource=lh)\n", + "await vis.setup()\n", + "\n", + "await star.disable_cover_control() # 😈\n", + "shutdown_executed = False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configure Deck Layout" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# Setup MFX Carrier with DWP PlateHolder that has a pedestal\n", + "\n", + "plateholder_pedestal_0 = hamilton_mfx_plateholder_DWP_metal_tapped(\n", + " name=f\"plateholder_pedestal_0\"\n", + ")\n", + "\n", + "mfx_carrier_0 = hamilton_mfx_carrier_L5_base(\n", + " name=\"mfx_carrier_0\",\n", + " modules={0: plateholder_pedestal_0}\n", + ")\n", + "\n", + "deck.assign_child_resource(mfx_carrier_0, rails=10)\n", + "\n", + "# Setup MFX Carrier with DWP PlateHolder that has a flat bottom\n", + "\n", + "plateholder_flat_0 = hamilton_mfx_plateholder_DWP_flat(name=f\"plateholder_flat_0\")\n", + "\n", + "mfx_carrier_1 = hamilton_mfx_carrier_L5_base(\n", + " name=\"mfx_carrier_1\",\n", + " modules={0: plateholder_flat_0}\n", + ")\n", + "\n", + "deck.assign_child_resource(mfx_carrier_1, rails=17)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Opening in existing browser session.\n" + ] + } + ], + "source": [ + "# Add plate onto PlateHolder with the pedestal\n", + "\n", + "test_plate_0 = plate_definition_function(name=\"test_plate_0\")\n", + "\n", + "mfx_carrier_0[0] = test_plate_0 " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step Calculations" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['A1', 'E1', 'H1', 'A2', 'E2', 'H2', 'A3', 'E3', 'H3', 'A4']\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[289359:289359:0100/000000.565807:ERROR:content/zygote/zygote_linux.cc:673] write: Broken pipe (32)\n" + ] + } + ], + "source": [ + "wells_to_probe = generate_channel_wells(test_plate_0, channels_used=n_channels)\n", + "\n", + "if len(wells_to_probe) > 10: # You might want to use this recipe to create a 6-wellplate definition\n", + " print(wells_to_probe[:10])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Execution" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "recipe_execution_start_time = time.time()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Pickup Teaching Needles & CORE Grippers" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'C0ZTid0019er00/00sx000 000 000 000 000 000 000 000sg000 000 000 000 000 000 486 509'" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "teaching_tip_rack = lh.deck.get_resource(\"teaching_tip_rack\")\n", + "await lh.pick_up_tips(teaching_tip_rack[\"A1:C1\"], use_channels=[0,1,2,3,4,5][:n_channels])\n", + "\n", + "await star.pick_up_core_gripper_tools(front_channel=7)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1) Focus: Flat PlateHolder -> `dz` + `material_z_thickness`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 1.a.) Probe True PlateHolder Origin" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "plate_holder_pedestal_child_coord = mfx_carrier_0[0].get_absolute_location()+mfx_carrier_0[0].child_location\n", + "\n", + "plate_holder_flat_child_coord = mfx_carrier_1[0].get_absolute_location()+mfx_carrier_1[0].child_location\n", + "\n", + "test_location_w_offset = plate_holder_flat_child_coord + Coordinate(x=3, y=3, z=0)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "from tqdm.auto import tqdm" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2a5e625b11ed471399ef73d2d6aedecb", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/3 [00:00 `material_z_thickness`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 2.a) Store Coordinates to Probe on Pedestal" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [], + "source": [ + "positions_to_probe_pedestal = [\n", + " well.get_absolute_location(\"c\", \"c\", \"top\") + Coordinate(x_offset, y_offset, 0)\n", + " for well in test_plate_0.children\n", + " if well.get_identifier() in wells_to_probe\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 2.b) Z Probing of Wells (on Pedestal)" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d33a4d7487194f06a258e474b5f9b894", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/12 [00:00