{ "cells": [ { "cell_type": "markdown", "id": "c3c71913", "metadata": {}, "source": [ "# Using the Visualizer\n", "\n", "The Visualizer is a tool that allows you to visualize the a Resource (like LiquidHandler) including its state to easily see what is going on, for example when executing a protocol on a robot or when developing a new protocol.\n", "\n", "When using a backend that does not require access to a physical robot, such as the {class}`~pylabrobot.liquid_handling.backends.chatterbox.ChatterboxBackend`, the Visualizer can be used to simulate a robot's behavior. Of course, you may also use the Visualizer when working with a real robot to see what is happening in the PLR resource and state trackers." ] }, { "attachments": {}, "cell_type": "markdown", "id": "d6cdca52", "metadata": {}, "source": [ "## Setting up a connection with the robot\n", "\n", "As described in the [basic liquid handling tutorial](basic), we will use the {class}`~pylabrobot.liquid_handling.liquid_handler.LiquidHandler` class to control the robot. This time, however, instead of using the Hamilton {class}`~pylabrobot.liquid_handling.backends.hamilton.STAR.STAR` backend, we are using the software-only {class}`~pylabrobot.liquid_handling.backends.chatterbox.ChatterboxBackend` backend. This means that liquid handling will work exactly the same, but commands are simply printed out to the console instead of being sent to a physical robot. We are still using the same deck." ] }, { "cell_type": "code", "execution_count": 1, "id": "7c5a2629", "metadata": {}, "outputs": [], "source": [ "from pylabrobot.liquid_handling import LiquidHandler\n", "from pylabrobot.liquid_handling.backends import ChatterboxBackend\n", "from pylabrobot.visualizer.visualizer import Visualizer" ] }, { "cell_type": "code", "execution_count": 2, "id": "272520d3", "metadata": {}, "outputs": [], "source": [ "from pylabrobot.resources.hamilton import STARLetDeck" ] }, { "cell_type": "code", "execution_count": 3, "id": "2e280caa", "metadata": {}, "outputs": [], "source": [ "lh = LiquidHandler(backend=ChatterboxBackend(), deck=STARLetDeck())" ] }, { "attachments": {}, "cell_type": "markdown", "id": "30dcf4a1", "metadata": {}, "source": [ "Calling {func}`~pylabrobot.liquid_handling.liquid_handler.LiquidHandler.setup` will print out \"Setting up the robot\" and also that two resources were assigned: the deck and the trash. Other than that, the chatter box backend has no setup to do." ] }, { "cell_type": "code", "execution_count": 4, "id": "1419f2b3", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Setting up the robot.\n", "Resource deck was assigned to the robot.\n", "Resource trash was assigned to the robot.\n", "Resource trash_core96 was assigned to the robot.\n" ] } ], "source": [ "await lh.setup()" ] }, { "cell_type": "markdown", "id": "871184ce", "metadata": {}, "source": [ "Next, we will create a {class}`~pylabrobot.visualizer.visualizer.Visualizer` object. The Visualizer expects a Resource, and we will pass the {class}`~pylabrobot.liquid_handling.liquid_handler.LiquidHandler` object to it. This will allow us to visualize the robot's state and actions." ] }, { "cell_type": "code", "execution_count": 5, "id": "165c9de4", "metadata": {}, "outputs": [ { "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" ] } ], "source": [ "vis = Visualizer(resource=lh)\n", "await vis.setup()" ] }, { "cell_type": "markdown", "id": "3002429b", "metadata": {}, "source": [ "![The empty simulator](./img/visualizer/empty.png)" ] }, { "attachments": {}, "cell_type": "markdown", "id": "050ccefd", "metadata": {}, "source": [ "## Build the deck layout: Assigning plates and tips\n", "\n", "When resources are assigned to the root resource of the Visualizer, in this case `lh`, they will automatically appear in the visualization." ] }, { "cell_type": "code", "execution_count": 6, "id": "c8c464c8", "metadata": {}, "outputs": [], "source": [ "from pylabrobot.resources import (\n", " TIP_CAR_480_A00,\n", " PLT_CAR_L5AC_A00,\n", " Cor_96_wellplate_360ul_Fb,\n", " HTF_L\n", ")" ] }, { "cell_type": "code", "execution_count": 7, "id": "b14e3628", "metadata": {}, "outputs": [], "source": [ "tip_car = TIP_CAR_480_A00(name='tip carrier')\n", "tip_car[0] = tip_rack1 = HTF_L(name='tips_01', with_tips=False)\n", "tip_car[1] = tip_rack2 = HTF_L(name='tips_02', with_tips=False)\n", "tip_car[2] = tip_rack3 = HTF_L(name='tips_03', with_tips=False)\n", "tip_car[3] = tip_rack4 = HTF_L(name='tips_04', with_tips=False)\n", "tip_car[4] = tip_rack5 = HTF_L(name='tips_05', with_tips=False)" ] }, { "cell_type": "code", "execution_count": 8, "id": "140872be", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Resource tip carrier was assigned to the robot.\n" ] } ], "source": [ "lh.deck.assign_child_resource(tip_car, rails=15)" ] }, { "cell_type": "code", "execution_count": 9, "id": "13cbc612", "metadata": {}, "outputs": [], "source": [ "plt_car = PLT_CAR_L5AC_A00(name='plate carrier')\n", "plt_car[0] = plate_1 = Cor_96_wellplate_360ul_Fb(name='plate_01')\n", "plt_car[1] = plate_2 = Cor_96_wellplate_360ul_Fb(name='plate_02')\n", "plt_car[2] = plate_3 = Cor_96_wellplate_360ul_Fb(name='plate_03')" ] }, { "cell_type": "code", "execution_count": 10, "id": "d618ec6a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Resource plate carrier was assigned to the robot.\n" ] } ], "source": [ "lh.deck.assign_child_resource(plt_car, rails=8)" ] }, { "cell_type": "markdown", "id": "21835f31", "metadata": {}, "source": [ "![The simulator after the resources have been assigned](./img/visualizer/assignment.png)" ] }, { "cell_type": "markdown", "id": "68a9721b", "metadata": {}, "source": [ "### Configuring the state of the deck\n", "\n", "As with every PyLabRobot script, you have the option of updating the state of the deck before you actually start your method. This will allow PyLabRobot to keep track of what is going on, enabling features like {func}`~pylabrobot.liquid_handling.liquid_handler.LiquidHandler.return_tips` and catching errors (like missed tips) before a command would be executed on the robot. With the visualizer, this state has the additional effect of updating the visualization." ] }, { "attachments": {}, "cell_type": "markdown", "id": "daf99520", "metadata": {}, "source": [ "### Tips\n", "\n", "Let's use {func}`~pylabrobot.resources.tip_rack.fill` to place tips at all spots in the tip rack in location `0`." ] }, { "cell_type": "code", "execution_count": 11, "id": "ca3152d0", "metadata": { "scrolled": true }, "outputs": [], "source": [ "tip_rack1.fill()" ] }, { "attachments": {}, "cell_type": "markdown", "id": "26f036c6", "metadata": {}, "source": [ "\n", "You can precisely control the presence of tips using {func}`~pylabrobot.resources.tip_rack.set_tip_state`. This function allows you to set whether there is a tip in each {class}`~pylabrobot.resources.tip_rack.TipSpot`." ] }, { "cell_type": "code", "execution_count": 12, "id": "8f574b8d", "metadata": {}, "outputs": [], "source": [ "tip_rack4 = lh.deck.get_resource(\"tips_04\")\n", "tip_rack4.set_tip_state([[True]*6 + [False]*6]*8)" ] }, { "cell_type": "code", "execution_count": 13, "id": "8e1ed1e6", "metadata": {}, "outputs": [], "source": [ "tip_rack3.set_tip_state([[True, False]*6]*8)" ] }, { "cell_type": "code", "execution_count": 14, "id": "890272f1", "metadata": {}, "outputs": [], "source": [ "tip_rack2.set_tip_state([[True, True, False, False]*3]*8)" ] }, { "attachments": {}, "cell_type": "markdown", "id": "594d8016", "metadata": {}, "source": [ "### Liquids\n", "\n", "Adding liquid to wells works similarly. You can use {func}`~pylabrobot.resources.plate.set_well_liquids` to set the liquid in each well of a plate. Each liquid is represented by a tuple where the first element corresponds to the type of liquid and the second to the volume in uL. Here, `None` is used to designate an unknown liquid." ] }, { "cell_type": "code", "execution_count": 15, "id": "5b76aac2", "metadata": {}, "outputs": [], "source": [ "plate_1_liquids = [[(None, 500)]]*96\n", "plate_1.set_well_liquids(plate_1_liquids)" ] }, { "cell_type": "code", "execution_count": 16, "id": "400208c7", "metadata": {}, "outputs": [], "source": [ "plate_2_liquids = [[(None, 100)], [(None, 500)]]*(96//2)\n", "plate_2.set_well_liquids(plate_2_liquids)" ] }, { "attachments": {}, "cell_type": "markdown", "id": "66281459", "metadata": {}, "source": [ "In the visualizer, you can see that the opacity of the well is proportional to how full the well is relative to its maximum volume." ] }, { "cell_type": "markdown", "id": "66a27cfc", "metadata": {}, "source": [ "![Simulator after the tips have been placed and the volumes adjusted](./img/visualizer/resources.png)" ] }, { "cell_type": "markdown", "id": "27af93d8", "metadata": {}, "source": [ "## Liquid handling\n", "\n", "Once the layout is complete, you can run the same commands as described in the [basic liquid handling tutorial](basic).\n", "\n", "It is important that both tip tracking and volume tracking are enabled globally, so that the visualizer can keep track of the state of the tips and the volumes of the liquids." ] }, { "cell_type": "code", "execution_count": 17, "id": "ce58298d", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(None, None)" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from pylabrobot.resources import set_tip_tracking, set_volume_tracking\n", "set_tip_tracking(True), set_volume_tracking(True)" ] }, { "cell_type": "markdown", "id": "21c3f2cd", "metadata": {}, "source": [ "### Picking up tips\n", "\n", "Note that since we are using the {class}`~pylabrobot.liquid_handling.backends.chatterbox.ChatterboxBackend`, we just print out \"Picking up tips\" instead of actually performing an operation. The visualizer will show the tips being picked up." ] }, { "cell_type": "code", "execution_count": 18, "id": "f97eadd4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Picking up tips [Pickup(resource=TipSpot(name=tips_01_tipspot_0_0, location=(007.200, 068.300, -83.500), size_x=9.0, size_y=9.0, size_z=0, category=tip_spot), offset=Coordinate(x=0, y=0, z=0), tip=HamiltonTip(HIGH_VOLUME, has_filter=True, maximal_volume=1065, fitting_depth=8, total_tip_length=95.1, pickup_method=OUT_OF_RACK)), Pickup(resource=TipSpot(name=tips_01_tipspot_1_1, location=(016.200, 059.300, -83.500), size_x=9.0, size_y=9.0, size_z=0, category=tip_spot), offset=Coordinate(x=0, y=0, z=0), tip=HamiltonTip(HIGH_VOLUME, has_filter=True, maximal_volume=1065, fitting_depth=8, total_tip_length=95.1, pickup_method=OUT_OF_RACK)), Pickup(resource=TipSpot(name=tips_01_tipspot_2_2, location=(025.200, 050.300, -83.500), size_x=9.0, size_y=9.0, size_z=0, category=tip_spot), offset=Coordinate(x=0, y=0, z=0), tip=HamiltonTip(HIGH_VOLUME, has_filter=True, maximal_volume=1065, fitting_depth=8, total_tip_length=95.1, pickup_method=OUT_OF_RACK)), Pickup(resource=TipSpot(name=tips_01_tipspot_3_3, location=(034.200, 041.300, -83.500), size_x=9.0, size_y=9.0, size_z=0, category=tip_spot), offset=Coordinate(x=0, y=0, z=0), tip=HamiltonTip(HIGH_VOLUME, has_filter=True, maximal_volume=1065, fitting_depth=8, total_tip_length=95.1, pickup_method=OUT_OF_RACK))].\n" ] } ], "source": [ "await lh.pick_up_tips(tip_rack1[\"A1\", \"B2\", \"C3\", \"D4\"])" ] }, { "cell_type": "code", "execution_count": 19, "id": "bf46e476", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dropping tips [Drop(resource=TipSpot(name=tips_01_tipspot_0_0, location=(007.200, 068.300, -83.500), size_x=9.0, size_y=9.0, size_z=0, category=tip_spot), offset=Coordinate(x=0, y=0, z=0), tip=HamiltonTip(HIGH_VOLUME, has_filter=True, maximal_volume=1065, fitting_depth=8, total_tip_length=95.1, pickup_method=OUT_OF_RACK)), Drop(resource=TipSpot(name=tips_01_tipspot_1_1, location=(016.200, 059.300, -83.500), size_x=9.0, size_y=9.0, size_z=0, category=tip_spot), offset=Coordinate(x=0, y=0, z=0), tip=HamiltonTip(HIGH_VOLUME, has_filter=True, maximal_volume=1065, fitting_depth=8, total_tip_length=95.1, pickup_method=OUT_OF_RACK)), Drop(resource=TipSpot(name=tips_01_tipspot_2_2, location=(025.200, 050.300, -83.500), size_x=9.0, size_y=9.0, size_z=0, category=tip_spot), offset=Coordinate(x=0, y=0, z=0), tip=HamiltonTip(HIGH_VOLUME, has_filter=True, maximal_volume=1065, fitting_depth=8, total_tip_length=95.1, pickup_method=OUT_OF_RACK)), Drop(resource=TipSpot(name=tips_01_tipspot_3_3, location=(034.200, 041.300, -83.500), size_x=9.0, size_y=9.0, size_z=0, category=tip_spot), offset=Coordinate(x=0, y=0, z=0), tip=HamiltonTip(HIGH_VOLUME, has_filter=True, maximal_volume=1065, fitting_depth=8, total_tip_length=95.1, pickup_method=OUT_OF_RACK))].\n" ] } ], "source": [ "await lh.drop_tips(tip_rack1[\"A1\", \"B2\", \"C3\", \"D4\"])" ] }, { "cell_type": "markdown", "id": "5c6948b2", "metadata": {}, "source": [ "### Aspirating and dispensing" ] }, { "cell_type": "code", "execution_count": 20, "id": "947977c7", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Picking up tips [Pickup(resource=TipSpot(name=tips_01_tipspot_0_0, location=(007.200, 068.300, -83.500), size_x=9.0, size_y=9.0, size_z=0, category=tip_spot), offset=Coordinate(x=0, y=0, z=0), tip=HamiltonTip(HIGH_VOLUME, has_filter=True, maximal_volume=1065, fitting_depth=8, total_tip_length=95.1, pickup_method=OUT_OF_RACK))].\n" ] } ], "source": [ "await lh.pick_up_tips(tip_rack1[\"A1\"])" ] }, { "cell_type": "code", "execution_count": 21, "id": "c25a147f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Aspirating [Aspiration(resource=Well(name=plate_01_well_1_0, location=(019.870, 070.770, 003.030), size_x=6.86, size_y=6.86, size_z=10.67, category=well), offset=Coordinate(x=0, y=0, z=0), tip=HamiltonTip(HIGH_VOLUME, has_filter=True, maximal_volume=1065, fitting_depth=8, total_tip_length=95.1, pickup_method=OUT_OF_RACK), volume=200.0, flow_rate=None, liquid_height=None, blow_out_air_volume=None, liquids=[(None, 200.0)])].\n" ] } ], "source": [ "await lh.aspirate(plate_1[\"A2\"], vols=[200])" ] }, { "cell_type": "code", "execution_count": 22, "id": "e86428ea", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dispensing [Dispense(resource=Well(name=plate_02_well_0_0, location=(010.870, 070.770, 003.030), size_x=6.86, size_y=6.86, size_z=10.67, category=well), offset=Coordinate(x=0, y=0, z=0), tip=HamiltonTip(HIGH_VOLUME, has_filter=True, maximal_volume=1065, fitting_depth=8, total_tip_length=95.1, pickup_method=OUT_OF_RACK), volume=200.0, flow_rate=None, liquid_height=None, blow_out_air_volume=None, liquids=[(None, 200.0)])].\n" ] } ], "source": [ "await lh.dispense(plate_2[\"A1\"], vols=[200])" ] }, { "cell_type": "code", "execution_count": 23, "id": "70117e04", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dropping tips [Drop(resource=TipSpot(name=tips_01_tipspot_0_0, location=(007.200, 068.300, -83.500), size_x=9.0, size_y=9.0, size_z=0, category=tip_spot), offset=Coordinate(x=0, y=0, z=0), tip=HamiltonTip(HIGH_VOLUME, has_filter=True, maximal_volume=1065, fitting_depth=8, total_tip_length=95.1, pickup_method=OUT_OF_RACK))].\n" ] } ], "source": [ "await lh.return_tips()" ] }, { "attachments": {}, "cell_type": "markdown", "id": "8a8b3d59", "metadata": {}, "source": [ "### Aspirating using CoRe 96\n", "\n", "The CoRe 96 head supports liquid handling operations for 96 channels at once. Here's how to use:\n", "\n", "- {func}`~pylabrobot.liquid_handling.liquid_handler.LiquidHandler.pick_up_tips96` for picking up 96 tips;\n", "- {func}`~pylabrobot.liquid_handling.liquid_handler.LiquidHandler.aspirate96` for aspirating liquid from an entire plate at once;\n", "- {func}`~pylabrobot.liquid_handling.liquid_handler.LiquidHandler.dispense96` for dispensing liquid to an entire plate at once;\n", "- {func}`~pylabrobot.liquid_handling.liquid_handler.LiquidHandler.drop_tips96` for dropping tips to the tip rack.\n" ] }, { "cell_type": "code", "execution_count": 24, "id": "b8c5706d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Picking up tips from tips_01.\n" ] } ], "source": [ "await lh.pick_up_tips96(tip_rack1)" ] }, { "cell_type": "code", "execution_count": 25, "id": "c09144c9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Aspirating 100 from Plate(name=plate_01, size_x=127.0, size_y=86.0, size_z=42.0, location=(000.000, 000.000, 000.000)).\n" ] } ], "source": [ "await lh.aspirate96(plate_1, volume=100)" ] }, { "cell_type": "code", "execution_count": 26, "id": "2ba711bb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dispensing 100 to Plate(name=plate_03, size_x=127.0, size_y=86.0, size_z=42.0, location=(000.000, 000.000, 000.000)).\n" ] } ], "source": [ "await lh.dispense96(plate_3, volume=100)" ] }, { "cell_type": "code", "execution_count": 27, "id": "6d205ea7", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dropping tips to tips_01.\n" ] } ], "source": [ "await lh.drop_tips96(tip_rack1)" ] }, { "cell_type": "markdown", "id": "9cc77505", "metadata": {}, "source": [ "![The simulator after the liquid handling operations completed](./img/visualizer/after_lh.png)" ] }, { "attachments": {}, "cell_type": "markdown", "id": "e49020a5", "metadata": {}, "source": [ "## Shutting down\n", "\n", "When you're done, you can stop the visualizer by calling {func}`~pylabrobot.visualizer.visualizer.Visualizer.stop`. This will stop the visualization." ] }, { "cell_type": "code", "execution_count": 28, "id": "44a61431", "metadata": {}, "outputs": [], "source": [ "await vis.stop()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.3" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false }, "varInspector": { "cols": { "lenName": 16, "lenType": 16, "lenVar": 40 }, "kernels_config": { "python": { "delete_cmd_postfix": "", "delete_cmd_prefix": "del ", "library": "var_list.py", "varRefreshCmd": "print(var_dic_list())" }, "r": { "delete_cmd_postfix": ") ", "delete_cmd_prefix": "rm(", "library": "var_list.r", "varRefreshCmd": "cat(var_dic_list()) " } }, "types_to_exclude": [ "module", "function", "builtin_function_or_method", "instance", "_Feature" ], "window_display": false }, "vscode": { "interpreter": { "hash": "bf274dfc1b974177267b6b8fba8543eeb0bb4c5d64c637dde420829b05625268" } } }, "nbformat": 4, "nbformat_minor": 5 }