From 719fb7d8fd23f04463299360e974abf710d4bd32 Mon Sep 17 00:00:00 2001 From: bsubbaraman Date: Wed, 29 Nov 2023 16:07:46 -0800 Subject: [PATCH] update syringe, camera, and loop tools with functions relevant for duckweed growth assays --- .../calibration/IntegrationTests.ipynb | 198 +++++++++ .../LabAutomationDeckCalibration.ipynb | 391 ++++++++++++++++++ .../lab_automation_deck_template.json | 22 + .../lab_automation_deck_MA.json | 52 +++ .../lab_automation_deck_blair.json | 2 +- .../generic_petri_dish_100ml.json | 61 +++ science_jubilee/tools/Camera.py | 50 ++- science_jubilee/tools/Loop.py | 96 ++++- science_jubilee/tools/Syringe.py | 2 +- science_jubilee/tools/Tool.py | 5 +- .../configs/calibration_checkerboard.yml | 15 + 11 files changed, 884 insertions(+), 10 deletions(-) create mode 100644 science_jubilee/calibration/IntegrationTests.ipynb create mode 100644 science_jubilee/calibration/LabAutomationDeckCalibration.ipynb create mode 100644 science_jubilee/calibration/templates/lab_automation_deck_template.json create mode 100644 science_jubilee/decks/deck_definition/lab_automation_deck_MA.json create mode 100644 science_jubilee/labware/labware_definition/generic_petri_dish_100ml.json create mode 100644 science_jubilee/tools/configs/calibration_checkerboard.yml diff --git a/science_jubilee/calibration/IntegrationTests.ipynb b/science_jubilee/calibration/IntegrationTests.ipynb new file mode 100644 index 0000000..029d271 --- /dev/null +++ b/science_jubilee/calibration/IntegrationTests.ipynb @@ -0,0 +1,198 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "70a3521f-de88-4091-a7c9-1e19de1eceb6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from science_jubilee.Machine import Machine\n", + "from science_jubilee.tools.Camera import Camera\n", + "from science_jubilee.decks.Deck import Deck\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a75259b4-0cfe-436b-8c78-df28521c398e", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "__init__() got an unexpected keyword argument 'address'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/tmp/ipykernel_2880/4017261540.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mm\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mMachine\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maddress\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'jubilee.local'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m: __init__() got an unexpected keyword argument 'address'" + ] + } + ], + "source": [ + "m = Machine(address='jubilee.local')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12f2ef15-7b6b-43e1-908a-876a378c7545", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "camera = Camera(3, \"camtool\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd05fee6-c43f-4dda-b0ae-162a7f105ae5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m.reload_tool(camera)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e93bfe1b-9100-4df8-af90-9cb39251707c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m.pickup_tool(camera)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da05da9a-fc6b-43ad-a9e5-78c4c8c47ebd", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "deck = m.load_deck(\"lab_automation_deck_blair\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9768b053-7ee4-40c6-9fe5-db3b9ef96e7a", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# l = deck.load_labware(\"corning_24_wellplate_3.4ml_flat\", 0)\n", + "l = deck.load_labware(\"corning_96_wellplate_360ul_flat\", 0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8dc8bbca-e69c-4cec-bd5a-b834793540fb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "well = l[\"A1\"]\n", + "well" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "09bc0ff0-3735-4d45-b11c-2a9b8ff4c048", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m.move_to(x=well.x, y=well.y)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29f128f3-79fa-43e3-ad39-fe4732fdb8df", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "camera.video_stream()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a26d1f17-019d-48fb-bbf2-3969d839edff", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "x = 10\n", + "\n", + "y = x\n", + "y -= 10\n", + "\n", + "print(x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8848d4c4-8d5a-4e52-9d5f-96ddab012bd1", + "metadata": {}, + "outputs": [], + "source": [ + "m.move_to(x=150)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d3fcad5-dd34-43a7-b5d0-aace4bda523b", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "science_jubilee_testing", + "language": "python", + "name": "science_jubilee_testing" + }, + "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.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/science_jubilee/calibration/LabAutomationDeckCalibration.ipynb b/science_jubilee/calibration/LabAutomationDeckCalibration.ipynb new file mode 100644 index 0000000..6918b8f --- /dev/null +++ b/science_jubilee/calibration/LabAutomationDeckCalibration.ipynb @@ -0,0 +1,391 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "19366248", + "metadata": {}, + "source": [ + "# Lab Automation Deck Calibration\n", + "After installing a lab automation deck on the machine, we need to record reference positions for each of the six slots for exact alingment. Step through this notebook to create a lab_automation_deck_config.json file!\n", + "\n", + "This notebook assumes you have a top-down camera tool setup on your machine. If you don't, you can use another tool to manually align each offset.\n", + "\n", + "This notebook also uses" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f29d106", + "metadata": {}, + "outputs": [], + "source": [ + "from science_jubilee.Machine import Machine, get_root_dir\n", + "from science_jubilee.tools.Camera import Camera\n", + "from jinja2 import Environment, FileSystemLoader, select_autoescape\n", + "import json\n", + "import os\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "080e1201-a926-478f-b535-ea3a6c52996e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Set up the calibration file\n", + "# The following default values apply to the standard lab automation deck\n", + "# Adapt them if you have customized you deck!\n", + "deck_type = \"Lab Automation Deck\" # What type of deck is this?\n", + "num_slots = 6 # How many slots are there\n", + "num_sharps_containers = 0 # How many sharps containers are you using, if any?\n", + "slot_type = \"SLAS Standard Labware\" # What do these slots hold?\n", + "plate_material = \"Aluminum\" # What is your Jubilee bed plate material\n", + "mask_material = \"Delrin\" # What material is your deck made of?\n", + "\n", + "# Your lab automation deck slots will have 1 corner with no flexure element\n", + "# Specify whether this is the top_left, top_right, bottom_left, or bottom_right\n", + "# where 'right' means larger x values and 'top' means larger y values\n", + "offset_corner = \"bottom_left\" # What corner are you offsetting from?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "499e38aa-1637-4e93-956a-d4ede03ef93a", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# We'll populate slot_data using this set_slot_data function\n", + "slot_data = {} \n", + "def set_slot_data(slot_index: int):\n", + " position = m.get_position()\n", + " slot_offset = [float(position['X']), float(position['Y'])]\n", + " slot_data[slot_index] = slot_offset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6dbd036d", + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize your machine connection\n", + "m = Machine()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "284ab467", + "metadata": {}, + "outputs": [], + "source": [ + "# Check which tools are currently configured on your machine\n", + "m._configured_tools" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e0d591b", + "metadata": {}, + "outputs": [], + "source": [ + "# Load your camera tool\n", + "# Change this to match the index of your camera tool\n", + "camera = Camera(index=3, name=\"top_down_camera\") \n", + "m.load_tool(camera)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "519586c0-7ed0-42c5-a40a-be1d4015e1e2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m.pickup_tool(camera)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7d687f2", + "metadata": {}, + "outputs": [], + "source": [ + "# Move to your camera's focus height\n", + "# For me, that's z=30mm\n", + "m.move_to(z=30)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebce24d1", + "metadata": {}, + "outputs": [], + "source": [ + "# The 0th slot is closest to the machine's (0,0)\n", + "# Open a camera feed and position the camera using the duet web controller\n", + "# Move the camera so that the center of the camera is over the slot corner specified above as 'offset_corner'\n", + "# press esc when done to close the camera feed\n", + "m.move_to(x=30, y=30)\n", + "camera.video_stream()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e838c96", + "metadata": {}, + "outputs": [], + "source": [ + "# Save this position\n", + "slot_index = 0\n", + "set_slot_data(slot_index)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ff27687", + "metadata": {}, + "outputs": [], + "source": [ + "# Now, repeat this in the following cells for each of the other slots!\n", + "# Be sure tolign to the same corner for each slot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b286d5b", + "metadata": {}, + "outputs": [], + "source": [ + "# Slot 1\n", + "m.move(dx=140) # Move to approximate position of slot 1\n", + "camera.video_stream() # Fine tune the position using the camera feed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "164f087f-7403-4c13-9f26-eb53d72159d5", + "metadata": {}, + "outputs": [], + "source": [ + "# Save this position\n", + "slot_index = 1\n", + "set_slot_data(slot_index)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d9d9cdc", + "metadata": {}, + "outputs": [], + "source": [ + "# Slot 2\n", + "m.move(dx=-140, dy=100) # Move to approximate position of slot 2\n", + "camera.video_stream() # Fine tune the position using the camera feed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5cbca426-185d-4ae6-8b4a-1694c2cbe22e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Save this position\n", + "slot_index = 2\n", + "set_slot_data(slot_index)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "044ede74", + "metadata": {}, + "outputs": [], + "source": [ + "# Slot 3\n", + "m.move(dx=140) # Move to approximate position of slot 3\n", + "camera.video_stream() # Fine tune the position using the camera feed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "666a1b78", + "metadata": {}, + "outputs": [], + "source": [ + "# Save this position\n", + "slot_index = 3\n", + "set_slot_data(slot_index)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf5a4f67", + "metadata": {}, + "outputs": [], + "source": [ + "# Slot 4\n", + "m.move(dx=-140, dy=100) # Move to approximate position of slot 4\n", + "camera.video_stream() # Fine tune the position using the camera feed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d21b486", + "metadata": {}, + "outputs": [], + "source": [ + "# Save this position\n", + "slot_index = 4\n", + "set_slot_data(slot_index)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3608bc74", + "metadata": {}, + "outputs": [], + "source": [ + "# Slot 5\n", + "m.move(dx=140) # Move to approximate position of slot 5\n", + "camera.video_stream() # Fine tune the position using the camera feed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6132fbf", + "metadata": {}, + "outputs": [], + "source": [ + "# Save this position\n", + "slot_index = 5\n", + "set_slot_data(slot_index)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f428490e", + "metadata": {}, + "outputs": [], + "source": [ + "# If you have a sharps container installed, manually move to it\n", + "# Skip to \"Save Calibration File\" below if you aren't installing a sharps container\n", + "camera.video_stream() " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99803e1b-51e6-4399-86e8-7b6192e2552d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# We use negative slot indices for sharps containers\n", + "slot_index = -1\n", + "set_slot_data(slot_index)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43112001-9ad7-4996-a0b9-954bae66a855", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Save Calibration File\n", + "file_name = \"lab_automation_deck_MA\" # Change this if you'd like to refer to this calibration by a different name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "968e5ef9-db85-45fd-ac26-b5e55dc07745", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Run this cell to save your calibration file!\n", + "deck_config_path = os.path.join(get_root_dir(), \"science_jubilee\", \"decks\", \"deck_definition\", f\"{file_name}.json\")\n", + "env = Environment(loader=FileSystemLoader(\"templates\"))\n", + "template = env.get_template(\"lab_automation_deck_template.json\")\n", + "calibration_contents = template.render(deck_type=deck_type, num_slots=num_slots, num_sharps_containers=num_sharps_containers, slot_type=slot_type, plate_material=plate_material, mask_material=mask_material, offset_corner=offset_corner, slot_data=slot_data)\n", + "\n", + "with open(deck_config_path, 'w') as f:\n", + " f.write(calibration_contents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f0aca1a", + "metadata": {}, + "outputs": [], + "source": [ + "# Done!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89fc458d-14d2-452c-b9eb-903b96f1b687", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "plos-revision-submission", + "language": "python", + "name": "plos-revision-submission" + }, + "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.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/science_jubilee/calibration/templates/lab_automation_deck_template.json b/science_jubilee/calibration/templates/lab_automation_deck_template.json new file mode 100644 index 0000000..b1237da --- /dev/null +++ b/science_jubilee/calibration/templates/lab_automation_deck_template.json @@ -0,0 +1,22 @@ +{ + "deck_type": "{{deck_type}}", + "deck_slots": { + "total": {{num_slots}}, + "type": "{{slot_type}}" + }, + "slots": { {% for slot in range(-num_sharps_containers, num_slots) %} + "{{slot}}": { + "offset": {{slot_data[slot]}}, + "has_labware": false, + "labware": null + }{{"," if not loop.last else ""}} + {% endfor %} + }, + "offset_from": { + "corner": "{{offset_corner}}" + }, + "material": { + "plate": "{{plate_material}}", + "mask": "{{mask_material}}" + } +} diff --git a/science_jubilee/decks/deck_definition/lab_automation_deck_MA.json b/science_jubilee/decks/deck_definition/lab_automation_deck_MA.json new file mode 100644 index 0000000..6fd870b --- /dev/null +++ b/science_jubilee/decks/deck_definition/lab_automation_deck_MA.json @@ -0,0 +1,52 @@ +{ + "deck_type": "Lab Automation Deck", + "deck_slots": { + "total": 6, + "type": "SLAS Standard Labware" + }, + "slots": { + "0": { + "offset": [20.0, 7.3], + "has_labware": false, + "labware": null + }, + + "1": { + "offset": [160.0, 7.3], + "has_labware": false, + "labware": null + }, + + "2": { + "offset": [20.0, 107.3], + "has_labware": false, + "labware": null + }, + + "3": { + "offset": [160.0, 107.3], + "has_labware": false, + "labware": null + }, + + "4": { + "offset": [20.0, 207.3], + "has_labware": false, + "labware": null + }, + + "5": { + "offset": [160.0, 207.3], + "has_labware": false, + "labware": null + } + + }, + "offset_from": { + "corner": "bottom_left" + }, + "material": { + "plate": "Aluminum", + "mask": "Delrin" + } +} \ No newline at end of file diff --git a/science_jubilee/decks/deck_definition/lab_automation_deck_blair.json b/science_jubilee/decks/deck_definition/lab_automation_deck_blair.json index d5738bf..61b265a 100644 --- a/science_jubilee/decks/deck_definition/lab_automation_deck_blair.json +++ b/science_jubilee/decks/deck_definition/lab_automation_deck_blair.json @@ -12,7 +12,7 @@ }, "0": { - "offset": [146.500, 89.700], + "offset": [14.9, 4.4], "has_labware": false, "labware": null }, diff --git a/science_jubilee/labware/labware_definition/generic_petri_dish_100ml.json b/science_jubilee/labware/labware_definition/generic_petri_dish_100ml.json new file mode 100644 index 0000000..9bbe900 --- /dev/null +++ b/science_jubilee/labware/labware_definition/generic_petri_dish_100ml.json @@ -0,0 +1,61 @@ +{ + "ordering": [ + [ + "A1" + ] + ], + "brand": { + "brand": "Generic", + "brandId": [] + }, + "metadata": { + "displayName": "Generic Petri Dish 100mL", + "displayCategory": "reservoir", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127, + "yDimension": 85, + "zDimension": 23.5 + }, + "wells": { + "A1": { + "depth": 22, + "totalLiquidVolume": 100000, + "shape": "circular", + "diameter": 86, + "x": 44, + "y": 41, + "z": 1.5 + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "flat" + }, + "wells": [ + "A1" + ] + } + ], + "parameters": { + "format": "irregular", + "quirks": [ + "centerMultichannelOnWells", + "touchTipDisabled" + ], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "generic_petri_dish_100ml" + }, + "namespace": "custom_beta", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } +} \ No newline at end of file diff --git a/science_jubilee/tools/Camera.py b/science_jubilee/tools/Camera.py index 1c809a4..ef9fd43 100644 --- a/science_jubilee/tools/Camera.py +++ b/science_jubilee/tools/Camera.py @@ -1,5 +1,6 @@ from .Tool import Tool, ToolStateError, requires_active_tool - +from science_jubilee.labware.Labware import Labware, Well +from typing import Tuple, Union import cv2 import matplotlib @@ -20,7 +21,7 @@ def __init__(self, index, name): self._dist_matrix = None self.load_coefficients( - "/home/pi/duckbot/duckbot/tools/calibration_checkerboard.yml" + "/home/pi/plos-revision-submission/duckbot/science_jubilee/science_jubilee/tools/configs/calibration_checkerboard.yml" ) def load_coefficients(self, path): @@ -56,7 +57,7 @@ def get_camera_indices(self): index += 1 i -= 1 return arr - + @requires_active_tool def get_frame(self, resolution=[1200, 1200], uvc=False): with picamera.PiCamera() as camera: @@ -64,7 +65,7 @@ def get_frame(self, resolution=[1200, 1200], uvc=False): camera.framerate = 24 time.sleep(2) # print("... camera connection established") - output = np.empty((resolution[1], resolution[0], 3), dtype=np.uint8) + output = np.empty((resolution[1], resolution[0], 3), dtype=np.uint8) camera.capture(output, "rgb", use_video_port=True) tpose = np.transpose(output, axes=(1, 0, 2)) undistorted = cv2.undistort( @@ -114,3 +115,44 @@ def video_stream(self, camera_index=0): cap.release() cv2.destroyAllWindows() + + @requires_active_tool + def image_wells(self, resolution=[1200, 1200], uvc=False, wells: Well = None): + # TODO: different functions for saving many images, showing images, or getting frames for analysis? + if type(wells) != list: + wells = [wells] + + for well in wells: + x, y, z_bottom = self._get_xyz(well=well) + self._machine.safe_z_movement() + self._machine.move_to(x=x, y=y) + self._machine.move_to(z=30) # focus height; read in from config + time.sleep(1) # ToDo: Better way to sync gcode movements & images + f = self.get_frame() + self.show_frame(f) + + @requires_active_tool + def get_well_image(self, resolution=[1200, 1200], uvc=False, well: Well = None): + x, y, z_bottom = self._get_xyz(well=well) + self._machine.safe_z_movement() + self._machine.move_to(x=x, y=y) + self._machine.move_to(z=30) # focus height; read in from config + time.sleep(1) # ToDo: Better way to sync gcode movements & images + f = self.get_frame() + return f + + @staticmethod + def _get_xyz(well: Well = None, location: Tuple[float] = None): + if well is not None and location is not None: + raise ValueError("Specify only one of Well or x,y,z location") + elif well is not None: + x, y, z = well.x, well.y, well.z + else: + x, y, z = location + return x, y, z + + @staticmethod + def _get_top_bottom(well: Well = None): + top = well.top + bottom = well.bottom + return top, bottom \ No newline at end of file diff --git a/science_jubilee/tools/Loop.py b/science_jubilee/tools/Loop.py index 32e7d7b..54a8866 100644 --- a/science_jubilee/tools/Loop.py +++ b/science_jubilee/tools/Loop.py @@ -1,6 +1,100 @@ -from science_jubilee.tools.Tool import Tool, ToolStateError +from science_jubilee.tools.Tool import Tool, ToolStateError, ToolConfigurationError, requires_active_tool +from science_jubilee.labware.Labware import Labware, Well +from typing import Tuple, Union +import warnings +import numpy as np +import os +import json +import random class Loop(Tool): def __init__(self, index, name): super().__init__(index, name) + + @requires_active_tool + def transfer( + self, + s: int = 2000, + source: Well = None, + destination: Well = None, + sweep_x: float = 5, + sweep_y: float = 5, + sweep_z: float = 10, + sweep_speed: float = 100, + up_speed: float = 800, + randomize_pickup: bool = False, + ): + if type(source) != list: + source = [source] + if type(destination) != list: + destination = [destination] + + # Assemble tuples of (source, destination) + num_source_wells = len(source) + num_destination_wells = len(destination) + if num_source_wells == num_destination_wells: # n to n transfers + pass + elif num_source_wells == 1 and num_destination_wells > 1: # one to many transfers + source = list(np.repeat(source, num_destination_wells)) + elif num_source_wells > 1 and num_destination_wells == 1: # many to one transfers + destination = list(np.repeat(destination, num_source_wells)) + elif num_source_wells > 1 and num_destination_wells > 1: # uneven transfers + # for uneven transfers, find least common multiple to pair off wells + # raise a warning, as this might be a mistake + # this mimics OT-2 behavior + least_common_multiple = np.lcm(num_source_wells, num_destination_wells) + source_repeat = least_common_multiple / num_source_wells + destination_repeat = least_common_multiple / num_destination_wells + source = list(np.repeat(source, source_repeat)) + destination = list(np.repeat(destination, destination_repeat)) + warnings.warn("Warning: Uneven source & destination wells specified.") + + source_destination_pairs = list(zip(source, destination)) + for source_well, destination_well in source_destination_pairs: + xs, ys, zs = self._get_xyz(well=source_well) + if randomize_pickup: # to make sure we don't try to pickup from an empty region + r = 20 + rx = random.randint(-r, r) + ry = random.randint(-r, r) + xs += rx + ys += ry + xd, yd, zd = self._get_xyz(well=destination_well) + + self._machine.safe_z_movement() + self._machine.move_to(x=xs, y=ys) + self._machine.move_to(z=zs + 5) + # slowly sweep in the reservoir to pick up duckweed + # can tune these default values + self._machine.move(dx=sweep_x, s=sweep_speed) + self._machine.move(dy=sweep_y, s=sweep_speed) + self._machine.move(dz=sweep_z, s=up_speed) + self.current_well = source_well + # self._aspirate(vol, s=s) + + self._machine.safe_z_movement() + self._machine.move_to(x=xd, y=yd) + self._machine.move_to(z=zd + 5) + # sweep again to drop off duckweed + # make smaller movements and move opposite direction + self._machine.move(dx=sweep_x/2, s=sweep_speed) + self._machine.move(dy=-sweep_y, s=sweep_speed) + self._machine.dwell(250) # give the duckweed time to release + self.current_well = destination_well + # self._dispense(vol, s=s) + + @staticmethod + def _get_xyz(well: Well = None, location: Tuple[float] = None): + if well is not None and location is not None: + raise ValueError("Specify only one of Well or x,y,z location") + elif well is not None: + x, y, z = well.x, well.y, well.z + else: + x, y, z = location + return x, y, z + + @staticmethod + def _get_top_bottom(well: Well = None): + top = well.top + bottom = well.bottom + return top, bottom \ No newline at end of file diff --git a/science_jubilee/tools/Syringe.py b/science_jubilee/tools/Syringe.py index f6f11e6..e3ca324 100644 --- a/science_jubilee/tools/Syringe.py +++ b/science_jubilee/tools/Syringe.py @@ -44,7 +44,7 @@ def post_load(self): """Find extruder drive for this tool.""" # To read the position of an extruder, we need to know which extruder # to look at # Query the object model to find this - tool_info = json.loads(self._machine.send('M409 K"tools[]"'))["result"] + tool_info = json.loads(self._machine.gcode('M409 K"tools[]"'))["result"] for tool in tool_info: if tool["number"] == self.index: self.e_drive = f"E{tool['extruders'][0]}" # Syringe tool has only 1 extruder diff --git a/science_jubilee/tools/Tool.py b/science_jubilee/tools/Tool.py index 58a4fbc..041d75b 100644 --- a/science_jubilee/tools/Tool.py +++ b/science_jubilee/tools/Tool.py @@ -34,10 +34,9 @@ def requires_active_tool(func): """Decorator to ensure that a tool cannot complete an action unless it is the current active tool. """ - # print('check') def wrapper(self, *args, **kwargs): if self.is_active_tool == False: raise ToolStateError (f"Error: Tool {self.name} is not the current `Active Tool`. Cannot perform this action") else: - func(self,*args, **kwargs) - return wrapper \ No newline at end of file + return func(self,*args, **kwargs) + return wrapper diff --git a/science_jubilee/tools/configs/calibration_checkerboard.yml b/science_jubilee/tools/configs/calibration_checkerboard.yml new file mode 100644 index 0000000..a66cba3 --- /dev/null +++ b/science_jubilee/tools/configs/calibration_checkerboard.yml @@ -0,0 +1,15 @@ +%YAML:1.0 +--- +K: !!opencv-matrix + rows: 3 + cols: 3 + dt: d + data: [ 3.3112734499444791e+03, 0., 6.8998248444736601e+02, 0., + 3.2240712043235176e+03, 3.8412116837794224e+02, 0., 0., 1. ] +D: !!opencv-matrix + rows: 1 + cols: 5 + dt: d + data: [ -1.4977421054844131e+00, 3.0708652349007988e+00, + 1.1789665067175169e-01, -4.0539193246590405e-03, + -1.4856365965816121e+01 ]