diff --git a/HTTP_debug.ipynb b/HTTP_debug.ipynb new file mode 100644 index 0000000..6e1f784 --- /dev/null +++ b/HTTP_debug.ipynb @@ -0,0 +1,757 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "c08001d7-7e69-4435-a26b-dad40fa6278b", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "%load_ext autoreload\n", + "%autoreload 2\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "7b79016e-5203-459a-8cc4-6a548c36689c", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "bb0d95d9-8d8e-4cc8-9504-b84971d3bdd9", + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import contextlib\n", + "from http.client import HTTPConnection\n", + "\n", + "def debug_requests_on():\n", + " '''Switches on logging of the requests module.'''\n", + " HTTPConnection.debuglevel = 1\n", + "\n", + " logging.basicConfig(filename = 'requestslog.log', format = '%(asctime)s %(message)s')\n", + " logging.getLogger().setLevel(logging.DEBUG)\n", + " requests_log = logging.getLogger(\"requests.packages.urllib3\")\n", + " requests_log.setLevel(logging.DEBUG)\n", + " requests_log.propagate = True\n", + "\n", + "def debug_requests_off():\n", + " '''Switches off logging of the requests module, might be some side-effects'''\n", + " HTTPConnection.debuglevel = 0\n", + "\n", + " root_logger = logging.getLogger()\n", + " root_logger.setLevel(logging.WARNING)\n", + " root_logger.handlers = []\n", + " requests_log = logging.getLogger(\"requests.packages.urllib3\")\n", + " requests_log.setLevel(logging.WARNING)\n", + " requests_log.propagate = False" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ffd76ba0-e80d-402d-b7de-73765dd8fb88", + "metadata": {}, + "outputs": [], + "source": [ + "debug_requests_on()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6e065e90-1633-4ff8-be82-47ce0dde17ad", + "metadata": {}, + "outputs": [], + "source": [ + "## try current reqeust method\n", + "jubilee_ip = '192.168.1.2'\n", + "\n", + "gcode_home = 'G28'\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd8c8c3a-e776-4551-887c-15ebb69bcfd7", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "\n", + "reponse = requests.post(\n", + " f\"http://{jubilee_ip}/machine/code\", data=f\"{gcode_home}\", timeout=5\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "6cdc83c2-a413-4624-a6bf-3df1cd43917b", + "metadata": {}, + "outputs": [], + "source": [ + "session = requests.session()\n", + "session.keep_alive = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70448aab-42b9-4d55-8c3f-e8a46df9794f", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "dd3e1331-9dbe-40da-80bc-430e7fa7f6d7", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'jubilee_ip' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[10], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m r \u001b[38;5;241m=\u001b[39m session\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mhttp://\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mjubilee_ip\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m/rr_model?key=seqs\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mNameError\u001b[0m: name 'jubilee_ip' is not defined" + ] + } + ], + "source": [ + "r = session.get(f\"http://{jubilee_ip}/rr_model?key=seqs\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c86cdc2a-2dfa-4379-b000-e29765ffa1d2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "200" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "r.status_code" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "78ddbcab-22d4-4de1-b4f7-5712f569210b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: keep-alive\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n" + ] + } + ], + "source": [ + "reply_response = session.get(\n", + " f\"http://{jubilee_ip}/rr_model?key=seqs\"\n", + " )\n", + "logging.debug(f'MODEL response, status: {reply_response.status_code}, headers:{reply_response.headers}, content:{reply_response.content}')\n", + "\n", + "reply_count = reply_response.json()[\"result\"][\"reply\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "7ff38d19-ced0-49ff-aa05-6f7b53dcad11", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "16" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "reply_count" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "c356ee78-77a5-455d-a725-1c828d092275", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "send: b'GET /rr_gcode?gcode=G28 HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: keep-alive\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 12\n", + "header: Connection: close\n" + ] + } + ], + "source": [ + "buffer_response = session.get(\n", + " f\"http://{jubilee_ip}/rr_gcode?gcode={gcode_home}\", timeout=30\n", + " )\n", + "logging.debug(f'GCODE response, status: {buffer_response.status_code}, headers:{buffer_response.headers}, content:{buffer_response.content}')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "5ba4c6ef-7cee-439b-a780-02ff397b69f9", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'buffer_response' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[7], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mbuffer_response\u001b[49m\n", + "\u001b[0;31mNameError\u001b[0m: name 'buffer_response' is not defined" + ] + } + ], + "source": [ + "reply_r" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "ee300d84-12fb-4403-b57e-a2abfb7a4466", + "metadata": {}, + "outputs": [], + "source": [ + "def delay_time(n):\n", + " \"\"\"\n", + " Calculate delay time for next request. dumb hard code for now, could be fancy exponential backoff\n", + " \"\"\"\n", + " if n == 0:\n", + " return 0\n", + " if n < 10:\n", + " return 0.1\n", + " if n < 20:\n", + " return 0.2\n", + " if n < 30:\n", + " return 0.3\n", + " else:\n", + " return 1\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "27551685-c0d1-453f-a562-aebf4c6e9aa5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: keep-alive\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "Response: \n", + "send: b'GET /rr_reply HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: keep-alive\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: text/plain\n", + "header: Content-Length: 0\n", + "header: Connection: close\n" + ] + } + ], + "source": [ + "tic = time.time()\n", + "response_wait = 60\n", + "\n", + "try_count = 0\n", + "while True:\n", + " try:\n", + " response = session.get(\n", + " f\"http://{jubilee_ip}/rr_model?key=seqs\"\n", + " )\n", + " logging.debug(f'MODEL response, status: {response.status_code}, headers:{response.headers}, content:{response.content}')\n", + " print('Response: ', response)\n", + " new_reply_count = response.json()[\"result\"][\"reply\"]\n", + " if new_reply_count != reply_count:\n", + " response = session.get(\n", + " f\"http://{jubilee_ip}/rr_reply\"\n", + " )\n", + " logging.debug(f'REPLY response, status: {response.status_code}, headers:{response.headers}, content:{response.content}')\n", + " response = response.text\n", + " break\n", + " elif time.time() - tic > response_wait:\n", + " response = None\n", + " break\n", + " print(f'Try count: {try_count}, delay time: {delay_time(try_count)} s')\n", + " time.sleep(delay_time(try_count))\n", + " try_count += 1\n", + " except Exception as e:\n", + " print(f\"Connection error {e}, sleeping 1 second\")\n", + " logging.debug(f'Error in gcode reply wait loop: {e}')\n", + " time.sleep(2)\n", + " continue" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "2ac91a82-c5b3-433e-89f6-d1d974f8b563", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'text/plain', 'Content-Length': '0', 'Connection': 'close'}" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "response.headers" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "6528c863-ef47-4c15-88c9-2e842451eabf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "send: b'GET /rr_reply HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: keep-alive\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: text/plain\n", + "header: Content-Length: 0\n", + "header: Connection: close\n" + ] + } + ], + "source": [ + "response = session.get(\n", + " f\"http://{jubilee_ip}/rr_reply\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "2d9daab1-c964-4faf-b715-1bfa71059c5d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "''" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "response.text" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "6c0915a4-8b19-4bb8-98e7-05a619a6790a", + "metadata": {}, + "outputs": [], + "source": [ + "from science_jubilee import Machine" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "121dbac9-f913-4f95-844f-8e2c16daf574", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "send: b'POST /machine/code HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: keep-alive\\r\\nContent-Length: 25\\r\\n\\r\\n'\n", + "send: b'M409 K\"move.axes[].homed\"'\n", + "reply: 'HTTP/1.1 500 only rr_upload is supported for POST requests\\r\\n'\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_gcode?gcode=M409%20K%22move.axes%5B%5D.homed%22 HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 12\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_reply HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: text/plain\n", + "header: Content-Length: 84\n", + "header: Connection: close\n", + "send: b'POST /machine/code HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: keep-alive\\r\\nContent-Length: 19\\r\\n\\r\\n'\n", + "send: b'M409 K\"move.axes[]\"'\n", + "reply: 'HTTP/1.1 500 only rr_upload is supported for POST requests\\r\\n'\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_gcode?gcode=M409%20K%22move.axes%5B%5D%22 HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 12\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_reply HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: text/plain\n", + "header: Content-Length: 2044\n", + "header: Connection: close\n", + "send: b'POST /machine/code HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: keep-alive\\r\\nContent-Length: 1\\r\\n\\r\\n'\n", + "send: b'T'\n", + "reply: 'HTTP/1.1 500 only rr_upload is supported for POST requests\\r\\n'\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_gcode?gcode=T HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 12\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_reply HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: text/plain\n", + "header: Content-Length: 20\n", + "header: Connection: close\n", + "send: b'POST /machine/code HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: keep-alive\\r\\nContent-Length: 13\\r\\n\\r\\n'\n", + "send: b'M409 K\"tools\"'\n", + "reply: 'HTTP/1.1 500 only rr_upload is supported for POST requests\\r\\n'\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_gcode?gcode=M409%20K%22tools%22 HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 12\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_reply HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: text/plain\n", + "header: Content-Length: 782\n", + "header: Connection: close\n", + "send: b'POST /machine/code HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: keep-alive\\r\\nContent-Length: 17\\r\\n\\r\\n'\n", + "send: b'M409 K\"move.axes\"'\n", + "reply: 'HTTP/1.1 500 only rr_upload is supported for POST requests\\r\\n'\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_gcode?gcode=M409%20K%22move.axes%22 HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 12\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_reply HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: text/plain\n", + "header: Content-Length: 2042\n", + "header: Connection: close\n", + "send: b'POST /machine/code HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: keep-alive\\r\\nContent-Length: 3\\r\\n\\r\\n'\n", + "send: b'G90'\n", + "reply: 'HTTP/1.1 500 only rr_upload is supported for POST requests\\r\\n'\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_gcode?gcode=G90 HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 12\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_reply HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: text/plain\n", + "header: Content-Length: 1\n", + "header: Connection: close\n", + "send: b'POST /machine/code HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: keep-alive\\r\\nContent-Length: 3\\r\\n\\r\\n'\n", + "send: b'G90'\n", + "reply: 'HTTP/1.1 500 only rr_upload is supported for POST requests\\r\\n'\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_gcode?gcode=G90 HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 12\n", + "header: Connection: close\n", + "send: b'GET /rr_model?key=seqs HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: application/json\n", + "header: Content-Length: 243\n", + "header: Connection: close\n", + "send: b'GET /rr_reply HTTP/1.1\\r\\nHost: 192.168.1.2\\r\\nUser-Agent: python-requests/2.31.0\\r\\nAccept-Encoding: gzip, deflate, br\\r\\nAccept: */*\\r\\nConnection: close\\r\\n\\r\\n'\n", + "reply: 'HTTP/1.1 200 OK\\r\\n'\n", + "header: Cache-Control: no-cache, no-store, must-revalidate\n", + "header: Pragma: no-cache\n", + "header: Expires: 0\n", + "header: Content-Type: text/plain\n", + "header: Content-Length: 1\n", + "header: Connection: close\n" + ] + } + ], + "source": [ + "jub = Machine.Machine(address = '192.168.1.2')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2ba18b02-d1e5-465f-9103-6f5218812181", + "metadata": {}, + "outputs": [], + "source": [ + "jub.home_all()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "01e13d61-4284-4cdd-af75-b82220bf3332", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'User-Agent': 'python-requests/2.31.0', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': '*/*', 'Connection': 'close'}" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "jub.session.headers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c76d58e-1d39-4462-b8cc-aaa9c19c8f50", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Untitled.ipynb b/Untitled.ipynb new file mode 100644 index 0000000..3906d8c --- /dev/null +++ b/Untitled.ipynb @@ -0,0 +1,134 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "b656762c-1ca9-4f9c-9ba6-86a3f7190265", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1127740c-7620-447b-8204-741dfc8fe3cc", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "# ----------- Science Jubilee -------------\n", + "from science_jubilee import Machine as Jub\n", + "from science_jubilee.tools import HTTPSyringe as syringe\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "64788e2b-4060-4b44-8c87-cf6a880ad318", + "metadata": {}, + "outputs": [], + "source": [ + "url = 'http://192.168.1.5:5000'\n", + "volume = {'volume':5}" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d1be2c94-8669-4bea-b057-a4a3acf035fd", + "metadata": {}, + "outputs": [], + "source": [ + "r = requests.get(url + '/get_status')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8228ca74-92e4-4761-b4ef-ef0c50fe7381", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee = Jub.Machine(address='192.168.1.2', simulated = False) " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c3d3bf61-1aed-476b-b36c-e2c5891f4986", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.home_all()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30f0c9b9-98da-46f9-945c-34607d3abaf5", + "metadata": {}, + "outputs": [], + "source": [ + "deck = jubilee.load_deck('lab_automation_deck.json')" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "8378fc8c-b57c-4f31-8acd-f681403444cc", + "metadata": {}, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'super' object has no attribute '_HTTPSyringe__init'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[13], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m syringe \u001b[38;5;241m=\u001b[39m \u001b[43msyringe\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mHTTPSyringe\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfrom_config\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m3\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m/home/brendenpelkie/Code/science-jubilee/src/science_jubilee/tools/configs/10cc_syringe.json\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Code/science-jubilee/src/science_jubilee/tools/HTTPSyringe.py:47\u001b[0m, in \u001b[0;36mHTTPSyringe.from_config\u001b[0;34m(cls, index, fp)\u001b[0m\n\u001b[1;32m 44\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(fp) \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[1;32m 45\u001b[0m kwargs \u001b[38;5;241m=\u001b[39m json\u001b[38;5;241m.\u001b[39mload(f)\n\u001b[0;32m---> 47\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mcls\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mindex\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Code/science-jubilee/src/science_jubilee/tools/HTTPSyringe.py:31\u001b[0m, in \u001b[0;36mHTTPSyringe.__init__\u001b[0;34m(self, index, url)\u001b[0m\n\u001b[1;32m 27\u001b[0m config_r \u001b[38;5;241m=\u001b[39m requests\u001b[38;5;241m.\u001b[39mget(url\u001b[38;5;241m+\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m/get_config\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 29\u001b[0m config \u001b[38;5;241m=\u001b[39m config_r\u001b[38;5;241m.\u001b[39mjson\n\u001b[0;32m---> 31\u001b[0m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m__init\u001b[49m(index, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mconfig, url \u001b[38;5;241m=\u001b[39m url)\n\u001b[1;32m 33\u001b[0m status_r \u001b[38;5;241m=\u001b[39m requests\u001b[38;5;241m.\u001b[39mget(url \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m/get_status\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 35\u001b[0m status \u001b[38;5;241m=\u001b[39m status_r\u001b[38;5;241m.\u001b[39mjson\n", + "\u001b[0;31mAttributeError\u001b[0m: 'super' object has no attribute '_HTTPSyringe__init'" + ] + } + ], + "source": [ + "syringe = syringe.HTTPSyringe.from_config(3, \"/home/brendenpelkie/Code/science-jubilee/src/science_jubilee/tools/configs/10cc_syringe.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4febab10-2592-4ffa-8435-79944923c1f8", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/debug_syringe-Copy1.ipynb b/debug_syringe-Copy1.ipynb new file mode 100644 index 0000000..3afa8d7 --- /dev/null +++ b/debug_syringe-Copy1.ipynb @@ -0,0 +1,466 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "b656762c-1ca9-4f9c-9ba6-86a3f7190265", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1127740c-7620-447b-8204-741dfc8fe3cc", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "# ----------- Science Jubilee -------------\n", + "from science_jubilee import Machine as Jub\n", + "from science_jubilee.tools import HTTPSyringe as syringe\n", + "import time\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "64788e2b-4060-4b44-8c87-cf6a880ad318", + "metadata": {}, + "outputs": [], + "source": [ + "url = 'http://192.168.1.5:5000'\n", + "volume = {'volume':5}" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d1be2c94-8669-4bea-b057-a4a3acf035fd", + "metadata": {}, + "outputs": [], + "source": [ + "r = requests.post(url + '/get_status', json = {'name':'1cc_2'})" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "bfd36a85-fdfa-47d3-9899-b1193f0c24a7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "b'{\"remaining_volume\":null,\"syringe_loaded\":false}\\n'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "r.content" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "8228ca74-92e4-4761-b4ef-ef0c50fe7381", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee = Jub.Machine(address='192.168.1.2', simulated = False) " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c3d3bf61-1aed-476b-b36c-e2c5891f4986", + "metadata": {}, + "outputs": [], + "source": [ + "#jubilee.home_all()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "30f0c9b9-98da-46f9-945c-34607d3abaf5", + "metadata": {}, + "outputs": [], + "source": [ + "deck = jubilee.load_deck('lab_automation_deck.json')" + ] + }, + { + "cell_type": "markdown", + "id": "9b3f6a03-ceb8-4e92-8018-5f451dec546d", + "metadata": {}, + "source": [ + "# load a well plate " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "76695d63-a431-4f0c-b3ef-4f7d7a4dd976", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New manual offset applied to falcon_48_wellplate_1500ul\n" + ] + } + ], + "source": [ + "samples = jubilee.load_labware('falcon_48_wellplate_1500ul.json', 0)\n", + "samples.manual_offset([(21.4, 82.9), (134.1, 83.2), (133.9, 12.8)])" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "ddb61ae3-feaf-4c6d-a7eb-1ad8abe41454", + "metadata": {}, + "outputs": [], + "source": [ + "stocks = jubilee.load_labware('20mlscintillation_12_wellplate_18000ul.json', 3)\n", + "#stocks.manual_offset()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "8378fc8c-b57c-4f31-8acd-f681403444cc", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_10 = syringe.HTTPSyringe.from_config(1, \"src/science_jubilee/tools/configs/10cc_syringe.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "803089bb-35b7-4590-925c-220f86a98827", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_1_1 = syringe.HTTPSyringe.from_config(2, \"src/science_jubilee/tools/configs/1cc_1_syringe.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "05164855-022b-4ca1-ba95-44ccaece855b", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_1_2 = syringe.HTTPSyringe.from_config(3, \"src/science_jubilee/tools/configs/1cc_2_syringe.json\")\n", + "syringe_1_3 = syringe.HTTPSyringe.from_config(4, \"src/science_jubilee/tools/configs/1cc_3_syringe.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "922e4387-4089-43cd-8b60-4a52b6dd86f8", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "cdf950ad-b31f-4af7-883f-28ed9be7ebe0", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.load_tool(syringe_10)\n", + "jubilee.load_tool(syringe_1_1)\n", + "jubilee.load_tool(syringe_1_2)\n", + "jubilee.load_tool(syringe_1_3)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "4febab10-2592-4ffa-8435-79944923c1f8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'remaining_volume': None, 'syringe_loaded': False}" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "syringe_1_1.status()" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "cd70098e-8cf9-40db-a50b-7c72cc723979", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.pickup_tool(syringe_1_1)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "df8d2f4e-b37a-4fe5-a2a1-2aa871dcfcea", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_1_1.set_pulsewidth(1850)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "cc8a3f25-f2de-4440-91ff-ab139923bc64", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded syringe, remaining volume 0 uL\n" + ] + } + ], + "source": [ + "syringe_1_1.load_syringe(0, 1850)" + ] + }, + { + "cell_type": "markdown", + "id": "780dab33-4b61-4d6f-b0e7-50e51fe4d79a", + "metadata": {}, + "source": [ + "## Manual syringe load:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "a9791e7c-fc23-48dc-a77b-0743bead2bcd", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_1_1.set_pulsewidth(syringe_1_1.empty_position-1)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "c9a07015-1ba3-46e7-b649-375d9c0d1cd4", + "metadata": {}, + "outputs": [], + "source": [ + "aspirate = samples[0].bottom(+200)\n", + "aspirate_vol = 995 #- syringe_1_1.remaining_volume\n", + "#aspirate_vol = 9995\n", + "syringe_1_1.aspirate(aspirate_vol, aspirate)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "0a969881-81f4-4fd2-9853-787a5f7af82a", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_1_1.set_pulsewidth(1550)" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "id": "5b057cac-d29f-465e-85bd-bd67674f8581", + "metadata": {}, + "outputs": [], + "source": [ + "## Run dispense experiment " + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "id": "384dddd4-8986-4472-8a36-62af9542644e", + "metadata": {}, + "outputs": [], + "source": [ + "# time step sequence:\n", + "\"\"\"\n", + "3 samples per each:\n", + "- 1\n", + "- 10\n", + "- 50\n", + "- 100\n", + "- 200\n", + "- 300\n", + "\"\"\"\n", + "\n", + "#### Modify here\n", + "test_step = 300\n", + "\n", + "######Don't touch here ######\n", + "syringe_1.set_pulsewidth(1300)\n", + "syringe_pulsewidth = 1300\n", + "\n", + "jubilee.pickup_tool(syringe_1)\n", + "for i in range(1):\n", + " dispense_location = samples[i].bottom(+25)\n", + " new_pulsewidth = syringe_pulsewidth + test_step\n", + " syringe_pulsewidth = new_pulsewidth\n", + " jubilee.move_to(z = jubilee.deck.safe_z)\n", + "\n", + " jubilee.move_to(x = dispense_location.point[0], y = dispense_location.point[1], wait = True)\n", + " jubilee.move_to(z = dispense_location.point[2], wait = True)\n", + " syringe_1.set_pulsewidth(new_pulsewidth)\n", + " time.sleep(10)\n" + ] + }, + { + "cell_type": "markdown", + "id": "d4a97722-fa87-4429-a402-ebd10249d3e8", + "metadata": {}, + "source": [ + "## Check calibration" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "35e34491-d83c-4170-be01-9e57e69df13e", + "metadata": {}, + "outputs": [], + "source": [ + "#syringe_1_1.dispense(30, samples[0])" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "da2dab5b-559b-4292-b9b4-1fa712627b93", + "metadata": {}, + "outputs": [], + "source": [ + "dispense_vol = 100\n", + "\n", + "#jubilee.pickup_tool(syringe_1)\n", + "for i in range(3):\n", + " dispense_location = samples[i].bottom(+30)\n", + " syringe_1_1.dispense(dispense_vol, dispense_location)\n", + " time.sleep(7)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "eeb72787-3a2d-4892-950d-d0002fbe3ac7", + "metadata": {}, + "outputs": [], + "source": [ + "dispense_location = samples[1].bottom(+25)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "73d8deb6-647a-4a8d-9b55-17571b8bb32d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Location(point=(37.499996100392444, 82.85570212147425, 27.41), labware=Well A2 form Falcon 48 Well Plate 1500 µL on slot 0)" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dispense_location" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "28c46881-8743-4c64-8ed7-410c14e4807f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Well A2 form Falcon 48 Well Plate 1500 µL on slot 0" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "samples[1]" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "56618d83-1ab6-4e32-aff7-4efc4b2dcf7f", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.park_tool()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d060c6b6-c93e-42ac-b4d3-9270b8b224c1", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.9.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/debug_syringe.ipynb b/debug_syringe.ipynb new file mode 100644 index 0000000..5dd8e8a --- /dev/null +++ b/debug_syringe.ipynb @@ -0,0 +1,400 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "b656762c-1ca9-4f9c-9ba6-86a3f7190265", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1127740c-7620-447b-8204-741dfc8fe3cc", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "# ----------- Science Jubilee -------------\n", + "from science_jubilee import Machine as Jub\n", + "from science_jubilee.tools import HTTPSyringe as syringe\n", + "import time\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "64788e2b-4060-4b44-8c87-cf6a880ad318", + "metadata": {}, + "outputs": [], + "source": [ + "url = 'http://192.168.1.5:5000'\n", + "volume = {'volume':5}" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d1be2c94-8669-4bea-b057-a4a3acf035fd", + "metadata": {}, + "outputs": [], + "source": [ + "r = requests.post(url + '/get_status', json = {'name':'10cc'})" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "bfd36a85-fdfa-47d3-9899-b1193f0c24a7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "b'{\"remaining_volume\":null,\"syringe_loaded\":false}\\n'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "r.content" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "8228ca74-92e4-4761-b4ef-ef0c50fe7381", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee = Jub.Machine(address='192.168.1.2', simulated = False) " + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "id": "c3d3bf61-1aed-476b-b36c-e2c5891f4986", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.home_all()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "30f0c9b9-98da-46f9-945c-34607d3abaf5", + "metadata": {}, + "outputs": [], + "source": [ + "deck = jubilee.load_deck('lab_automation_deck.json')" + ] + }, + { + "cell_type": "markdown", + "id": "9b3f6a03-ceb8-4e92-8018-5f451dec546d", + "metadata": {}, + "source": [ + "# load a well plate " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "76695d63-a431-4f0c-b3ef-4f7d7a4dd976", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New manual offset applied to falcon_48_wellplate_1500ul\n" + ] + } + ], + "source": [ + "samples = jubilee.load_labware('falcon_48_wellplate_1500ul.json', 0)\n", + "samples.manual_offset([(21.4, 82.9), (134.1, 83.2), (133.9, 12.8)])" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "ddb61ae3-feaf-4c6d-a7eb-1ad8abe41454", + "metadata": {}, + "outputs": [], + "source": [ + "stocks = jubilee.load_labware('20mlscintillation_12_wellplate_18000ul.json', 3)\n", + "#stocks.manual_offset()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "8378fc8c-b57c-4f31-8acd-f681403444cc", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_10 = syringe.HTTPSyringe.from_config(3, \"/home/brendenpelkie/Code/science-jubilee/src/science_jubilee/tools/configs/10cc_syringe.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "803089bb-35b7-4590-925c-220f86a98827", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_1 = syringe.HTTPSyringe.from_config(2, \"/home/brendenpelkie/Code/science-jubilee/src/science_jubilee/tools/configs/1cc_syringe.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "cdf950ad-b31f-4af7-883f-28ed9be7ebe0", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.load_tool(syringe_10)\n", + "jubilee.load_tool(syringe_1)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "4febab10-2592-4ffa-8435-79944923c1f8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'remaining_volume': None, 'syringe_loaded': False}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "syringe_1.status()" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "586398ab-1ba1-4521-9257-e0a59108df0d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'1cc'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "syringe_1.name" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "df8d2f4e-b37a-4fe5-a2a1-2aa871dcfcea", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_1.set_pulsewidth(1500)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "cc8a3f25-f2de-4440-91ff-ab139923bc64", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded syringe, remaining volume 560 uL\n" + ] + } + ], + "source": [ + "syringe_1.load_syringe(560, 1500)" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "abf10a23-7f03-4340-80a1-8682b7726ff4", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.park_tool()" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "id": "7e642d60-0d64-476a-98d1-7d5e50786c9f", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.pickup_tool(syringe_1)" + ] + }, + { + "cell_type": "markdown", + "id": "780dab33-4b61-4d6f-b0e7-50e51fe4d79a", + "metadata": {}, + "source": [ + "## Manual syringe load:" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "id": "a9791e7c-fc23-48dc-a77b-0743bead2bcd", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_1.set_pulsewidth(syringe_1.empty_position-1)" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "id": "c9a07015-1ba3-46e7-b649-375d9c0d1cd4", + "metadata": {}, + "outputs": [], + "source": [ + "aspirate = samples[0].bottom(+200)\n", + "aspirate_vol = 995 - syringe_1.remaining_volume\n", + "#aspirate_vol = 9995\n", + "syringe_1.aspirate(aspirate_vol, aspirate)" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "id": "0a969881-81f4-4fd2-9853-787a5f7af82a", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_1.set_pulsewidth(1280)" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "5b057cac-d29f-465e-85bd-bd67674f8581", + "metadata": {}, + "outputs": [], + "source": [ + "## Run dispense experiment " + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "384dddd4-8986-4472-8a36-62af9542644e", + "metadata": {}, + "outputs": [], + "source": [ + "# time step sequence:\n", + "\"\"\"\n", + "3 samples per each:\n", + "- 1\n", + "- 10\n", + "- 50\n", + "- 100\n", + "- 200\n", + "- 300\n", + "\"\"\"\n", + "\n", + "#### Modify here\n", + "test_step = 100\n", + "\n", + "######Don't touch here ######\n", + "syringe_1.set_pulsewidth(1300)\n", + "syringe_pulsewidth = 1300\n", + "\n", + "jubilee.pickup_tool(syringe_1)\n", + "for i in range(1):\n", + " dispense_location = samples[i].bottom(+55)\n", + " new_pulsewidth = syringe_pulsewidth + test_step\n", + " syringe_pulsewidth = new_pulsewidth\n", + "\n", + " jubilee.move_to(x = dispense_location.point[0], y = dispense_location.point[1], wait = True)\n", + " jubilee.move_to(z = dispense_location.point[2], wait = True)\n", + " syringe_1.set_pulsewidth(new_pulsewidth)\n", + " time.sleep(10)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "da2dab5b-559b-4292-b9b4-1fa712627b93", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(176.41, 170.79, 67.5)" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dispense_location.point" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eeb72787-3a2d-4892-950d-d0002fbe3ac7", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/building/_static/center_carriage.jpg:Zone.Identifier b/docs/building/_static/center_carriage.jpg:Zone.Identifier new file mode 100644 index 0000000..d6b76ee --- /dev/null +++ b/docs/building/_static/center_carriage.jpg:Zone.Identifier @@ -0,0 +1,4 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=https://photos.google.com/ +HostUrl=https://photos.fife.usercontent.google.com/pw/AP1GczNTKqrB6x5tvbBMxMTJFyDbgy_zk3AxiAneT9psVhzxOWMV_G5hJgculg=w1118-h839-s-no?authuser=0 diff --git a/docs/building/_static/manual_tool_load.jpg:Zone.Identifier b/docs/building/_static/manual_tool_load.jpg:Zone.Identifier new file mode 100644 index 0000000..9471aa5 --- /dev/null +++ b/docs/building/_static/manual_tool_load.jpg:Zone.Identifier @@ -0,0 +1,4 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=https://photos.google.com/ +HostUrl=https://photos.fife.usercontent.google.com/pw/AP1GczOZskrK77M2lCCHUA6WmsVrBlFzXpJk3F_SxMnI3xSaLL2wpOspwIOUOg=w755-h839-s-no?authuser=0 diff --git a/docs/building/_static/parking_post_installed.jpg:Zone.Identifier b/docs/building/_static/parking_post_installed.jpg:Zone.Identifier new file mode 100644 index 0000000..3d13072 --- /dev/null +++ b/docs/building/_static/parking_post_installed.jpg:Zone.Identifier @@ -0,0 +1,4 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=https://photos.google.com/ +HostUrl=https://photos.fife.usercontent.google.com/pw/AP1GczNdml1rYSJs7I5ePvPf3_soG-_yw-1atLrOcG8NfM-14XQY_JKRHXBVGA=w849-h839-s-no?authuser=0 diff --git a/docs/building/_static/pin_alignment.jpg:Zone.Identifier b/docs/building/_static/pin_alignment.jpg:Zone.Identifier new file mode 100644 index 0000000..963017c --- /dev/null +++ b/docs/building/_static/pin_alignment.jpg:Zone.Identifier @@ -0,0 +1,4 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=https://photos.google.com/ +HostUrl=https://photos.fife.usercontent.google.com/pw/AP1GczOSgRCBeDBH3jrVPtdYDXOI-K-oUlXqCQRL40T92fFPf5QpQnQQbPcCuA=w629-h839-s-no?authuser=0 diff --git a/docs/building/_static/post_wings_alignment.jpg:Zone.Identifier b/docs/building/_static/post_wings_alignment.jpg:Zone.Identifier new file mode 100644 index 0000000..a61448e --- /dev/null +++ b/docs/building/_static/post_wings_alignment.jpg:Zone.Identifier @@ -0,0 +1,4 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=https://photos.google.com/ +HostUrl=https://photos.fife.usercontent.google.com/pw/AP1GczM2ekpx6LZXizZWJMpK-hmlVSloet7gAPBww-Umlxy0LoZTn6Q0dlf4rA=w1118-h839-s-no?authuser=0 diff --git a/septa_test_experiment.ipynb b/septa_test_experiment.ipynb new file mode 100644 index 0000000..4f5390b --- /dev/null +++ b/septa_test_experiment.ipynb @@ -0,0 +1,319 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "b656762c-1ca9-4f9c-9ba6-86a3f7190265", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "1127740c-7620-447b-8204-741dfc8fe3cc", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "# ----------- Science Jubilee -------------\n", + "from science_jubilee import Machine as Jub\n", + "from science_jubilee.tools import HTTPSyringe as syringe\n", + "from science_jubilee.tools import Pipette\n", + "import time\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8228ca74-92e4-4761-b4ef-ef0c50fe7381", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee = Jub.Machine(address='192.168.1.2', simulated = False) " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c3d3bf61-1aed-476b-b36c-e2c5891f4986", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.home_all()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "30f0c9b9-98da-46f9-945c-34607d3abaf5", + "metadata": {}, + "outputs": [], + "source": [ + "deck = jubilee.load_deck('lab_automation_deck.json')" + ] + }, + { + "cell_type": "markdown", + "id": "9b3f6a03-ceb8-4e92-8018-5f451dec546d", + "metadata": {}, + "source": [ + "# load a well plate " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "76695d63-a431-4f0c-b3ef-4f7d7a4dd976", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New manual offset applied to falcon_48_wellplate_1500ul\n" + ] + } + ], + "source": [ + "samples = jubilee.load_labware('falcon_48_wellplate_1500ul.json', 0)\n", + "samples.manual_offset([(21.4, 82.9), (134.1, 83.2), (133.9, 12.8)])\n", + "\n", + "\n", + "# -------------- Labware ------------------\n", + "tiprack = jubilee.load_labware('opentrons_96_tiprack_300ul.json', 2)\n", + "#tiprack.load_manualOffset()\n", + "stocks = jubilee.load_labware('20mlscintillation_12_wellplate_18000ul.json', 3)\n", + "trash = jubilee.load_labware('agilent_1_reservoir_290ml.json', 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "fdab5a24-cc2a-4ea2-a545-c841c5ef2334", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New manual offset applied to opentrons_96_tiprack_300ul\n" + ] + } + ], + "source": [ + "UL = (28.4, 175.5)\n", + "UR = (126.8, 175.0)\n", + "BR = (127.3, 112.5)\n", + "tiprack.manual_offset((UL, UR, BR))" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8378fc8c-b57c-4f31-8acd-f681403444cc", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_10 = syringe.HTTPSyringe.from_config(3, \"src/science_jubilee/tools/configs/10cc_syringe.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "803089bb-35b7-4590-925c-220f86a98827", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_1 = syringe.HTTPSyringe.from_config(2, \"src/science_jubilee/tools/configs/1cc_syringe.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cdf950ad-b31f-4af7-883f-28ed9be7ebe0", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.load_tool(syringe_10)\n", + "jubilee.load_tool(syringe_1)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "6437e211-ddd5-4fba-a1f4-804a5ec805d9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded syringe, remaining volume 560 uL\n" + ] + } + ], + "source": [ + "syringe_1.set_pulsewidth(1500)\n", + "syringe_1.load_syringe(560, 1500)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "bd28c5e6-215e-477c-b838-4f4b476e2365", + "metadata": {}, + "outputs": [], + "source": [ + "# load pipette\n", + "P300 = Pipette.Pipette.from_config(1, 'Pipette', 'P300_config.json')\n", + "jubilee.load_tool(P300)\n", + "\n", + "P300.add_tiprack(tiprack)\n", + "P300.trash = trash[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dcdc7846-488d-4f2f-8de5-97b593cffa77", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "618cbed0-c674-4f4e-9148-8d0498db7621", + "metadata": {}, + "source": [ + "## Check P300 dispense" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "b260a734-67b5-441c-a882-1fb2190bcaa1", + "metadata": {}, + "outputs": [], + "source": [ + "vol = 100" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "a886fcf4-a145-4daa-b152-241fbedc68b8", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.pickup_tool(P300)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "d22ab2f4-873c-47ea-a794-fd258a40f6d7", + "metadata": {}, + "outputs": [], + "source": [ + "drop_location = samples[8].bottom(+20)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "89dfa336-1349-43d3-b018-e6feda31a836", + "metadata": {}, + "outputs": [], + "source": [ + "P300.transfer(vol, stocks[0], drop_location, new_tip = 'always')" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "eef9bb33-382c-414c-8fe6-b208f6b45ef3", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.park_tool()" + ] + }, + { + "cell_type": "markdown", + "id": "a9eb6f32-9d18-43c1-b888-ddde0cf83608", + "metadata": {}, + "source": [ + "## Check syringe dispense " + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "aef8ca71-9405-4fdb-9471-289ed4347f67", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.pickup_tool(syringe_1)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "4febab10-2592-4ffa-8435-79944923c1f8", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_1.dispense(vol, drop_location)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "00aa6177-52a0-483c-9b7f-156044d59a4e", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.move(dz = 50)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d060c6b6-c93e-42ac-b4d3-9270b8b224c1", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.pickup(P300)\n", + "P300.pickuptip\n", + "for i in range(5)" + ] + } + ], + "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.9.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/silica_nps_trial.ipynb b/silica_nps_trial.ipynb new file mode 100644 index 0000000..6adb2b1 --- /dev/null +++ b/silica_nps_trial.ipynb @@ -0,0 +1,801 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "110d0499-187d-47cb-b3bd-52e828f810d0", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "023a9737-b2dd-4151-af0b-67ea2aeb7386", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a8305f4f-98d9-46aa-b3a5-aef3875465c8", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "# ----------- Science Jubilee -------------\n", + "from science_jubilee import Machine as Jub\n", + "from science_jubilee.tools import HTTPSyringe as syringe\n", + "from science_jubilee.tools import Pipette\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "14a2a2fa-72ba-40b2-b36a-aee72d96c4b8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "b'{\"remaining_volume\":null,\"syringe_loaded\":false}\\n'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "url = 'http://192.168.1.5:5000'\n", + "volume = {'volume':5}\n", + "r = requests.post(url + '/get_status', json = {'name':'1cc_2'})\n", + "r.content" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "bb05bc51-e88d-493a-b7ae-ec244bab05d1", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee = Jub.Machine(address='192.168.1.2', simulated = False) " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8c90cdc0-c96e-48c3-972a-03b11576bdc1", + "metadata": {}, + "outputs": [], + "source": [ + "#jubilee.home_all()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7dd1d29b-638a-4d85-9cf3-986931deae0f", + "metadata": {}, + "outputs": [], + "source": [ + "deck = jubilee.load_deck('lab_automation_deck_AFL_bolton.json')" + ] + }, + { + "cell_type": "markdown", + "id": "43d1f97d-4c55-40d3-ac5b-9c3a3f1062ed", + "metadata": {}, + "source": [ + "## Load labware\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "19b4bfa8-6878-4b0b-a564-369faa361b1c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New manual offset applied to septavialrev1_44_holder_2000ul\n" + ] + } + ], + "source": [ + "samples = jubilee.load_labware('septavialrev1_44_holder_2000ul.json', 2)\n", + "samples.manual_offset([(19.8,178.1),(132.5,177.6),(132.5,106.6)])" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "19ad67cc-b0a8-4c1f-8d6a-60b173599c82", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New manual offset applied to 20mlscintillation_12_wellplate_18000ul\n" + ] + } + ], + "source": [ + "stocks = jubilee.load_labware('20mlscintillation_12_wellplate_18000ul.json', 4)\n", + "stocks.manual_offset([(31.3,268.1),(117.6,267),(117.9,210.9)])" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "f539dbaa-8bb3-4945-96a5-8e82ee77a839", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New manual offset applied to opentrons_96_tiprack_300ul\n" + ] + } + ], + "source": [ + "tiprack = jubilee.load_labware('opentrons_96_tiprack_300ul.json', 0)\n", + "tiprack.manual_offset([(28,77.3),(127.1,76.8),(127.8,14.5)])" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "84418e76-bfeb-4dc0-9759-700653860f74", + "metadata": {}, + "outputs": [], + "source": [ + "trash = jubilee.load_labware('agilent_1_reservoir_290ml.json', 1)" + ] + }, + { + "cell_type": "markdown", + "id": "b8f140ff-c5f8-494d-bd58-093982edd086", + "metadata": {}, + "source": [ + "## Load Tools" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "c50a8f1e-c1d9-4bf6-a714-8c5eda5fde45", + "metadata": {}, + "outputs": [], + "source": [ + "P300 = Pipette.Pipette.from_config(0, 'Pipette', 'P300_config.json')\n", + "jubilee.load_tool(P300)\n", + "P300.add_tiprack(tiprack)\n", + "P300.trash = trash[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "d73bac24-917c-4c8c-8c30-494146511312", + "metadata": {}, + "outputs": [], + "source": [ + "syringe_10 = syringe.HTTPSyringe.from_config(1, \"src/science_jubilee/tools/configs/10cc_syringe.json\")\n", + "syringe_1_1 = syringe.HTTPSyringe.from_config(2, \"src/science_jubilee/tools/configs/1cc_1_syringe.json\")\n", + "syringe_1_2 = syringe.HTTPSyringe.from_config(3, \"src/science_jubilee/tools/configs/1cc_2_syringe.json\")\n", + "syringe_1_3 = syringe.HTTPSyringe.from_config(4, \"src/science_jubilee/tools/configs/1cc_3_syringe.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "9448524a-f60e-43fd-ae53-0a51c87e6035", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.load_tool(syringe_10)\n", + "jubilee.load_tool(syringe_1_1)\n", + "jubilee.load_tool(syringe_1_2)\n", + "jubilee.load_tool(syringe_1_3)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "21127bbe-632c-4ec0-84f9-57d9772a90db", + "metadata": {}, + "outputs": [], + "source": [ + "mix_syringe = syringe_10\n", + "water_syringe = syringe_1_1\n", + "ammonia_syringe = syringe_1_2\n", + "teos_syringe = syringe_1_3" + ] + }, + { + "cell_type": "markdown", + "id": "0aca8e4d-66e2-4757-aada-8975aa30893e", + "metadata": {}, + "source": [ + "## Manual load solution" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "fb5924f6-6d8f-4c23-87c0-57e2272fbaf4", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.pickup_tool(water_syringe)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "32066efa-33dd-4e38-be7c-f768e8d8422a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded syringe, remaining volume 0 uL\n" + ] + } + ], + "source": [ + "teos_syringe.set_pulsewidth(1844)\n", + "teos_syringe.load_syringe(0, 1844)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "119e829e-e1ba-4d9e-8fb3-bacc1baa347f", + "metadata": {}, + "outputs": [], + "source": [ + "#water_syringe.set_pulsewidth(water_syringe.empty_position-1)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "6e2522d1-f31a-4bc1-8f73-52ad91b59bf4", + "metadata": {}, + "outputs": [], + "source": [ + "aspirate = samples[0].bottom(+200)\n", + "aspirate_vol = 995 #- syringe_1_1.remaining_volume\n", + "#aspirate_vol = 9995\n", + "teos_syringe.aspirate(aspirate_vol, aspirate)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "3d2e8779-8b93-4314-9357-613142bbe9bf", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "jubilee.park_tool()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "775f7391-b4d8-490f-8e14-ac2df6fd6f0a", + "metadata": {}, + "outputs": [], + "source": [ + "# deck is at lowest point now" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de4b3043-819d-462d-84d3-4dd2c220bbf1", + "metadata": {}, + "outputs": [], + "source": [ + "# repeat for other solutions" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "3d137f98-3067-4b3d-8067-f9e32a797065", + "metadata": {}, + "outputs": [], + "source": [ + "#jubilee.move(dz = 50)" + ] + }, + { + "cell_type": "markdown", + "id": "b9dff3f4-8be6-4f73-b343-6221f8df76dc", + "metadata": {}, + "source": [ + "## Experiment" + ] + }, + { + "cell_type": "markdown", + "id": "fee5c3c1-2208-491d-991f-9076742143e4", + "metadata": {}, + "source": [ + "##### EtOH: 1851.8 ul\n", + "##### NH3OH: 67.6 ul\n", + "##### H20: 36 ul\n", + "##### TEOS: 44.6 ul" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "88802e66-f526-45a6-8e95-1c8277d768e5", + "metadata": {}, + "outputs": [], + "source": [ + "etoh_dispense_vol_1 = 300\n", + "etoh__dispense_vol_2 = 175.9\n", + "ammonia_dispense_vol = 67.6\n", + "water_dispense_vol = 36\n", + "teos_dispense_vol = 44.6\n", + "mix_vol = 2000" + ] + }, + { + "cell_type": "markdown", + "id": "5230295b-a88c-48e5-84d8-b1e43692146d", + "metadata": {}, + "source": [ + "### Add EtOH" + ] + }, + { + "cell_type": "code", + "execution_count": 116, + "id": "b658cbe8-d213-4ece-8f85-9a8c1399b0f0", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.pickup_tool(P300)" + ] + }, + { + "cell_type": "code", + "execution_count": 117, + "id": "e610289b-b25b-4192-822a-04470c552c4d", + "metadata": {}, + "outputs": [], + "source": [ + "P300.pickup_tip()" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "a6105e35-b5dc-4abd-954d-f20b51255349", + "metadata": {}, + "outputs": [], + "source": [ + "#jubilee.move_to(x = samples[0].x, y = samples[0].y)" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "id": "8eae0f56-535b-4fa4-ae23-ebe069c105db", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.move(dz = 60)" + ] + }, + { + "cell_type": "code", + "execution_count": 118, + "id": "bb3e9db1-db05-4282-a6ef-63f5e88e3e9a", + "metadata": {}, + "outputs": [], + "source": [ + "etoh_vol_1 = 300\n", + "etoh_vol_2 = 175.9\n", + "for i in range(2):\n", + " for k in range(5):\n", + " P300.aspirate(etoh_dispense_vol_1, stocks[0])\n", + " jubilee.move(dz=60)\n", + " P300.dispense(etoh_dispense_vol_1, samples[i].bottom(+15))\n", + " jubilee.move(dz=60)\n", + " time.sleep(1)\n", + " for j in range (2):\n", + " P300.aspirate(etoh_dispense_vol_2, stocks[0])\n", + " jubilee.move(dz=60)\n", + " P300.dispense(etoh_dispense_vol_2, samples[i].bottom(+15))\n", + " jubilee.move(dz=60)\n", + " time.sleep(1)" + ] + }, + { + "cell_type": "code", + "execution_count": 119, + "id": "8f624c82-4a5f-4e1a-b561-1a2665e13596", + "metadata": {}, + "outputs": [], + "source": [ + "P300.drop_tip()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 120, + "id": "8ddedc6d-ea60-47a1-825f-7b0bd4aaa6da", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.park_tool()" + ] + }, + { + "cell_type": "markdown", + "id": "e456176e-5b2f-4422-ad2d-4ea12e268f4d", + "metadata": {}, + "source": [ + "### Add NH3OH (syringe_1_2)" + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "id": "c4b6ee77-d604-4bb4-afe4-009df3aeb266", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.pickup_tool(ammonia_syringe)" + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "id": "57251bd9-ecd6-4061-b298-bb8b7a93a276", + "metadata": {}, + "outputs": [], + "source": [ + "#jubilee.move_to(x = samples[0].x, y = samples[0].y)" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "id": "a8aaad9d-e627-4415-90c4-1ee9c9b42de1", + "metadata": {}, + "outputs": [], + "source": [ + "ammonia_dispense_vol = 67.6\n", + "\n", + "\n", + "for i in range(2):\n", + " dispense_location = samples[i].bottom(+25)\n", + " ammonia_syringe.dispense(ammonia_dispense_vol, dispense_location)\n", + " time.sleep(3)\n", + " #jubilee.move(dz = 20)" + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "id": "f445076a-117a-4cd4-bc8e-794cae0aefc7", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.park_tool()" + ] + }, + { + "cell_type": "markdown", + "id": "385691e1-b9e1-442a-ab9a-5bd20d7b2c32", + "metadata": {}, + "source": [ + "### Add water (syringe_1_1)" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "id": "31c7ce21-2c1e-4296-bd8c-c9e4f2893ac6", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.pickup_tool(water_syringe)" + ] + }, + { + "cell_type": "code", + "execution_count": 114, + "id": "945a4ca2-8ea2-4874-a006-70a6d610136a", + "metadata": {}, + "outputs": [], + "source": [ + "water_dispense_vol = 36\n", + "\n", + "\n", + "for i in range(2):\n", + " dispense_location = samples[i].bottom(+25)\n", + " water_syringe.dispense(water_dispense_vol, dispense_location)\n", + " time.sleep(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 115, + "id": "5c512666-472c-4a3e-968d-bc67e1650c8b", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.park_tool()" + ] + }, + { + "cell_type": "markdown", + "id": "6bb658fb-cce8-441e-b79e-253450cacf02", + "metadata": {}, + "source": [ + "### Mix (syringe_10)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "3344fc30-f119-4b6a-8404-cd9b32332842", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.pickup_tool(mix_syringe)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "b21a4f9f-d424-406f-8bb1-a2fc1f0c5813", + "metadata": {}, + "outputs": [], + "source": [ + "mix_syringe.aspirate(5, samples[0].bottom(+5))" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "9c9b82cd-f4de-4795-8b38-dec7f0ce15a6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n" + ] + } + ], + "source": [ + "for i in range(2):\n", + " mix_loc = samples[i].bottom(+5)\n", + " mix_syringe.mix(mix_vol, 5, mix_loc, t_hold = 3)\n", + "\n", + " wash_loc_1 = stocks[1]\n", + " mix_syringe.aspirate(5, wash_loc_1)\n", + " for k in range(2):\n", + " wash_loc = stocks[k+1]\n", + " mix_syringe.mix(mix_vol, 3, wash_loc, t_hold = 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "ce39c3ad-1c05-47b0-985a-66cef5e6ebec", + "metadata": {}, + "outputs": [], + "source": [ + "jubilee.park_tool()" + ] + }, + { + "cell_type": "markdown", + "id": "1b0466c1-60e7-4806-8352-c69aec650a40", + "metadata": {}, + "source": [ + "### Add TEOS and mix" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "c49ccd69-382f-431c-951d-a328d67bf31b", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "77c368c1-7a55-4ed5-9794-8ee4dcc7bfcd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n", + "aspirate\n", + "dispense\n" + ] + } + ], + "source": [ + "teos_dispense_vol = 44.6\n", + "\n", + "\n", + "for i in range(2):\n", + " jubilee.pickup_tool(teos_syringe)\n", + " dispense_location = samples[i].bottom(+25)\n", + " mix_location = samples[i].bottom(+5)\n", + " teos_syringe.dispense(teos_dispense_vol, dispense_location)\n", + " time.sleep(3)\n", + " jubilee.park_tool()\n", + " time.sleep(1)\n", + " \n", + " jubilee.pickup_tool(mix_syringe)\n", + " \n", + " mix_loc = samples[i].bottom(+5)\n", + " mix_syringe.aspirate(5, mix_loc)\n", + " mix_syringe.mix(mix_vol, 5, mix_loc, t_hold = 3)\n", + "\n", + " wash_loc_1 = stocks[1]\n", + " mix_syringe.aspirate(5, wash_loc_1)\n", + " for k in range(2):\n", + " wash_loc = stocks[k+1]\n", + " mix_syringe.mix(mix_vol, 3, wash_loc, t_hold = 3)\n", + "\n", + " jubilee.park_tool()\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "baa8c9c9-7643-47a0-955e-b24bee542bf7", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "604eb7ac-8826-454e-bbe8-66bc42e5c432", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.9.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/science_jubilee/Machine.py b/src/science_jubilee/Machine.py index c9cfe08..0574eaf 100644 --- a/src/science_jubilee/Machine.py +++ b/src/science_jubilee/Machine.py @@ -3,6 +3,7 @@ # import websocket # for reading the machine model import json +import logging import os import time import warnings @@ -175,6 +176,7 @@ def __init__( ) requests_session.mount("http://", HTTPAdapter(max_retries=retries)) + requests_session.headers["Connection"] = "close" self.session = requests_session if deck_config is not None: @@ -442,7 +444,7 @@ def load_deck( self.deck = deck return deck - def gcode(self, cmd: str = "", timeout=None, response_wait: float = 30): + def gcode(self, cmd: str = "", timeout=None, response_wait: float = 60): """Send a G-Code command to the Machine and return the response. :param cmd: The G-Code command to send, defaults to "" @@ -474,30 +476,54 @@ def gcode(self, cmd: str = "", timeout=None, response_wait: float = 30): # Paraphrased from Duet HTTP-requests page: # Client should query `rr_model?key=seqs` and monitor `seqs.reply`. If incremented, the command went through # and the response is available at `rr_reply`. - reply_count = self.session.get( + reply_response = self.session.get( f"http://{self.address}/rr_model?key=seqs" - ).json()["result"]["reply"] + ) + logging.debug( + f"MODEL response, status: {reply_response.status_code}, headers:{reply_response.headers}, content:{reply_response.content}" + ) + + reply_count = reply_response.json()["result"]["reply"] buffer_response = self.session.get( f"http://{self.address}/rr_gcode?gcode={cmd}", timeout=timeout ) + logging.debug( + f"GCODE response, status: {buffer_response.status_code}, headers:{buffer_response.headers}, content:{buffer_response.content}" + ) # wait for a response code to be appended # TODO: Implement retry backoff for managing long-running operations to avoid too many requests error. Right now this is handled by the generic exception catch then sleep. Real fix is some sort of backoff for things running longer than a few seconds. tic = time.time() + try_count = 0 while True: try: - new_reply_count = self.session.get( + new_reply_response = self.session.get( f"http://{self.address}/rr_model?key=seqs" - ).json()["result"]["reply"] + ) + + logging.debug( + f"MODEL response, status: {new_reply_response.status_code}, headers:{new_reply_response.headers}, content:{new_reply_response.content}" + ) + new_reply_count = new_reply_response.json()["result"]["reply"] + if new_reply_count != reply_count: response = self.session.get( f"http://{self.address}/rr_reply" - ).text + ) + + logging.debug( + f"REPLY response, status: {response.status_code}, headers:{response.headers}, content:{response.content}" + ) + + response = response.text break elif time.time() - tic > response_wait: response = None break + time.sleep(self.delay_time(try_count)) + try_count += 1 except Exception as e: - print("Connection error, sleeping 1 second") + print(f"Connection error ({e}), sleeping 1 second") + logging.debug(f"Error in gcode reply wait loop: {e}") time.sleep(2) continue @@ -507,6 +533,21 @@ def gcode(self, cmd: str = "", timeout=None, response_wait: float = 30): # TODO: handle this with logging. Also fix so all output goes to logs return response + def delay_time(self, n): + """ + Calculate delay time for next request. dumb hard code for now, could be fancy exponential backoff + """ + if n == 0: + return 0 + if n < 10: + return 0.1 + if n < 20: + return 0.2 + if n < 30: + return 0.3 + else: + return 1 + def _set_absolute_positioning(self): """Set absolute positioning for all axes except extrusion""" self.gcode("G90") diff --git a/src/science_jubilee/calibration/.ipynb_checkpoints/CalibrationControlPanel-checkpoint.py b/src/science_jubilee/calibration/.ipynb_checkpoints/CalibrationControlPanel-checkpoint.py new file mode 100644 index 0000000..6d714fa --- /dev/null +++ b/src/science_jubilee/calibration/.ipynb_checkpoints/CalibrationControlPanel-checkpoint.py @@ -0,0 +1,70 @@ +import functools + +import ipywidgets as widgets + + +# This will make a little control panel to help us align things +def make_control_panel(m): + def create_expanded_button(description): + return widgets.Button( + description=description, + button_style="warning", + layout=widgets.Layout(height="auto", width="auto"), + ) + + def button_move(b, dx=0, dy=0): + m.move(dx=dx, dy=dy) + + control_panel = widgets.GridspecLayout(7, 7) + + # y buttons + bp5y = create_expanded_button("+5mm") + bp5y.on_click(functools.partial(button_move, dx=0, dy=5)) + control_panel[0, 3] = bp5y + + bp1y = create_expanded_button("+1mm") + bp1y.on_click(functools.partial(button_move, dx=0, dy=1)) + control_panel[1, 3] = bp1y + + bppt1y = create_expanded_button("+.1mm") + bppt1y.on_click(functools.partial(button_move, dx=0, dy=0.1)) + control_panel[2, 3] = bppt1y + + bmpt1y = create_expanded_button("-.1mm") + bmpt1y.on_click(functools.partial(button_move, dx=0, dy=-0.1)) + control_panel[4, 3] = bmpt1y + + bm1y = create_expanded_button("-1mm") + bm1y.on_click(functools.partial(button_move, dx=0, dy=-1)) + control_panel[5, 3] = bm1y + + bm5y = create_expanded_button("-5mm") + bm5y.on_click(functools.partial(button_move, dx=0, dy=-5)) + control_panel[6, 3] = bm5y + + # x buttons + bp5x = create_expanded_button("+5mm") + bp5x.on_click(functools.partial(button_move, dx=5, dy=0)) + control_panel[3, 6] = bp5x + + bp1x = create_expanded_button("+1mm") + bp1x.on_click(functools.partial(button_move, dx=1, dy=0)) + control_panel[3, 5] = bp1x + + bppt1x = create_expanded_button("+.1mm") + bppt1x.on_click(functools.partial(button_move, dx=0.1, dy=0)) + control_panel[3, 4] = bppt1x + + bmpt1x = create_expanded_button("-.1mm") + bmpt1x.on_click(functools.partial(button_move, dx=-0.1, dy=0)) + control_panel[3, 2] = bmpt1x + + bm1x = create_expanded_button("-1mm") + bm1x.on_click(functools.partial(button_move, dx=-1, dy=0)) + control_panel[3, 1] = bm1x + + bm5x = create_expanded_button("-5mm") + bm5x.on_click(functools.partial(button_move, dx=-5, dy=0)) + control_panel[3, 0] = bm5x + + return control_panel diff --git a/src/science_jubilee/decks/deck_definition/.ipynb_checkpoints/Untitled-checkpoint.ipynb b/src/science_jubilee/decks/deck_definition/.ipynb_checkpoints/Untitled-checkpoint.ipynb new file mode 100644 index 0000000..363fcab --- /dev/null +++ b/src/science_jubilee/decks/deck_definition/.ipynb_checkpoints/Untitled-checkpoint.ipynb @@ -0,0 +1,6 @@ +{ + "cells": [], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/science_jubilee/decks/deck_definition/.ipynb_checkpoints/lab_automation_deck-checkpoint.json b/src/science_jubilee/decks/deck_definition/.ipynb_checkpoints/lab_automation_deck-checkpoint.json new file mode 100644 index 0000000..f32fb8a --- /dev/null +++ b/src/science_jubilee/decks/deck_definition/.ipynb_checkpoints/lab_automation_deck-checkpoint.json @@ -0,0 +1,64 @@ +{ + "bedType": "Lab Automation Plate", + "deckSlots": { + "total": 6, + "type": "SLAS Standard Labware" + }, + "slots": { + "0": { + "offset": [ + 14.9, + 4.4 + ], + "has_labware": false, + "labware": null + }, + "1": { + "offset": [ + 157.0, + 5.8 + ], + "has_labware": false, + "labware": null + }, + "2": { + "offset": [ + 14.3, + 100.8 + ], + "has_labware": false, + "labware": null + }, + "3": { + "offset": [ + 154.6, + 100.1 + ], + "has_labware": false, + "labware": null + }, + "4": { + "offset": [ + 17.1, + 201.1 + ], + "has_labware": false, + "labware": null + }, + "5": { + "offset": [ + 157.0, + 201.9 + ], + "has_labware": false, + "labware": null + } + }, + "offsetFrom": { + "corner": "Bottom Left" + }, + "material": { + "Deck": "Aluminum", + "mask": "Delrin" + } +} diff --git a/src/science_jubilee/decks/deck_definition/.ipynb_checkpoints/lab_automation_deck_AFL_bolton-checkpoint.json b/src/science_jubilee/decks/deck_definition/.ipynb_checkpoints/lab_automation_deck_AFL_bolton-checkpoint.json new file mode 100644 index 0000000..3bccdc8 --- /dev/null +++ b/src/science_jubilee/decks/deck_definition/.ipynb_checkpoints/lab_automation_deck_AFL_bolton-checkpoint.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" + } +} diff --git a/src/science_jubilee/decks/deck_definition/.ipynb_checkpoints/lab_automation_deck_MA-checkpoint.json b/src/science_jubilee/decks/deck_definition/.ipynb_checkpoints/lab_automation_deck_MA-checkpoint.json new file mode 100644 index 0000000..3bccdc8 --- /dev/null +++ b/src/science_jubilee/decks/deck_definition/.ipynb_checkpoints/lab_automation_deck_MA-checkpoint.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" + } +} diff --git a/src/science_jubilee/decks/deck_definition/Untitled.ipynb b/src/science_jubilee/decks/deck_definition/Untitled.ipynb new file mode 100644 index 0000000..363fcab --- /dev/null +++ b/src/science_jubilee/decks/deck_definition/Untitled.ipynb @@ -0,0 +1,6 @@ +{ + "cells": [], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/science_jubilee/decks/deck_definition/lab_automation_deck_AFL_bolton.json b/src/science_jubilee/decks/deck_definition/lab_automation_deck_AFL_bolton.json new file mode 100644 index 0000000..3bccdc8 --- /dev/null +++ b/src/science_jubilee/decks/deck_definition/lab_automation_deck_AFL_bolton.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" + } +} diff --git a/src/science_jubilee/labware/.ipynb_checkpoints/Labware-checkpoint.py b/src/science_jubilee/labware/.ipynb_checkpoints/Labware-checkpoint.py new file mode 100644 index 0000000..55cbfb6 --- /dev/null +++ b/src/science_jubilee/labware/.ipynb_checkpoints/Labware-checkpoint.py @@ -0,0 +1,892 @@ +import json +import os +import string +from dataclasses import dataclass +from itertools import chain +from math import acos, cos, sin, sqrt +from typing import Dict, Iterable, List, NamedTuple, Tuple, Union + +import numpy as np + + +@dataclass +class Well: + """A class representing a well of a labware. + + Each Well is associated with a specific name, depth, total liquid volume, shape, diameter, + x, y, and z dimension, y-dimension, as well as its coordinates and any applied offset + + :return: A :class:`Well` object with various information about the geometry of the well and its position in the labware + :rtype: :class:`Well` + """ + + name: str + depth: float + totalLiquidVolume: float + shape: str + diameter: float = None + xDimension: float = None + yDimension: float = None + x: float + y: float + z: float + offset: Tuple[float] = None + slot: int = None + has_tip: bool = False + clean_tip: bool = False + labware_name: str = None + + @property + def x(self): + """Offsets the x-position of the each well with respect to the deck-slot coordinates + + :return: The x-coordinate of the well + :rtype: float + """ + return self._x + + @x.setter + def x(self, new_x): + """Setter for the offsetted x-position of each well with respect to the deck-slot coordinates + + :param new_x: the new y-coordinate of the well + :type new_x: float + """ + self._x = new_x + + @property + def y(self): + """Offsets the y-position of the each well with respect to the deck-slot coordinates + + :return: The y-coordinate of the well + :rtype: float + """ + return self._y + + @y.setter + def y(self, new_y): + """Setter for the offsetted y-position of each well with respect to the deck-slot coordinates + + :param new_y: The new y-coordinate of the well + :type new_y: float + """ + + self._y = new_y + + @property + def z(self): + """Offsets the z-position of each well with respect to the deck-slot coordinates + + :return: The z-coordinate of the well + :rtype: float + """ + return self._z + + @z.setter + def z(self, new_z): + """Setter for the offsetted z-position of each well with respect to the deck-slot coordinates + + :param new_z: The new z-coordinate of the well + :type new_z: flaot + """ + self._z = new_z + + def apply_offset(self, offset: Tuple[float]): + """Allows the user to offset the coordinates of the well with respect to the deck-slot coordinates + + :param offset: A tuple of floats with the new offset of the well + :type offset: Tuple[float] + """ + self._x = self.x + offset[0] + self._y = self.y + offset[1] + + if len(offset) == 3: + self._z = self.z + offset[2] + + self.offset = offset + + @property + def top_(self): + """Defines the top-most point of the well + + :return: The z-coordinate of the top of the well + :rtype: float + """ + return self.z + self.depth + + @property + def bottom_(self): + """Defines the bottom-most point of the well + + :return: The z-coordinate of the bottom of the well + :rtype: float + """ + return self.z + + def bottom(self, z: float, check=False): + """Allows the user to dinamically indicate a new Z location relative to the + bottom of the well. + + :param z: the distance in mm to offset the coordinates from the bottom of the well. Should be + + :type z: float + :param check: the 'z' parameters can either be + or -. If negative, an assert error is raised to + avoid collision with the labware. However, there might be instances of custom labware where the bottom of the well + is purposely set as higher during the generation of its config .json file., defaults to False + :type check: bool, optional + :return: A :class:`Location` which contains information about the new coordinates generated and the + :class:`Well` object + :rtype: :class:`Location` + """ + from_bottom_z = self.bottom_ + z + if check: + pass + else: + assert z >= 0, ( + "Indicated location is lower than the bottom of the labware and" + " could result in crash. Input a positive 'z' value " + ) + + coord = (self.x, self.y, from_bottom_z) + + return Location(coord, self) + + def top(self, z: float): + """Allows the user to dinamically indicate a new Z location relative to the + top of the well. + + :param z: the distance in mm to offset the coordinates from the top of the well.Can be either + or - + :type z: float + :return: A :class:`Location` which contains information about the new coordinates generated and the + :class:`Well` object. + :rtype: :class:`Location` + """ + from_top_z = self.top_ + z + assert ( + from_top_z > self.bottom_ + ), "Indicated location is lower than the bottom of the labware." + coord = (self.x, self.y, from_top_z) + + return Location(coord, self) + + def __repr__(self): + """Displayed representation of a :class:`Well` object indicating its name and its coordinates + + :return: A string representation of the name and coordinates of a well + :rtype: str + """ + if self.slot != None: + message = f"Well {self.name} form {self.labware_name} on slot {self.slot}" + else: + message = f"Well {self.name} at coordinates {self.x, self.y, self.z}" + return message + + def set_has_tip(self, value: bool): + """Set the value of the `has_tip` attribute. + + :param value: The new value for the `has_tip` attribute + :type value: bool + """ + self.has_tip = value + + def set_clean_tip(self, value: bool): + """Returns the value of the `clean_tip` attribute. + + :param value: The new value for the `clean_tip` attribute + :type value: bool + """ + self.clean_tip = value + + +@dataclass(repr=False) +class WellSet: + """A class defining a set of wells expressed as a dictionary in which each keys is the + the :attribute:`Well.name` object and the value is the :class:`Well` object itself. + """ + + wells: Dict[str, Well] + + def __repr__(self): + """Displays the wellset as a :list: of wells and the deck-slot nunmber + + :return: A :list: of :class:`Well` objects diplayed by their :attribute:`Well.name` + :rtype: :class:`Row` + """ + return str(f"{list(self.wells.keys())}") + + def __getitem__(self, id_: Union[str, int]): + """Allows the user to select a :class:`Well` object by either their :attribute:`Well.name` or + their index in a :list: + + :param id_: The :attribute:`Well.name` or index representing a :class:`Well` in the labware + :type id_: Union[str, int] + :return: The :class:`Well` object + :rtype: :class:`Well` + """ + try: + if isinstance(id_, slice): + well_list = [] + start = id_.start + stop = id_.stop + if id_.step is not None: + step = id_.step + else: + step = 1 + for sub_id in range(start, stop, step): + well_list.append(self.wells[sub_id]) + return well_list + else: + return self.wells[id_] + except KeyError: + return list(self.wells.values())[id_] + + +@dataclass(repr=False) +class Row(WellSet): + """A class representing a row of a labware, for example 'A', 'B', etc + + :param WellSet: A dictionary of :class:`Well` objects in which each keys is the the :attribute:`Well.name` object and the value is the :class:`Well` object itself. + :type WellSet: Dict[str, Well] + """ + + identifier: str + + +@dataclass(repr=False) +class Column(WellSet): + """A class representing a column of a labware, for example 1, 2, etc. + + :param WellSet: A dictionary of :class:`Well` objects in which each keys is the the :attribute:`Well.name` object and the value is the :class:`Well` object itself. + :type WellSet: Dict[str, Well] + """ + + identifier: int + + +class Labware(WellSet): + """A class representing a basic laboratory labware made up of a set of wells/pipette tips. + + :param labware_filename: The name of the config `.json` + :type labware_filename: str + :param offset: Coordinates to use to offset all the wells in a labware for easier handling of coordinates. + For example this is called by the :method:`Deck.load_labware` when assignign a labware to a deck slot, defaults to None + :type offset: Tuple[float], optional + :param order: Option to order the wells of a labware either by `row` or `columns`, defaults to 'rows' + :type order: str, optional + :param path: Path to the folder containing the configuration `.json` files for the labware, + defaults to the 'labware_definition/' in the science_jubilee/labware directory. + :type path: str, optional + """ + + def __init__( + self, + labware_filename: str, + offset: Tuple[float] = None, + order: str = "rows", + path: str = os.path.join(os.path.dirname(__file__), "labware_definition"), + ): + """Initializes a :class:`Labware` object by loading its configuration file and creating a dictionary of :class:`Well` objects. + + :param labware_filename: The name of the config `.json` + :type labware_filename: str + :param offset: Coordinates to use to offset all the wells in a labware for easier handling of coordinates. + For example this is called by the :method:`Deck.load_labware` when assignign a labware to a deck slot, defaults to None + :type offset: Tuple[float], optional + :param order: Option to order the wells of a labware either by `row` or `columns`, defaults to 'rows' + :type order: str, optional + :param path: Path to the folder containing the configuration `.json` files for the labware, + defaults to the 'labware_definition/' in the science_jubilee/labware directory. + :type path: str, optional + """ + # load in the labware configuration file + if labware_filename[-4:] != "json": + labware_filename = labware_filename + ".json" + + config_path = os.path.join(path, f"{labware_filename}") + + with open(config_path, "r") as f: + # this will be the raw .json file data and fields should not be modified directly + # current exceptions is 'manual_offset' field to allow to save custom data for easier handling + # of recurrent slot-labware combinations + self.data = json.load(f) + + self.config_path = config_path + self.wells_data = self.data.get("wells", {}) + self.row_data, self.column_data, self.wells = self._create_rows_and_columns() + + order_options = [ + "rows", + "row", + "Rows", + "Row", + "R", + "cols", + "col", + "C", + "columns", + "Columns", + ] + assert order in order_options, "Order must be one of {}".format(order_options) + self.withWellOrder(order) + self.offset = offset + self.slot = None + + # check to see if a manual offset was saved for this labware in a specific slot + if "manual_offset" in self.data: + self.manualOffset = self.data["manual_offset"] + else: + # otherwise initialize manual_offset instance variable + self.manualOffset = {} + + def __repr__(self): + """Displayed representation of a :class:`Labware` object indicating the type of labware and + its name. Additionally, it will show the :attribute:`Deck.slots` number if the labware has been + already assigned to it. + """ + display = ( + self.metadata()["displayCategory"] + ": " + self.parameters()["loadName"] + ) + if self.slot is not None: + display = display + " " + f" on {self.slot}" + return display + + def _create_rows_and_columns(self): + """Creates a dictionary of :class:`Row` and :class:`Column` and :class:`Well` objects from the data in the config `.json` file. + + :return: A dictionary of :class:`Row` and :class:`Column` and :class:`Well` objects + :rtype: :class:`Row`, :class:`Column`, :class:`Well` + """ + rows = {} + columns = {} + wells = {} + + for row_order, column_data in enumerate(self.ordering): + # Assumes the first char is the row identifier, e.g., "A" in "A1" + row_id = column_data[0][0] + # Extracts column number, e.g., "1" in "A1" + col_ids = [int(well[1:]) for well in column_data] + + if row_id not in rows: + rows[row_id] = {} + + for col_order, well_id in enumerate(column_data): + well = Well(name=well_id, **self.wells_data[well_id]) + rows[row_id][well_id] = well + + if col_order + 1 not in columns: # +1 since indexing starts at 0 + columns[col_order + 1] = {} + + columns[col_order + 1][well_id] = well + wells[well_id] = well + + # add tip tracking to the wells + if self.is_tip_rack: + for well in wells.values(): + well.has_tip = True + well.clean_tip = True + + # add labware name to each Well object + for well in wells.values(): + well.labware_name = self.display_name + + # Convert dictionary data to Row and Column classes + _rows = {k: Row(identifier=k, wells=v) for k, v in rows.items()} + _columns = {k: Column(identifier=k, wells=v) for k, v in columns.items()} + + return _rows, _columns, wells + + def get_row(self, row_id: str) -> Row: + """Fucntions to fetch the :class:`Well.name` of the indicated row. + + :param row_id: The name of a row of the labware, usually indicated by a capital letter (e.g., A, B, etc.) + :type row_id: str + :return: A list of :class:`Well` objects diplayed by their :attribute:`Well.name` + :rtype: :class:`Row` + """ + return self.row_data.get(row_id) + + def get_column(self, col_id: int) -> Column: + """Fucntions to fetch the :class:`Well.name` of the indicated column. + + :param col_id: The name of a column of the labware, usually indicated by an integer number (e.g., 1, 2, etc.) + :type col_id: str + :return: A list of :class:`Well` objects diplayed by their :attribute:`Well.name` + :rtype: :class:`Column` + """ + return self.column_data.get(col_id) + + @property + def shape(self): + """Returns the shape of the labware as a tuple of (rows, columns) + + :return: A tuple of (rows, columns) + :rtype: Tuple[int, int] + """ + return (len(self.row_data), len(self.column_data)) + + @property + def ordering(self) -> List[List[str]]: + """Returns the ordering of the wells in the labware as a list of lists. Each list represents a row of the labware. + + :return: A list of lists of :class:`Well.name` objects + :rtype: List[List[str]] + """ + + return np.array(self.data["ordering"]).T + + @property + def brand(self) -> dict: + """Returns the brand of the labware as a strin + + :return: A string with the brand of the labware + :rtype: str + """ + return self.data.get("brand", {})["brand"] + + def metadata(self) -> dict: + """Returns the metadata of the labware as a dictionary + + The metadata of a labware will generally contain the display name, the type of labware, and the units of volume. + These can also be found as attributes of the :class:`Labware` object. + + :return: A dictionary with the metadata of the labware + :rtype: dict + """ + return self.data.get("metadata", {}) + + @property + def display_name(self): + """Returns the display name of the labware as a string + + :return: A string with the display name of the labware + :rtype: str + """ + return self.metadata()["displayName"] + + @property + def labware_type(self): + """Returns the type of labware as a string + + The type fo labware will generally either be a tiprack, wellplate, reservoir, etc. + + :return: A string with the type of labware + :rtype: str + """ + return self.metadata()["displayCategory"] + + @property + def volume_units(self): + """Returns the units of volume of the labware as a string + + The volume units will be either uL or mL. + + :return: A string with the units of volume of the labware + :rtype: str + """ + return self.metadata()["displayVolumeUnits"] + + @property + def dimensions(self) -> dict: + """Returns the dimensions of the labware as a dictionary + + :return: A dictionary with the x,y, and z dimensions of the labware + :rtype: dict + """ + return self.data.get("dimensions", {}) + + def parameters(self) -> dict: + """Returns the parameters describing certain features of the labware as a dictionary + + The parameters genereally include whether the shape of the labware is regular or irregular, if it is a tiprack, + and other Opentrons specific parameters as we are using their 'Custom Labware Page' to generate the .json config files. + + :return: A dictionary with the parameters of the labware + :rtype: dict + """ + return self.data.get("parameters", {}) + + @property + def is_tip_rack(self): + """Returns a boolean indicating if the labware is a tiprack + + :return: True if the labware is a tiprack, False otherwise + :rtype: bool + """ + return self.parameters()["isTiprack"] + + @property + def load_name(self): + """Returns the name of the labware as a string + + :return: A string with the name of the labware + :rtype: str + """ + return self.parameters()["loadName"] + + @property + def tip_length(self): + """Returns the length of the tip of the labware as a float if the labware is a tiprack, otherwise returns None + + :return: A float with the length of the tip of the labware or None otherwise + :rtype: float + """ + try: + return self.parameters()["tipLength"] + except: + pass + + @property + def tip_overlap(self): + """Returns the overlap of the tip of the labware as a float if the labware is a tiprack, otherwise returns None + + :return: A float with the overlap of the tip of the labware or None otherwise + :rtype: float + """ + try: + return self.parameters()["tipOverlap"] + except: + pass + + @property + def offset(self): + """Returns the offset of the labware as a tuple of floats + + :return: A tuple of floats with the offset of the labware + :rtype: Tuple[float] + """ + return self._offset + + @offset.setter + def offset(self, new_offset): + """Sets the offset of the labware to the indicated values and updates the offset of each well in the labware + + :param new_offset: A tuple of floats with the new offset of the labware + :type new_offset: Tuple[float] + """ + self._offset = new_offset + if new_offset is not None: + for w in self: + w.apply_offset(new_offset) + + def add_slot(self, slot_): + """Add name of deck slot after labware has been loaded + + :param slot_: The name of the deck slot + :type slot_: str + """ + self.slot = slot_ + for w in self: + w.slot = slot_ + + def withWellOrder(self, order) -> list: + """Reorders the wells by rows or by columns. Automatically updates the :attribute:`Labware.wells` + + :param order: The order in which to reorder the wells. Can be either 'rows' or 'columns' + :type order: str + :return: A list of :class:`Well` objects diplayed by their :attribute:`Well.name` + :rtype: list + """ + ordered_wells = {} + if order in ["rows", "row", "Rows", "Row", "R"]: + for well in list(chain(*self.row_data.values())): + ordered_wells[well.name] = well + elif order in ["cols", "col", "C", "columns", "Columns"]: + for well in list(chain(*self.column_data.values())): + ordered_wells[well.name] = well + else: + print("Order needs to be either rows or columns") + + self.wells = ordered_wells + + # @staticmethod + def _translate_point( + self, + well: Well, + theta: float, + x_space: float, + y_space: float, + upper_left: Tuple[float], + ): + """ + Helper function to translate the coordinates of a well by a given angle theta. + + :param well: A :class:`Well` object + :type well: :class:`Well` + :param theta: The angle by which to translate the coordinates of the well + :type theta: float + + :return: The new x and y coordinates of the well + :rtype: float, float + """ + x_nom, y_nom = self._nominal_coordinates(well, x_space, y_space) + + x_translated = upper_left[0] + x_nom * cos(theta) - y_nom * sin(theta) + y_translated = upper_left[1] - (x_nom * sin(theta) + y_nom * cos(theta)) + + return x_translated, y_translated + + @staticmethod + def _nominal_coordinates(well: Well, x_space: float, y_space: float): + """ + Helper function to calculate the nominal coordinates of a well in a labware + based on its row and column index. + """ + col_index = int(well.name[1:]) - 1 + row_index = list(string.ascii_uppercase).index(well.name[0]) + + x_nominal = col_index * x_space + y_nominal = row_index * y_space + + return x_nominal, y_nominal + + def manual_offset(self, corner_wells: List[Tuple[float]], save: bool = False): + """Allows the user to manually offset the coordinates of the labware based on three corner wells. + + Adapted from `https://github.com/machineagency/sonication_station` labware calibration procedure. + + :param offset: A list containing tuples of floats + :type offset: Tuple[float] + :param save: Option to save the manual offset to the original config `.json` file, defaults to False + :type save: bool, optional + + :return: An updated :class:`Labware` object with the new coordinates of the wells + :rtype: :class:`Labware` + """ + assert ( + self.slot is not None + ), "Labware has not been assigned to a slot yet. Use the 'add_slot' method to assign a slot" + + assert len(corner_wells) == 3, "Three points needed to apply manual offset" + assert all( + [len(o) == 2 for o in corner_wells] + ), "Each point should have three coordinates (x,y)" + + # Get the coordinates of the three corner wells (e.g., A1, A12, H12) + upper_left = corner_wells[0] + upper_right = corner_wells[1] + bottom_right = corner_wells[2] + + # Get the coordinates of the three corner wells + # calculate total spacing between wells in each row (width) and column (height) + plate_width = sqrt( + (upper_right[0] - upper_left[0]) ** 2 + + (upper_right[1] - upper_left[1]) ** 2 + ) + plate_height = sqrt( + (bottom_right[0] - upper_right[0]) ** 2 + + (bottom_right[1] - upper_right[1]) ** 2 + ) + + # Assume evenly spaced wells, but possible to have different spacing in rows and columns + x_space = plate_width / (len(self.column_data) - 1) + y_space = plate_height / (len(self.row_data) - 1) + + # Define and average the offset angles for the plate + theta1 = acos((upper_right[1] - bottom_right[1]) / plate_height) + theta2 = acos((upper_right[0] - upper_left[0]) / plate_width) + theta = (theta1 + theta2) / 2.0 + # apply offset to all wells in the labware object + + for well in self: + new_x, new_y = self._translate_point( + well, theta, x_space, y_space, upper_left + ) + well.x = new_x + well.y = new_y + print(f'New manual offset applied to {self.parameters()["loadName"]}') + + if save: + if str(self.slot) in self.manualOffset.keys(): + k = input( + "Are you sure you want to overwrite the manual offset for this labware? Press 'y' key to continue" + ) + if k == "y": + self.manualOffset[str(self.slot)] = corner_wells + with open(self.config_path, "w") as f: + self.data["manual_offset"] = {str(self.slot): corner_wells} + json.dump(self.data, f) + print("Manual offset saved") + else: + print("Manual offset applied, but not saved") + else: + self.manualOffset[str(self.slot)] = corner_wells + with open(self.config_path, "w") as f: + self.data["manual_offset"] = {str(self.slot): corner_wells} + f.seek(0) + json.dump(self.data, f, indent=4) + print("Manual offset saved") + else: + self.manualOffset[str(self.slot)] = corner_wells + + def load_manualOffset(self, apply: bool = True): + """Loads the manual offset of a labware from its config `.json` file for a specific slot + + :param apply: Option to apply the manual offset to the labware or return values, defaults to False + :type apply: bool, optional + + :return: A list of tuples containing the manual offset of the labware + :rtype: List[Tuple[float]] + """ + assert ( + self.slot is not None + ), "Labware has not been assigned to a slot yet. Use the 'add_slot' method to assign a slot" + if self.manualOffset[str(self.slot)]: + if apply: + self.manual_offset(self.manualOffset[str(self.slot)]) + return + else: + return self.manualOffset[str(self.slot)] + else: + return self.data["manual_offset"][self.slot] + + @staticmethod + def _getxyz(location: Union[Well, Tuple, "Location"]): + """Helper function to extract the x, y, z coordinates of a location object. + + :param location: The location object to extract the coordinates from. This can either be a + :class:`Well`, a :tuple: of x, y, z coordinates, or a :class:`Location` object + :type location: Union[Well, Tuple, Location] + :raises ValueError: If the location is not a :class:`Well`, a :class:`tuple`, or a :class:`Location` object + :return: The x, y, z coordinates of the location + :rtype: float, float, float + """ + if type(location) == Well: + x, y, z = location.x, location.y, location.z + elif type(location) == tuple: + x, y, z = location + elif type(location) == Location: + x, y, z = location._point + else: + raise ValueError("Location should be of type Well or Tuple") + + return x, y, z + + +## Adapted from Opentrons API opentrons.types## +class Point(NamedTuple): + """A point in the Jubilee 3D coordinate system. + + :param NamedTuple: A list-like container with a fixed number of elements + :type NamedTuple: :class:`NamedTuple` + :return: A tuple of coordinates (x,y,z) + :rtype: :class:`Point` + """ + + x: float = 0.0 + y: float = 0.0 + z: float = 0.0 + + def add(self, other): + """Adds the coordinates of two points + + :param other: A tuple of coordinates (x,y,z) + :type other: :class:`Point` + :return: A new :class:`Point` object + :rtype: :class:`Point` + """ + if not isinstance(other, Point): + return NotImplemented + return Point(self.x + other.x, self.y + other.y, self.z + other.z) + + def substract(self, other): + """Substracts the coordinates of two points + + :param other: A tuple of coordinates (x,y,z) + :type other: :class:`Point` + :return: A new :class:`Point` object + :rtype: :class:`Point` + """ + if not isinstance(other, Point): + return NotImplemented + return Point(self.x - other.x, self.y - other.y, self.z - other.z) + + def multiply(self, other: Union[int, float]): + """Multiplies the coordinates of a point by a scalar + + :param other: A scalar to multiply the coordinates of a point + :type other: Union[int, float] + :return: A new :class:`Point` object scaled by the value indicated as the function parameter + :rtype: :class:`Point` + """ + + if not isinstance(other, (float, int)): + return NotImplemented + return Point(self.x * other, self.y * other, self.z * other) + + def absolute(self): + """Returns the absolute value of the coordinates of a point. + + :return: The absolute values of a :class:`Point` object + :rtype: :class:`Point` + """ + return Point(abs(self.x), abs(self.y), abs(self.z)) + + def __repr__(self) -> str: + """Returns a string representation of the coordinates of a point. + + :return: A string representation of the coordinates of a point + :rtype: str + """ + + display = "x:{}, y: {}, z:{}".format(self.x, self.y, self.z) + return display + + +class Location: + """A location to target as a motion. + + The location contains a :class:`Point` and possibly an associated + :class:`Labware` or :class:`Well` instance. + """ + + def __init__(self, point: Point, labware: Union[Well, Labware]): + + self._point = point + self._labware = labware + + @property + def point(self) -> Point: + """The coordinates (x,y,z) of a Well or a Labware + + :return: A tuple of coordinates (x,y,z) + :rtype: :class:`Point` + """ + return self._point + + @property + def labware(self): + """The :class:`Well` object associated with the coordinates (x,y,z) + + :return: A :class:`Well` object + :rtype: :class:`Well` + """ + return self._labware + + def __iter__(self) -> Iterable[Union[Point, Well, Labware]]: + """Iterable interface to support unpacking of :class:`Location` objects. + + :return: An interable of :class:`Location` objects + :rtype: Iterable[Union[Point, Well, Labware]] + """ + return iter((self._point, self._labware)) + + def __eq__(self, other: object) -> bool: + """Comparison between two :class:`Location` objects. + + :param other: A :class:`Location` object + :type other: :class:`Location` + :return: True if the two :class:`Location` objects are equal, False otherwise + :rtype: bool + """ + return ( + isinstance(other, Location) + and other._point == self._point + and other._labware == self._labware + ) + + def __repr__(self) -> str: + """Returns a string representation of the :class:`Location` object. + + :return: A string representation of the :class:`Location` object + :rtype: str + """ + return f"Location(point={repr(self._point)}, labware={self._labware})" diff --git a/src/science_jubilee/labware/.ipynb_checkpoints/__init__-checkpoint.py b/src/science_jubilee/labware/.ipynb_checkpoints/__init__-checkpoint.py new file mode 100644 index 0000000..45d5cf4 --- /dev/null +++ b/src/science_jubilee/labware/.ipynb_checkpoints/__init__-checkpoint.py @@ -0,0 +1,31 @@ +# # Iterate through and impoprt all modules in the plates/ +# # this allows us to add new plates in the plates folder and have it automatically usable in Machine.py +# from inspect import isclass +# from pkgutil import iter_modules +# from pathlib import Path +# from importlib import import_module +# # Iterate through and impoprt all modules in the plates/ +# # this allows us to add new plates in the plates folder and have it automatically usable in Machine.py +# from inspect import isclass +# from pkgutil import iter_modules +# from pathlib import Path +# from importlib import import_module + +# # iterate through the modules in the current package +# package_dir = Path(__file__).resolve().parent +# for (_, module_name, _) in iter_modules([package_dir]): +# # import the module and iterate through its attributes +# module = import_module(f"{__name__}.{module_name}") +# for attribute_name in dir(module): +# attribute = getattr(module, attribute_name) +# # iterate through the modules in the current package +# package_dir = Path(__file__).resolve().parent +# for (_, module_name, _) in iter_modules([package_dir]): +# # import the module and iterate through its attributes +# module = import_module(f"{__name__}.{module_name}") +# for attribute_name in dir(module): +# attribute = getattr(module, attribute_name) + +# if isclass(attribute): +# # Add the class to this package's variables +# globals()[attribute_name] = attribute diff --git a/src/science_jubilee/labware/labware_definition/.ipynb_checkpoints/septavialrev1_44_holder_2000ul-checkpoint.json b/src/science_jubilee/labware/labware_definition/.ipynb_checkpoints/septavialrev1_44_holder_2000ul-checkpoint.json new file mode 100644 index 0000000..d667a9e --- /dev/null +++ b/src/science_jubilee/labware/labware_definition/.ipynb_checkpoints/septavialrev1_44_holder_2000ul-checkpoint.json @@ -0,0 +1,547 @@ +{ + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1" + ], + [ + "A2", + "C2", + "D2", + "F2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6" + ], + [ + "A7", + "C7", + "D7", + "F7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8" + ] + ], + "brand": { + "brand": "inHouse", + "brandId": [ + "BP_REV1" + ] + }, + "metadata": { + "displayName": "septavialrev1_44_holder_2000ul", + "displayCategory": "wellPlate", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 39.39 + }, + "wells": { + "A1": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 7.5, + "y": 77.98, + "z": 5.99 + }, + "B1": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 7.5, + "y": 63.88, + "z": 5.99 + }, + "C1": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 7.5, + "y": 49.78, + "z": 5.99 + }, + "D1": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 7.5, + "y": 35.68, + "z": 5.99 + }, + "E1": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 7.5, + "y": 21.58, + "z": 5.99 + }, + "F1": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 7.5, + "y": 7.48, + "z": 5.99 + }, + "A2": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 23.61, + "y": 77.98, + "z": 5.99 + }, + "C2": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 23.61, + "y": 49.78, + "z": 5.99 + }, + "D2": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 23.61, + "y": 35.68, + "z": 5.99 + }, + "F2": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 23.61, + "y": 7.48, + "z": 5.99 + }, + "A3": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 39.72, + "y": 77.98, + "z": 5.99 + }, + "B3": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 39.72, + "y": 63.88, + "z": 5.99 + }, + "C3": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 39.72, + "y": 49.78, + "z": 5.99 + }, + "D3": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 39.72, + "y": 35.68, + "z": 5.99 + }, + "E3": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 39.72, + "y": 21.58, + "z": 5.99 + }, + "F3": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 39.72, + "y": 7.48, + "z": 5.99 + }, + "A4": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 55.83, + "y": 77.98, + "z": 5.99 + }, + "B4": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 55.83, + "y": 63.88, + "z": 5.99 + }, + "C4": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 55.83, + "y": 49.78, + "z": 5.99 + }, + "D4": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 55.83, + "y": 35.68, + "z": 5.99 + }, + "E4": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 55.83, + "y": 21.58, + "z": 5.99 + }, + "F4": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 55.83, + "y": 7.48, + "z": 5.99 + }, + "A5": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 71.94, + "y": 77.98, + "z": 5.99 + }, + "B5": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 71.94, + "y": 63.88, + "z": 5.99 + }, + "C5": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 71.94, + "y": 49.78, + "z": 5.99 + }, + "D5": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 71.94, + "y": 35.68, + "z": 5.99 + }, + "E5": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 71.94, + "y": 21.58, + "z": 5.99 + }, + "F5": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 71.94, + "y": 7.48, + "z": 5.99 + }, + "A6": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 88.05, + "y": 77.98, + "z": 5.99 + }, + "B6": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 88.05, + "y": 63.88, + "z": 5.99 + }, + "C6": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 88.05, + "y": 49.78, + "z": 5.99 + }, + "D6": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 88.05, + "y": 35.68, + "z": 5.99 + }, + "E6": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 88.05, + "y": 21.58, + "z": 5.99 + }, + "F6": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 88.05, + "y": 7.48, + "z": 5.99 + }, + "A7": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 104.16, + "y": 77.98, + "z": 5.99 + }, + "C7": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 104.16, + "y": 49.78, + "z": 5.99 + }, + "D7": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 104.16, + "y": 35.68, + "z": 5.99 + }, + "F7": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 104.16, + "y": 7.48, + "z": 5.99 + }, + "A8": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 120.27, + "y": 77.98, + "z": 5.99 + }, + "B8": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 120.27, + "y": 63.88, + "z": 5.99 + }, + "C8": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 120.27, + "y": 49.78, + "z": 5.99 + }, + "D8": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 120.27, + "y": 35.68, + "z": 5.99 + }, + "E8": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 120.27, + "y": 21.58, + "z": 5.99 + }, + "F8": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 120.27, + "y": 7.48, + "z": 5.99 + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "flat" + }, + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "A2", + "C2", + "D2", + "F2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "A7", + "C7", + "D7", + "F7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8" + ] + } + ], + "parameters": { + "format": "irregular", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "septavialrev1_44_holder_2000ul" + }, + "namespace": "custom_beta", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } +} diff --git a/src/science_jubilee/labware/labware_definition/septavialrev1_44_holder_2000ul.json b/src/science_jubilee/labware/labware_definition/septavialrev1_44_holder_2000ul.json new file mode 100644 index 0000000..7b0601e --- /dev/null +++ b/src/science_jubilee/labware/labware_definition/septavialrev1_44_holder_2000ul.json @@ -0,0 +1,592 @@ +{ + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2" + + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8" + ] + ], + "brand": { + "brand": "inHouse", + "brandId": [ + "BP_REV1" + ] + }, + "metadata": { + "displayName": "septavialrev1_44_holder_2000ul", + "displayCategory": "wellPlate", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 39.39 + }, + "wells": { + "A1": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 7.5, + "y": 77.98, + "z": 5.99 + }, + "B1": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 7.5, + "y": 63.88, + "z": 5.99 + }, + "C1": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 7.5, + "y": 49.78, + "z": 5.99 + }, + "D1": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 7.5, + "y": 35.68, + "z": 5.99 + }, + "E1": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 7.5, + "y": 21.58, + "z": 5.99 + }, + "F1": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 7.5, + "y": 7.48, + "z": 5.99 + }, + "A2": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 23.61, + "y": 77.98, + "z": 5.99 + }, + "B2": { + "depth": 0, + "totalLiquidVolume": 0, + "shape": "circular", + "diameter": 0, + "x": 0, + "y": 0, + "z": 20 + }, + "C2": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 23.61, + "y": 49.78, + "z": 5.99 + }, + "D2": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 23.61, + "y": 35.68, + "z": 5.99 + }, + "E2":{ + "depth": 0, + "totalLiquidVolume": 0, + "shape": "circular", + "diameter": 0, + "x": 0, + "y": 0, + "z": 20 + }, + "F2": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 23.61, + "y": 7.48, + "z": 5.99 + }, + "A3": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 39.72, + "y": 77.98, + "z": 5.99 + }, + "B3": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 39.72, + "y": 63.88, + "z": 5.99 + }, + "C3": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 39.72, + "y": 49.78, + "z": 5.99 + }, + "D3": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 39.72, + "y": 35.68, + "z": 5.99 + }, + "E3": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 39.72, + "y": 21.58, + "z": 5.99 + }, + "F3": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 39.72, + "y": 7.48, + "z": 5.99 + }, + "A4": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 55.83, + "y": 77.98, + "z": 5.99 + }, + "B4": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 55.83, + "y": 63.88, + "z": 5.99 + }, + "C4": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 55.83, + "y": 49.78, + "z": 5.99 + }, + "D4": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 55.83, + "y": 35.68, + "z": 5.99 + }, + "E4": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 55.83, + "y": 21.58, + "z": 5.99 + }, + "F4": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 55.83, + "y": 7.48, + "z": 5.99 + }, + "A5": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 71.94, + "y": 77.98, + "z": 5.99 + }, + "B5": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 71.94, + "y": 63.88, + "z": 5.99 + }, + "C5": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 71.94, + "y": 49.78, + "z": 5.99 + }, + "D5": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 71.94, + "y": 35.68, + "z": 5.99 + }, + "E5": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 71.94, + "y": 21.58, + "z": 5.99 + }, + "F5": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 71.94, + "y": 7.48, + "z": 5.99 + }, + "A6": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 88.05, + "y": 77.98, + "z": 5.99 + }, + "B6": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 88.05, + "y": 63.88, + "z": 5.99 + }, + "C6": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 88.05, + "y": 49.78, + "z": 5.99 + }, + "D6": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 88.05, + "y": 35.68, + "z": 5.99 + }, + "E6": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 88.05, + "y": 21.58, + "z": 5.99 + }, + "F6": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 88.05, + "y": 7.48, + "z": 5.99 + }, + "A7": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 104.16, + "y": 77.98, + "z": 5.99 + }, + "B7":{ + "depth": 0, + "totalLiquidVolume": 0, + "shape": "circular", + "diameter": 0, + "x": 0, + "y": 0, + "z": 20 + }, + "C7": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 104.16, + "y": 49.78, + "z": 5.99 + }, + "D7": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 104.16, + "y": 35.68, + "z": 5.99 + }, + "E7":{ + "depth": 0, + "totalLiquidVolume": 0, + "shape": "circular", + "diameter": 0, + "x": 0, + "y": 0, + "z": 20 + }, + "F7": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 104.16, + "y": 7.48, + "z": 5.99 + }, + "A8": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 120.27, + "y": 77.98, + "z": 5.99 + }, + "B8": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 120.27, + "y": 63.88, + "z": 5.99 + }, + "C8": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 120.27, + "y": 49.78, + "z": 5.99 + }, + "D8": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 120.27, + "y": 35.68, + "z": 5.99 + }, + "E8": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 120.27, + "y": 21.58, + "z": 5.99 + }, + "F8": { + "depth": 33.4, + "totalLiquidVolume": 2000, + "shape": "circular", + "diameter": 6, + "x": 120.27, + "y": 7.48, + "z": 5.99 + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "flat" + }, + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8" + ] + } + ], + "parameters": { + "format": "irregular", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "septavialrev1_44_holder_2000ul" + }, + "namespace": "custom_beta", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } +} diff --git a/src/science_jubilee/tools/.ipynb_checkpoints/Pipette-checkpoint.py b/src/science_jubilee/tools/.ipynb_checkpoints/Pipette-checkpoint.py new file mode 100644 index 0000000..d0afcbc --- /dev/null +++ b/src/science_jubilee/tools/.ipynb_checkpoints/Pipette-checkpoint.py @@ -0,0 +1,829 @@ +import json +import logging +import os +from itertools import dropwhile, takewhile +from typing import Iterator, List, Tuple, Union + +from science_jubilee.labware.Labware import Labware, Location, Well +from science_jubilee.tools.Tool import ( + Tool, + ToolConfigurationError, + ToolStateError, + requires_active_tool, +) + +logger = logging.getLogger(__name__) + + +def tip_check(func): + """Decorator to check if the pipette has a tip attached before performing an action.""" + + def wrapper(self, *args, **kwargs): + if self.has_tip == False: + raise ToolStateError( + "Error: No tip is attached. Cannot complete this action" + ) + else: + func(self, *args, **kwargs) + + return wrapper + + +class Pipette(Tool): + """A class representation of an Opentrons OT2 pipette.""" + + def __init__( + self, + index, + name, + brand, + model, + max_volume, + min_volume, + zero_position, + blowout_position, + drop_tip_position, + mm_to_ul, + ): + """Initialize the pipette object + + :param index: The tool index of the pipette on the machine + :type index: int + :param name: The name associated with the tool (e.g. 'p300_single') + :type name: str + :param brand: The brand of the pipette + :type brand: str + :param model: The model of the pipette + :type model: str + :param max_volume: The maximum volume of the pipette in uL + :type max_volume: float + :param min_volume: The minimum volume of the pipette in uL + :type min_volume: float + :param zero_position: The position of the plunger before using a :method:`aspirate` step + :type zero_position: float + :param blowout_position: The position of the plunger for running a :method:`blowout` step + :type blowout_position: float + :param drop_tip_position: The position of the plunger for running a :method:`drop_tip` step + :type drop_tip_position: float + :param mm_to_ul: The conversion factor for converting motor microsteps in mm to uL + :type mm_to_ul: float + """ + super().__init__( + index, + name, + brand=brand, + model=model, + max_volume=max_volume, + min_volume=min_volume, + zero_position=zero_position, + blowout_position=blowout_position, + drop_tip_position=drop_tip_position, + mm_to_ul=mm_to_ul, + ) + self.has_tip = False + self.current_well = None + self.trash = None + self.is_primed = False + + @classmethod + def from_config( + cls, + index, + name, + config_file: str, + path: str = os.path.join(os.path.dirname(__file__), "configs"), + ): + """Initialize the pipette object from a config file + + :param index: The tool index of the pipette on the machine + :type index: int + :param name: The tool name + :type name: str + :param config_file: The name of the config file containign the pipette parameters + :type config_file: str + :param path: The path to the labware configuration `.json` files for the labware, + defaults to the 'labware_definition/' in the science_jubilee/labware directory. + :returns: A :class:`Pipette` object + :rtype: :class:`Pipette` + """ + config = os.path.join(path, config_file) + with open(config) as f: + kwargs = json.load(f) + + return cls(index, name, **kwargs) + + def post_load(self): + """Prime the Pipette after loading it onto the Machine sot hat it is ready to use""" + self.prime() + + def vol2move(self, vol): + """Converts desired volume in uL to a movement of the pipette motor axis + + :param vol: The desired amount of liquid expressed in uL + :type vol: float + :return: The corresponding motor movement in mm + :rtype: float + """ + dv = vol * self.mm_to_ul # will need to change this value + + return dv + + def prime(self, s=2500): + """Moves the plunger to the low-point on the pipette motor axis to prepare for further commands + Note::This position should not engage the pipette tip plunger + + :param s: The speed of the plunger movement in mm/min + :type s: int + """ + self._machine.move_to(v=self.zero_position, s=s, wait=True) + self.is_primed = True + + @requires_active_tool + def _aspirate(self, vol: float, s: int = 2000): + """Moves the plunger upwards to aspirate liquid into the pipette tip + + :param vol: The volume of liquid to aspirate in uL + :type vol: float + :param s: The speed of the plunger movement in mm/min + :type s: int + """ + if self.is_primed == True: + pass + else: + self.prime() + + dv = self.vol2move(vol) * -1 + pos = self._machine.get_position() + end_pos = float(pos["V"]) + dv + + self._machine.move_to(v=end_pos, s=s) + + @requires_active_tool + @tip_check + def aspirate( + self, vol: float, location: Union[Well, Tuple, Location], s: int = 2000 + ): + """Moves the pipette to the specified location and aspirates the desired volume of liquid + + :param vol: The volume of liquid to aspirate in uL + :type vol: float + :param location: The location from where to aspirate the liquid from. + :type location: Union[Well, Tuple, Location] + :param s: The speed of the plunger movement in mm/min, defaults to 2000 + :type s: int, optional + :raises ToolStateError: If the pipette does not have a tip attached + """ + x, y, z = Labware._getxyz(location) + + if type(location) == Well: + self.current_well = location + elif type(location) == Location: + self.current_well = location._labware + else: + pass + + self._machine.safe_z_movement() + self._machine.move_to(x=x, y=y) + self._machine.move_to(z=z) + self._aspirate(vol, s=s) + + @requires_active_tool + @tip_check + def _dispense(self, vol: float, s: int = 2000): + """Moves the plunger downwards to dispense liquid out of the pipette tip + + :param vol: The volume of liquid to dispense in uL + :type vol: float + :param s: The speed of the plunger movement in mm/min + :type s: int + + Note:: Ideally the user does not call this functions directly, but instead uses the :method:`dispense` method + """ + dv = self.vol2move(vol) + pos = self._machine.get_position() + end_pos = float(pos["V"]) + dv + + # TODO: Figure out why checks break for transfer, work fine for manually aspirating and dispensing + # if end_pos > self.zero_position: + # raise ToolStateError("Error: Pipette does not have anything to dispense") + # elif dv > self.zero_position: + # raise ToolStateError ("Error : The volume to be dispensed is greater than what was aspirated") + self._machine.move_to(v=end_pos, s=s) + + @requires_active_tool + @tip_check + def dispense( + self, vol: float, location: Union[Well, Tuple, Location], s: int = 2000 + ): + """Moves the pipette to the specified location and dispenses the desired volume of liquid + + :param vol: The volume of liquid to dispense in uL + :type vol: float + :param location: The location to dispense the liquid into. + :type location: Union[Well, Tuple, Location] + :param s: The speed of the plunger movement in mm/min, defaults to 2000 + :type s: int, optional + :raises ToolStateError: If the pipette does not have a tip attached + """ + x, y, z = Labware._getxyz(location) + + if type(location) == Well: + self.current_well = location + if z == location.z: + z = z + 10 + else: + pass + elif type(location) == Location: + self.current_well = location._labware + else: + pass + + self._machine.safe_z_movement() + self._machine.move_to(x=x, y=y) + self._machine.move_to(z=z) + self._dispense(vol, s=s) + + @requires_active_tool + def transfer( + self, + vol: Union[float, list[float]], + source_well: Union[Well, Tuple, Location], + destination_well: Union[Well, Tuple, Location], + s: int = 3000, + blowout=None, + mix_before: tuple = None, + mix_after: tuple = None, + air_gap: float = 0, + new_tip: str = "always", + ): + """Transfers the desired volume of liquid from the source well to the destination well + + This is a combination of the :method:`aspirate` and :method:`dispense` steps. + + :param vol: The volume of liquid to transfer in uL + :type vol: float + :param source_well: The location from where to aspirate the liquid from. + :type source_well: Union[Well, Tuple, Location] + :param destination_well: The location to dispense the liquid into. + :type destination_well: Union[Well, Tuple, Location] + :param s: The speed of the plunger movement in mm/min, defaults to 3000 + :type s: int, optional + :param blowout: The location to blowout any remainign liquid in the pipette tip + :type blowout: Union[Well, Tuple, Location], optional + :param mix_before: The number of times to mix before dispensing and the volume to mix + :type mix_before: tuple, optional + :param mix_after: The number of times to mix after dispensing and the volume to mix + :type mix_after: tuple, optional + :param new_tip: Whether or not to use a new tip for the transfer. Can be 'always', 'never', or 'once' + :type new_tip: str, optional + :param air_gap: The volume of air to aspirate after aspirating the liquid, defaults to None + :type air_gap: float, optional + + Note:: :param new_tip: still not implemented at the moment (2023-11-16) + """ + # TODO: check that tip picked up and get a new one if not + + if self.is_primed == True: + pass + else: + self.prime() + + if new_tip == "never": + assert ( + self.has_tip == True + ), "Error: Pipette should have a tip before transfer with new_tip = 'never'" + else: + assert ( + self.has_tip == False + ), f"Error: Pipette should not have a tip before transfer with new_tip = '{new_tip}'" + if air_gap > 0: + assert ( + air_gap < self.max_volume + ), "Error: Air gap volume should be less than Pipette max volume." + assert ( + self.trash != None + ), "Error: Trash location not set, do so before continuing." + # saves some code if we make a list regardless + if type(destination_well) != list: + destination_well = [destination_well] # make it into a list + + if type(source_well) != list: + source_well = [source_well] # make it into a list + + total_transfers = max(len(source_well), len(destination_well)) + volumes = self._create_volume_list(vol, total_transfers) + source_well, destination_well = self._extend_source_target_lists( + source_well, destination_well + ) + targets = zip(source_well, destination_well) + iterations = self._expand_for_volume_contraints( + volumes, targets, self.max_volume - air_gap + ) + + ## TODO: maybe add TiTracker to pipette class + TT = self.TipTracker + TT_dict = TT._tip_stock_mapping + + for step_vol, (source, dest) in iterations: + # skip if the volume is zero + if step_vol == 0: + continue + else: + if type(source) == Well: + src = source + elif type(source) == Location: + src = source._labware + + # get coordinates for source and destination wells + xs, ys, zs = Labware._getxyz(source) + xd, yd, zd = Labware._getxyz(dest) + + source_name = f"{src.name}_{src.slot}" + + # --------------- Tip Strategy ---------------- + if new_tip == "never": + pass + elif new_tip == "once": + if source_name in TT_dict.keys(): + tip = TT_dict[source_name] + else: + tip = TT.next_tip() + TT.assign_tip_to_stock(tip, source_name) + else: + tip = TT.next_tip() + + self.pickup_tip(tip) + TT.use_tip(tip) # note on the TipTracker class that tip is being used + + # --------------- Aspirate ---------------- + + self._machine.safe_z_movement() + self._machine.move_to(x=xs, y=ys) + + self.current_well = src + + self._machine.move_to(z=zs) + self._aspirate(step_vol, s=s) + + if air_gap > 0: + self.air_gap( + air_gap + ) # the check for this is in the _create_volume_list method + + # mix before moving to destination well + if mix_before: + self.mix(mix_before[0], mix_before[1]) + else: + pass + + # --------------- Dispense ---------------- + + self._machine.safe_z_movement() + self._machine.move_to(x=xd, y=yd) + if type(dest) == Well: + self.current_well = dest + elif type(dest) == Location: + self.current_well = dest._labware + self._machine.move_to(z=zd) + self._dispense(step_vol, s=s) + + # mix after dispensing into destination well + # check if user indicated a specific well to mix after dispensing + if mix_after: + if len(mix_after) == 3: + stock_to_mix = mix_after[2] + if type(stock_to_mix) == Location: + stock_to_mix = stock_to_mix._labware + elif type(stock_to_mix) == Well: + pass + if src == stock_to_mix: + self.mix(mix_after[0], mix_after[1]) + else: + pass + elif mix_after: + self.mix(mix_after[0], mix_after[1]) + else: + pass + + # blow_out if indicated + if blowout: + self.blowout() + else: + pass + + # --------------- Tip Strategy ---------------- + if new_tip == "always": + self.drop_tip() + elif new_tip == "once": + if mix_after: + if len(mix_after) == 3 and src == stock_to_mix: + self.drop_tip() + TT_dict.pop(source_name) + else: + self.return_tip(tip) + else: + self.return_tip(tip) + else: + pass + + def _create_volume_list(self, volume, total_xfers): + """Creates a list of volumes to transfer + + :param volume: The volume of liquid to transfer + :type volume: Union[float, list[float]] + :param total_xfers: The total number of transfer steps to perform + :type total_xfers: int + :return: A list of volumes to transfer + + Note: This function was taken from the Opentrons API and modified to work with the Pipette class of Science_Jubilee + """ + + if isinstance(volume, (float, int)): + vol_list = [self.vol2move(volume)] * total_xfers + return [volume] * total_xfers + elif isinstance(volume, list): + if not len(volume) == total_xfers: + raise RuntimeError( + "List of volumes should be equal to number " "of transfers" + ) + else: + vol_list = [self.vol2move(v) for v in volume] + return vol_list + else: + if not isinstance(volume, List): + raise TypeError( + "Volume expected as a number or List or" + " tuple but got {}".format(volume) + ) + return vol_list + + @staticmethod + def _extend_source_target_lists( + sources: List[Union[Well, Location]], + targets: List[Union[Well, Location]], + ): + """Extend source or target list to match the length of the other + + :param sources: The list of source wells + :type sources: List[Union[Well, Location]] + :param targets: The list of target wells + :type targets: List[Union[Well, Location]] + :return: The extended source and target lists + :rtype: Tuple[List[Union[Well, Location]], List[Union[Well, Location]]] + + Note: This function was taken from the Opentrons API and modified to work with the Pipette class of Science_Jubilee + """ + if len(sources) < len(targets): + if len(targets) % len(sources) != 0: + raise ValueError("Source and destination lists must be divisible") + sources = [ + source + for source in sources + for i in range(int(len(targets) / len(sources))) + ] + elif len(sources) > len(targets): + if len(sources) % len(targets) != 0: + raise ValueError("Source and destination lists must be divisible") + targets = [ + target + for target in targets + for i in range(int(len(sources) / len(targets))) + ] + return sources, targets + + @staticmethod + def _expand_for_volume_contraints( + volumes: Iterator[float], targets: Iterator, max_volume: float + ): + """Expands the volumes and targets to ensure that the volume does not exceed the maximum volume of the pipette + + :param volumes: An iterator of the volumes to transfer + :type volumes: Iterator[float] + :param targets: An iterator of the targets to transfer the volumes to + :type targets: Iterator + :param max_volume: The maximum volume of the pipette in uL + :type max_volume: float + :return: An iterator of the volumes and targets to transfer + :rtype: Iterator + + Note: This function was taken from the Opentrons API and modified to work with the Pipette class of Science_Jubilee + """ + for volume, target in zip(volumes, targets): + while volume > max_volume * 2: + yield max_volume, target + volume -= max_volume + + if volume > max_volume: + volume /= 2 + yield volume, target + yield volume, target + + @requires_active_tool + @tip_check + def blowout(self, s: int = 6000): + """Blows out any remaining liquid in the pipette tip + + :param s: The speed of the plunger movement in mm/min, defaults to 3000 + :type s: int, optional + """ + + well = self.current_well + self._machine.move_to(z=well.top_ + 2) + self._machine.move_to(v=self.blowout_position, s=s) + self.prime() + + @requires_active_tool + @tip_check + def air_gap(self, vol): + """Moves the plunger upwards to aspirate air into the pipette tip + + :param vol: The volume of air to aspirate in uL + :type vol: float + """ + # TODO: Add a check to ensure compounded volume does not exceed max volume of pipette + + dv = self.vol2move(vol) * -1 + well = self.current_well + self._machine.move_to(z=well.top_ + 20) + self._machine.move(v=-1 * dv) + + @requires_active_tool + @tip_check + def mix(self, vol: float, n: int, s: int = 5500): + """Mixes liquid by alternating aspirate and dispense steps for the specified number of times + + :param vol: The volume of liquid to mix in uL + :type vol: float + :param n: The number of times to mix + :type n: int + :param s: The speed of the plunger movement in mm/min, defaults to 5000 + :type s: int, optional + """ + v = self.vol2move(vol) * -1 + + self._machine.move_to(z=self.current_well.top_ + 1) + self.prime() + + # TODO: figure out a better way to indicate mixing height position that is not hardcoded + self._machine.move_to(z=self.current_well.bottom_ + 1) + for i in range(0, n): + self._aspirate(vol, s=s) + self.prime(s=s) + + ## In progress (2023-10-12) To test + @requires_active_tool + @tip_check + def stir(self, n_times: int = 1, height: float = None): + """Stirs the liquid in the current well by moving the pipette tip in a circular motion + + :param n_times: The number of times to stir the liquid, defaults to 1 + :type n_times: int, optional + :param height: The z-coordinate to move the tip to during the stir step, defaults to None + :type height: float, optional + :raises ToolStateError: If the pipette does not have a tip attached before stirring or if the pipette is not in a well + """ + z = self.current_well.z + 0.5 # place pieptte tip close to the bottom + pos = self._machine.get_position() + x_ = float(pos["X"]) + y_ = float(pos["Y"]) + z_ = float(pos["Z"]) + + # check position first + if x_ != round(self.current_well.x) and y_ != round(self.current_well.y, 2): + raise ToolStateError( + "Error: Pipette shuold be in a well before it can stir" + ) + elif z_ != round(z, 2): + self._machine.move_to(z=z) + + radius = self.current_well.diameter / 2 - ( + self.current_well.diameter / 6 + ) # adjusted so that it does not hit the walls fo the well + + for n in range(n_times): + x_sp = self.current_well.x + y_sp = self.current_well.y + I = -1 * radius + J = 0 # keeping same y so relative y difference is 0 + if height: + Z = z + height + self._machine.gcode(f"G2 X{x_sp} Y{y_sp} Z{Z} I{I} J{J}") + self._machine.gcode(f"M400") # wait until movement is completed + self._machine.move_to(z=z) + else: + self._machine.gcode(f"G2 X{x_sp} Y{y_sp} I{I} J{J}") + self._machine.gcode(f"M400") # wait until movement is completed + + def update_z_offset(self, tip: bool = None): + """Shift the z-offset of the tool to account for the tip length + + :param tip: Parameter to indicated whether to add or remove the tip offset, defaults to None + :type tip: bool, optional + """ + if isinstance(self.tiprack, list): + tip_offset = self.tiprack[0].tip_length - self.tiprack[0].tip_overlap + else: + tip_offset = self.tiprack.tip_length - self.tiprack.tip_overlap + + if tip == True: + new_z = self.tool_offset - tip_offset + else: + new_z = self.tool_offset + + self._machine.gcode(f"G10 P{self.index} Z{new_z}") + + def add_tiprack(self, tiprack: Union[Labware, list]): + """Associate a tiprack with the pipette tool + + :param tiprack: The tiprack to associate with the pipette + :type tiprack: Union[Labware, list] + """ + if type(tiprack) != list: + tiprack = [tiprack] + + tips = [] + for rack in tiprack: + for t in range(96): + tips.append(rack[t]) + + self.tips = tips + self.TipTracker = TipTracker(tips) + self.tiprack = tiprack + + @requires_active_tool + def _pickup_tip(self, z): + """Moves the Jubilee Z-axis upwards to pick up a pipette tip and stops once the tip sensor is triggered + + :param z: The z-coordinate to move the pipette to + :type z: float + :raises ToolStateError: If the pipette already has a tip attached + """ + if self.has_tip == False: + # self._machine.move_to(z=z-10 , s=1200) # test this- we might benefit from a faster approach to pickingup pipette and then slowing down + self._machine.move_to(z=z, s=800, param="H4") + else: + raise ToolStateError("Error: Pipette already equipped with a tip.") + # TODO: Should this be an error or a warning? + + @requires_active_tool + def pickup_tip(self, tip_: Union[Well, Tuple] = None): + """Moves the pipette to the specified location and picks up a tip + + This function can either take a specific tip or if not specified, will pick up the next available + tip in the tiprack associated with the pipette. + + :param tip_: The location of the pipette tip to pick up, defaults to None + :type tip_: Union[Well, Tuple], optional + """ + if tip_ is None: + tip = self.TipTracker.next_tip() + self.TipTracker.use_tip(tip) + else: + tip = tip_ + tip_.set_has_tip(False) + tip_.set_clean_tip(False) + + x, y, z = Labware._getxyz(tip) + self._machine.safe_z_movement() + self._machine.move_to(x=x, y=y) + self._pickup_tip(z) + self.has_tip = True + self.update_z_offset(tip=True) + # # move the plate down( should be + z) for safe movement + self._machine.move_to(z=self._machine.deck.safe_z + 10) + + @requires_active_tool + def return_tip(self, location: Well = None): + """Returns the pipette tip to the either the specified location or to where the tip was picked up from + + :param location: The location to return the tip to, defaults to None (i.e. return to where the tip was picked up from) + :type location: :class:`Well`, optional + """ + if location is None: + w = self.TipTracker.previous_tip() + x, y, z = Labware._getxyz(w) + else: + if type(location) == Well: + w = location + elif type(location) == Location: + w = location._labware + + x, y, z = Labware._getxyz(location) + + self.TipTracker.return_tip( + w + ) # this will still setthe pipette tip as not clean! + + self._machine.safe_z_movement() + self._machine.move_to(x=x, y=y) + # z moves up/down to make sure tip actually makes it into rack + self._machine.move_to(z=w.bottom_ + 20) + self._drop_tip() + self.prime() + self._machine.move_to(z=w.bottom_ + 30) + self.has_tip = False + self.update_z_offset(tip=False) + + @requires_active_tool + @tip_check + def _drop_tip(self): + """Moves the plunger to eject the pipette tip + + :raises ToolConfigurationError: If the pipette does not have a tip attached + """ + self._machine.move_to(v=self.drop_tip_position, s=5000) + + @requires_active_tool + @tip_check + def drop_tip(self, location: Union[Well, Tuple] = None): + """Moves the pipette to the specified location and drops the pipette tip + + :param location: The location to drop the tip into + :type location: Union[:class:`Well`, tuple] + """ + + if location is None and self.trash: + x, y, z = Labware._getxyz(self.trash) + elif location is not None: + x, y, z = Labware._getxyz(location) + else: + raise ToolConfigurationError( + "Error: No location specified to drop tip into. Either specify a location or set the trash location for the Pipette" + ) + + self._machine.safe_z_movement() + self._machine.move_to(x=x, y=y) + self._drop_tip() + self.prime() + self.has_tip = False + self.update_z_offset(tip=False) + + # logger.info(f"Dropped tip at {(x,y,z)}") + + +class TipTracker: + """A class to track the usage of pipette tips and their location in the tiprack + + :param tips: The list of tips in the tiprack + :type tips: list[:class:`Well`] + :param start_well: The starting well to begin tracking the tips from, defaults to None + :type start_well: :class:`Well`, optional + + Note: This function was taken from the Opentrons API and modified to work with the Pipette class of Science_Jubilee + """ + + def __init__(self, tips, start_well=None): + + self._wells = tips[start_well:] + self._available_clean_tips = tips + self._tip_stock_mapping = {} + + def next_tip(self, start_well=None): + if start_well: + start_index = self._wells.index(start_well) + available_wells = list( + dropwhile( + lambda w: not w.has_tip, self._available_clean_tips[start_index:] + ) + ) + else: + available_wells = list( + dropwhile(lambda w: not w.has_tip, self._available_clean_tips) + ) + + assert available_wells, "No more available tips" + + clean_tips = list(dropwhile(lambda w: not w.clean_tip == True, available_wells)) + if clean_tips: + self._available_clean_tips = clean_tips + else: + self._available_clean_tips = [] + + first_available_well = clean_tips[0] + return first_available_well + + def use_tip(self, tip_well): + tip_well.set_has_tip(False) + tip_well.set_clean_tip(False) + + def previous_tip(self): + + drop_leading_filled = list(dropwhile(lambda w: w.has_tip, self._wells)) + first_gap = list(takewhile(lambda w: not w.has_tip, drop_leading_filled)) + try: + return first_gap[-1] + except IndexError: + return None + + def return_tip(self, well=None): + if well.has_tip: + raise AssertionError(f"Well {repr(well)} has a tip") + else: + well.set_has_tip(True) + + def assign_tip_to_stock(self, tip_well, stock_well): + + if stock_well in self._tip_stock_mapping.keys(): + pass + else: + self._tip_stock_mapping[stock_well] = tip_well diff --git a/src/science_jubilee/tools/HTTPSyringe.py b/src/science_jubilee/tools/HTTPSyringe.py new file mode 100644 index 0000000..0c75187 --- /dev/null +++ b/src/science_jubilee/tools/HTTPSyringe.py @@ -0,0 +1,264 @@ +import json +import logging +import os +import time +from itertools import dropwhile, takewhile +from typing import Iterator, List, Tuple, Union + +import numpy as np +import requests + +from science_jubilee.labware.Labware import Labware, Location, Well +from science_jubilee.tools.Tool import ( + Tool, + ToolConfigurationError, + ToolStateError, + requires_active_tool, +) + + +class HTTPSyringe(Tool): + + def __init__(self, index, name, url): + """ + HTTP Syringe is digital syringe for Jubilee + + """ + + self.name = name + self.index = index + # get config things from HTTP interface + config_r = requests.post(url + "/get_config", json={"name": name}) + + config = config_r.json() + + super().__init__(index, **config, url=url) + + status_r = requests.post(url + "/get_status", json={"name": name}) + + status = status_r.json() + + self.syringe_loaded = status["syringe_loaded"] + self.remaining_volume = status["remaining_volume"] + + return + + @classmethod + def from_config(cls, index, fp): + with open(fp) as f: + kwargs = json.load(f) + + return cls(index, **kwargs) + + def status(self): + """ + Fetch and update status + """ + + r = requests.post(self.url + "/get_status", json={"name": self.name}) + status = r.json() + + self.syringe_loaded = status["syringe_loaded"] + self.remaining_volume = status["remaining_volume"] + + return status + + def load_syringe(self, volume, pulsewidth): + """ + Configure a syringe after physically loading it + + volume: Current loaded volume in syringe + pulsewidth: current pulsewidth position of servo + """ + + data = {} + data["volume"] = volume + data["pulsewidth"] = pulsewidth + data["name"] = self.name + + requests.post(self.url + "/load_syringe", json=data) + + status = self.status() + + print(f'Loaded syringe, remaining volume {status["remaining_volume"]} uL') + + return + + @requires_active_tool + def _aspirate(self, vol, s): + + assert isinstance(vol, float) or isinstance( + vol, int + ), "Vol must be float or int" + + assert ( + vol < self.capacity - self.remaining_volume + ), f"Error: Syringe {self.name} available volume is {self.capacity - self.remaining_volume} uL, {vol} mL aspiration requested" + + r = requests.post( + self.url + "/aspirate", json={"volume": vol, "name": self.name, "speed": s} + ) + + assert r.status_code == 200, f"Error in aspirate request: {r.content}" + + status_r = requests.post(self.url + "/get_status", json={"name": self.name}) + + status_dict = status_r.json() + + self.remaining_volume = status_dict["remaining_volume"] + + return + + @requires_active_tool + def _dispense(self, vol, s): + + assert isinstance(vol, float) or isinstance( + vol, int + ), "Vol must be flaot or int" + assert ( + vol <= self.remaining_volume + ), f"Error: Syringe {self.name} remaining volume is {self.remaining_volume} uL, but {vol} uL dispense requested" + + r = requests.post( + self.url + "/dispense", json={"volume": vol, "name": self.name, "speed": s} + ) + + assert r.status_code == 200, f"Error in dispense request: {r.content}" + + status_r = requests.post(self.url + "/get_status", json={"name": self.name}) + status_dict = status_r.json() + self.remaining_volume = status_dict["remaining_volume"] + return + + @requires_active_tool + def dispense( + self, vol: float, location: Union[Well, Tuple, Location], s: int = 100 + ): + """Moves the pipette to the specified location and dispenses the desired volume of liquid + + :param vol: The volume of liquid to dispense in uL + :type vol: float + :param location: The location to dispense the liquid into. + :type location: Union[Well, Tuple, Location] + :param s: Speed at which to dispense. Best effort compliance based on constraints of system. uL/S + :type s: int + :raises ToolStateError: If the pipette does not have a tip attached + + """ + x, y, z = Labware._getxyz(location) + + if type(location) == Well: + self.current_well = location + if z == location.z: + z = z + 10 + else: + pass + elif type(location) == Location: + self.current_well = location._labware + else: + pass + + self._machine.safe_z_movement() + self._machine.move_to(x=x, y=y, wait=True) + self._machine.move_to(z=z, wait=True) + self._dispense(vol, s) + + @requires_active_tool + def aspirate( + self, vol: float, location: Union[Well, Tuple, Location], s: int = 100 + ): + """Moves the pipette to the specified location and aspirates the desired volume of liquid + + :param vol: The volume of liquid to aspirate in uL + :type vol: float + :param location: The location from where to aspirate the liquid from. + :type location: Union[Well, Tuple, Location] + :param s: Speed at which to aspirate. Best effort compliance based on constraints of system. uL/S. + :type s: int + :raises ToolStateError: If the pipette does not have a tip attached + """ + x, y, z = Labware._getxyz(location) + + if type(location) == Well: + self.current_well = location + elif type(location) == Location: + self.current_well = location._labware + else: + pass + + self._machine.safe_z_movement() + self._machine.move_to(x=x, y=y, wait=True) + self._machine.move_to(z=z, wait=True) + self._aspirate(vol, s) + + @requires_active_tool + def mix( + self, + vol: float, + n_mix: int, + location: Union[Well, Tuple, Location], + t_hold: int = 1, + s_aspirate: int = 100, + s_dispense: int = 100, + ): + """ + Mixes n times with volume vol + + :param vol: Volume to aspirate/dispense in mixing. uL + :type vol: float + :param n_mix: Number of times to aspirate/dispense in mixing. + :type n_mix: int + :param location: The location from where to mix + :type location: Union[Well, Tuple, Location] + :param t_hold: Hold time at top and bottom of mix cycle to allow liquid to 'catch up'. Units seconds + :type t_hold: int + :param s: Speed at which to mix in uL/S. Best effort compliance. + :type s: int + """ + x, y, z = Labware._getxyz(location) + + if type(location) == Well: + self.current_well = location + elif type(location) == Location: + self.current_well = location._labware + else: + pass + + self._machine.safe_z_movement() + self._machine.move_to(x=x, y=y, wait=True) + self._aspirate( + 500, s_aspirate + ) # pre-aspirate 500 uL then blow this out at the end to avoid holding onto extra solution + self._machine.move_to(z=z, wait=True) + + for _ in range(n_mix): + self._aspirate(vol, s_aspirate) + time.sleep(t_hold) + self._dispense(vol, s_dispense) + time.sleep(t_hold) + + self._dispense(500, s_dispense) + + def set_pulsewidth(self, pulsewidth: int, s: int = 100): + """ + Manually move the servo actuator to a new location by setting the new pulsewidth. + + Does not update volume, use carefully + + :param pulsewidth: The servo motor pulsewidth to set the servo to. Read up on servo positioning for this to make sense. Ranges from 1000-2000 with a stricter range of accessible values for each syringe tool. + :type pulsewidth: int + :param s: Speed of movement in uL/S. + :type s: int + """ + + assert pulsewidth > self.full_position + assert pulsewidth < self.empty_position + + r = requests.post( + self.url + "/set_pulsewidth", + json={"pulsewidth": pulsewidth, "name": self.name, "speed": s}, + ) + + status = self.status() + + return diff --git a/src/science_jubilee/tools/configs/.ipynb_checkpoints/P300_config-checkpoint.json b/src/science_jubilee/tools/configs/.ipynb_checkpoints/P300_config-checkpoint.json new file mode 100644 index 0000000..e26a2a5 --- /dev/null +++ b/src/science_jubilee/tools/configs/.ipynb_checkpoints/P300_config-checkpoint.json @@ -0,0 +1,10 @@ +{ + "brand": "Opentrons OT2", + "model": "P300 Gen 2", + "max_volume": 300, + "min_volume": 20, + "zero_position": 310, + "blowout_position": 350, + "drop_tip_position": 425, + "mm_to_ul": 0.91 +} diff --git a/src/science_jubilee/tools/configs/10cc_syringe.json b/src/science_jubilee/tools/configs/10cc_syringe.json index da11454..92b54b2 100644 --- a/src/science_jubilee/tools/configs/10cc_syringe.json +++ b/src/science_jubilee/tools/configs/10cc_syringe.json @@ -1,8 +1,4 @@ { - "unit" : "milliliters", - "max_volume": 10, - "min_volume": 0, - "min_range": -70, - "max_range": 0, - "mm_to_ml": 7 + "url": "http://192.168.1.5:5000", + "name": "10cc_1" } diff --git a/src/science_jubilee/tools/configs/1cc_1_syringe.json b/src/science_jubilee/tools/configs/1cc_1_syringe.json new file mode 100644 index 0000000..bf99f66 --- /dev/null +++ b/src/science_jubilee/tools/configs/1cc_1_syringe.json @@ -0,0 +1,4 @@ +{ + "url": "http://192.168.1.5:5000", + "name": "1cc_1" +} diff --git a/src/science_jubilee/tools/configs/1cc_2_syringe.json b/src/science_jubilee/tools/configs/1cc_2_syringe.json new file mode 100644 index 0000000..7d6aa06 --- /dev/null +++ b/src/science_jubilee/tools/configs/1cc_2_syringe.json @@ -0,0 +1,4 @@ +{ + "url": "http://192.168.1.5:5000", + "name": "1cc_2" +} diff --git a/src/science_jubilee/tools/configs/1cc_3_syringe.json b/src/science_jubilee/tools/configs/1cc_3_syringe.json new file mode 100644 index 0000000..280803d --- /dev/null +++ b/src/science_jubilee/tools/configs/1cc_3_syringe.json @@ -0,0 +1,4 @@ +{ + "url": "http://192.168.1.5:5000", + "name": "1cc_3" +} diff --git a/src/science_jubilee/tools/configs/P20_config.json b/src/science_jubilee/tools/configs/P20_config.json new file mode 100644 index 0000000..f73a59d --- /dev/null +++ b/src/science_jubilee/tools/configs/P20_config.json @@ -0,0 +1,10 @@ +{ + "brand": "Opentrons OT2", + "model": "P20 Gen 2", + "max_volume": 20, + "min_volume": 1, + "zero_position": 250, + "blowout_position": 290, + "drop_tip_position": 400, + "mm_to_ul": 0.8531 +}