diff --git a/abr-testing/Makefile b/abr-testing/Makefile index d9ec6bdbb31..f711579ff57 100644 --- a/abr-testing/Makefile +++ b/abr-testing/Makefile @@ -12,6 +12,9 @@ package_version = $(call python_package_version,abr-testing,$(project_rs_default wheel_file = dist/$(call python_get_wheelname,abr-testing,$(project_rs_default),$(package_name),$(BUILD_NUMBER)) sdist_file = dist/$(call python_get_sdistname,abr-testing,$(project_rs_default),$(package_name)) +# Find the branch, sha, version that will be used to update the VERSION.json file +version_file = $(call python_get_git_version,abr-testing,$(project_rs_default),abr-testing) + tests ?= tests test_opts ?= @@ -78,3 +81,10 @@ format: .PHONY: test test: @echo "No tests yet" + +.PHONY: push-no-restart-ot3 +push-no-restart-ot3: sdist Pipfile.lock + $(call push-python-sdist,$(host),$(ssh_key),$(ssh_opts),$(sdist_file),/opt/opentrons-robot-server,"abr_testing",,,$(version_file)) + +.PHONY: push-ot3 +push-ot3: push-no-restart-ot3 diff --git a/abr-testing/abr_testing/automation/google_sheets_tool.py b/abr-testing/abr_testing/automation/google_sheets_tool.py index afac386bc40..7e8b4dc0f29 100644 --- a/abr-testing/abr_testing/automation/google_sheets_tool.py +++ b/abr-testing/abr_testing/automation/google_sheets_tool.py @@ -15,6 +15,12 @@ """ +class google_interaction_error(gspread.exceptions.APIError): + """Internal use exception so we don't need to import gspread directly in other projects.""" + + pass + + class google_sheet: """Google Sheets Tool.""" diff --git a/hardware-testing/Makefile b/hardware-testing/Makefile index afe2a57c2ee..f063a31654a 100755 --- a/hardware-testing/Makefile +++ b/hardware-testing/Makefile @@ -159,9 +159,6 @@ test-scripts: test-liquid-sense: $(python) -m hardware_testing.liquid_sense --simulate --pipette 1000 --channels 1 $(python) -m hardware_testing.liquid_sense --simulate --pipette 50 --channels 1 - $(python) -m hardware_testing.liquid_sense --simulate --pipette 1000 --channels 8 - $(python) -m hardware_testing.liquid_sense --simulate --pipette 50 --channels 8 - $(python) -m hardware_testing.liquid_sense --simulate --pipette 1000 --channels 96 .PHONY: test-integration test-integration: test-production-qc test-examples test-scripts test-gravimetric @@ -198,6 +195,11 @@ push-description-ot3: $(python) -c "from hardware_testing.data import create_git_description_file; create_git_description_file()" scp $(ssh_helper_ot3) ./.hardware-testing-description root@$(host):/data/ +.PHONY: push-labware-ot3 +push-labware-ot3: + ssh $(ssh_helper_ot3) root@$(host) "mkdir -p /data/labware/v2/custom_definitions/custom_beta" + scp $(ssh_helper_ot3) -r hardware_testing/labware/* root@$(host):/data/labware/v2/custom_definitions/custom_beta/ + .PHONY: restart restart: $(call restart-service,$(host),$(ssh_key),$(ssh_opts),"opentrons-robot-server") @@ -218,10 +220,10 @@ push-no-restart-ot3: sdist Pipfile.lock $(call push-python-sdist,$(host),$(ssh_key),$(ssh_opts),$(sdist_file),/opt/opentrons-robot-server,"hardware_testing",,,$(version_file)) .PHONY: push-ot3 -push-ot3: push-no-restart-ot3 push-plot-webpage-ot3 push-description-ot3 +push-ot3: push-no-restart-ot3 push-plot-webpage-ot3 push-description-ot3 push-labware-ot3 .PHONY: push-all -push-all: clean wheel push-no-restart push-plot-webpage +push-all: clean wheel push-no-restart push-plot-webpage-ot3 .PHONY: term term: @@ -275,6 +277,16 @@ push-ot3-fixture: $(MAKE) remove-patches-fixture +.PHONY: push-ot3-lld +push-ot3-lld: + $(MAKE) apply-patches-fixture + cd ../ && $(MAKE) -C shared-data push-ot3 + cd ../ && $(MAKE) -C hardware push-ot3 + cd ../ && $(MAKE) -C hardware_testing push-ot3 + cd ../ && $(MAKE) -C api push-ot3 + $(MAKE) remove-patches-fixture + + .PHONY: apply-patches-fixture apply-patches-fixture: cd ../ && git apply ./hardware-testing/fixture_overrides/*.patch --allow-empty @@ -294,8 +306,6 @@ sync-ot3: sync-sw-ot3 sync-fw-ot3 .PHONY: push-ot3-gravimetric push-ot3-gravimetric: $(MAKE) push-ot3 - ssh $(ssh_helper_ot3) root@$(host) "mkdir -p /data/labware/v2/custom_definitions/custom_beta" - scp $(ssh_helper_ot3) -r hardware_testing/labware/* root@$(host):/data/labware/v2/custom_definitions/custom_beta/ $(MAKE) apply-patches-gravimetric cd ../ && $(MAKE) -C shared-data push-ot3 $(MAKE) remove-patches-gravimetric diff --git a/hardware-testing/Pipfile b/hardware-testing/Pipfile index e851331de96..4f9d82e5964 100644 --- a/hardware-testing/Pipfile +++ b/hardware-testing/Pipfile @@ -8,6 +8,7 @@ opentrons = {editable = true, path = "./../api", extras=['flex-hardware']} opentrons-shared-data = {editable = true, path = "./../shared-data/python"} opentrons-hardware = {editable = true, path = "./../hardware", extras=['FLEX']} hardware-testing = { editable = true, path = "." } +abr-testing = { editable = true, path = "./../abr-testing" } pyserial = "==3.5" [dev-packages] diff --git a/hardware-testing/Pipfile.lock b/hardware-testing/Pipfile.lock index cbe473a96c3..400c519bf27 100644 --- a/hardware-testing/Pipfile.lock +++ b/hardware-testing/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "29fd7580a2384b6f20317de4c4869f27ede50315e7d87d75346bfbabd0ef683c" + "sha256": "1e609e94df92fa225c1352401ecd3c21e2c7ec319754ae3f209155553712704f" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,10 @@ ] }, "default": { + "abr-testing": { + "editable": true, + "path": "./../abr-testing" + }, "aionotify": { "hashes": [ "sha256:385e1becfaac2d9f4326673033d53912ef9565b6febdedbec593ee966df392c6", @@ -304,11 +308,11 @@ }, "setuptools": { "hashes": [ - "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987", - "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32" + "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", + "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0" ], "markers": "python_version >= '3.8'", - "version": "==69.5.1" + "version": "==70.0.0" }, "sniffio": { "hashes": [ @@ -320,11 +324,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", - "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8", + "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594" ], "markers": "python_version >= '3.8'", - "version": "==4.11.0" + "version": "==4.12.0" }, "wrapt": { "hashes": [ @@ -484,61 +488,61 @@ }, "coverage": { "hashes": [ - "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de", - "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661", - "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26", - "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41", - "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d", - "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981", - "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2", - "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34", - "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f", - "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a", - "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35", - "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223", - "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1", - "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746", - "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90", - "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c", - "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca", - "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8", - "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596", - "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e", - "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd", - "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e", - "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3", - "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e", - "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312", - "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7", - "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572", - "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428", - "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f", - "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07", - "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e", - "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4", - "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136", - "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5", - "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8", - "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d", - "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228", - "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206", - "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa", - "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e", - "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be", - "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5", - "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668", - "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601", - "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057", - "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146", - "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f", - "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8", - "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7", - "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987", - "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19", - "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece" + "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523", + "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f", + "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d", + "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb", + "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0", + "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c", + "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98", + "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83", + "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8", + "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7", + "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac", + "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84", + "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb", + "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3", + "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884", + "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614", + "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd", + "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807", + "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd", + "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8", + "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc", + "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db", + "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0", + "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08", + "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232", + "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d", + "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a", + "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1", + "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286", + "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303", + "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341", + "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84", + "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45", + "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc", + "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec", + "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd", + "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155", + "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52", + "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d", + "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485", + "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31", + "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d", + "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d", + "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d", + "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85", + "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce", + "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb", + "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974", + "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24", + "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56", + "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9", + "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35" ], "markers": "python_version >= '3.8'", - "version": "==7.5.1" + "version": "==7.5.3" }, "flake8": { "hashes": [ @@ -762,11 +766,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", - "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8", + "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594" ], "markers": "python_version >= '3.8'", - "version": "==4.11.0" + "version": "==4.12.0" }, "urllib3": { "hashes": [ diff --git a/hardware-testing/hardware_testing/liquid_sense/__main__.py b/hardware-testing/hardware_testing/liquid_sense/__main__.py index eb118c2edff..b1fc67a2d2e 100644 --- a/hardware-testing/hardware_testing/liquid_sense/__main__.py +++ b/hardware-testing/hardware_testing/liquid_sense/__main__.py @@ -8,6 +8,7 @@ import os from typing import List, Any, Optional import traceback +import sys from hardware_testing.opentrons_api import helpers_ot3 from hardware_testing.gravimetric import helpers, workarounds @@ -31,30 +32,51 @@ from hardware_testing.liquid_sense import execute from .report import build_ls_report, store_config, store_serial_numbers -from .post_process import process_csv_directory +from .post_process import process_csv_directory, process_google_sheet from hardware_testing.protocols.liquid_sense_lpc import ( - liquid_sense_ot3_p50_single, - liquid_sense_ot3_p50_multi, - liquid_sense_ot3_p1000_single, - liquid_sense_ot3_p1000_multi, - liquid_sense_ot3_p1000_96, + liquid_sense_ot3_p50_single_vial, + liquid_sense_ot3_p1000_single_vial, ) +try: + from abr_testing.automation import google_sheets_tool +except ImportError: + ui.print_error( + "Unable to import abr repo if this isn't a simulation push the abr_testing package" + ) + from . import google_sheets_tool # type: ignore[no-redef] + + pass + +CREDENTIALS_PATH = "/var/lib/jupyter/notebooks/abr.json" + API_LEVEL = "2.18" LABWARE_OFFSETS: List[LabwareOffset] = [] +# NOTE: (sigler) plunger on 1ch/8ch won't move faster than ~20mm second +# which means it take ~3.5 seconds to reach full plunger travel. +# Therefore, there is no need for any probing in this test script to +# take longer than 3.5 seconds. +# NOTE: (sigler) configuring the starting height of each probing sequence +# not based on millimeters but instead on the number seconds it takes +# before the tip contacts the meniscus will help make sure that adjusting +# the Z-speed will inadvertently affect the pressure's rate-of-change +# (which could happen if the meniscus seal is formed at wildly different +# positions along the plunger travel). +MAX_PROBE_SECONDS = 3.5 + LIQUID_SENSE_CFG = { 50: { - 1: liquid_sense_ot3_p50_single, - 8: liquid_sense_ot3_p50_multi, + 1: liquid_sense_ot3_p50_single_vial, + 8: None, }, 1000: { - 1: liquid_sense_ot3_p1000_single, - 8: liquid_sense_ot3_p1000_multi, - 96: liquid_sense_ot3_p1000_96, + 1: liquid_sense_ot3_p1000_single_vial, + 8: None, + 96: None, }, } @@ -91,7 +113,7 @@ class RunArgs: ctx: ProtocolContext protocol_cfg: Any test_report: CSVReport - start_height_offset: float + probe_seconds_before_contact: float aspirate: bool dial_indicator: Optional[mitutoyo_digimatic_indicator.Mitutoyo_Digimatic_Indicator] plunger_speed: float @@ -141,15 +163,13 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": """Build.""" _ctx = RunArgs._get_protocol_context(args) run_id, start_time = create_run_id_and_start_time() - environment_sensor = asair_sensor.BuildAsairSensor( - _ctx.is_simulating() or args.ignore_env - ) + environment_sensor = asair_sensor.BuildAsairSensor(simulate=True) git_description = get_git_description() protocol_cfg = LIQUID_SENSE_CFG[args.pipette][args.channels] - name = protocol_cfg.metadata["protocolName"] # type: ignore[attr-defined] + name = protocol_cfg.metadata["protocolName"] # type: ignore[union-attr] ui.print_header("LOAD PIPETTE") pipette = _ctx.load_instrument( - f"flex_{args.channels}channel_{args.pipette}", "left" + f"flex_{args.channels}channel_{args.pipette}", args.mount ) loaded_labwares = _ctx.loaded_labwares if 12 in loaded_labwares.keys(): @@ -159,13 +179,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": pipette.trash_container = trash pipette_tag = helpers._get_tag_from_pipette(pipette, False, False) - if args.trials == 0: - if args.channels < 96: - trials = 10 - else: - trials = 7 - else: - trials = args.trials + trials = args.trials if args.tip == 0: if args.pipette == 1000: @@ -175,17 +189,17 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": else: tip_volumes = [args.tip] - scale = Scale.build(simulate=_ctx.is_simulating() or args.ignore_scale) + scale = Scale.build(simulate=True) recorder: GravimetricRecorder = execute._load_scale( name, scale, run_id, pipette_tag, start_time, - _ctx.is_simulating() or args.ignore_scale, + simulating=True, ) dial: Optional[mitutoyo_digimatic_indicator.Mitutoyo_Digimatic_Indicator] = None - if not _ctx.is_simulating() and not args.ignore_dial: + if not _ctx.is_simulating(): dial_port = list_ports_and_select("Dial Indicator") dial = mitutoyo_digimatic_indicator.Mitutoyo_Digimatic_Indicator( port=dial_port @@ -209,11 +223,11 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": args.pipette, tip_volumes, trials, - args.plunger_direction, + "aspirate" if args.aspirate else "dispense", args.liquid, - protocol_cfg.LABWARE_ON_SCALE, # type: ignore[attr-defined] + protocol_cfg.LABWARE_ON_SCALE, # type: ignore[union-attr] args.z_speed, - args.start_height_offset, + args.probe_seconds_before_contact, ) return RunArgs( tip_volumes=tip_volumes, @@ -232,8 +246,8 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": ctx=_ctx, protocol_cfg=protocol_cfg, test_report=report, - start_height_offset=args.start_height_offset, - aspirate=args.plunger_direction == "aspirate", + probe_seconds_before_contact=args.probe_seconds_before_contact, + aspirate=args.aspirate, dial_indicator=dial, plunger_speed=args.plunger_speed, trials_before_jog=args.trials_before_jog, @@ -245,63 +259,83 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": parser = argparse.ArgumentParser("Pipette Testing") parser.add_argument("--simulate", action="store_true") parser.add_argument("--pipette", type=int, choices=[50, 1000], required=True) + parser.add_argument("--mount", type=str, choices=["left", "right"], default="left") parser.add_argument("--channels", type=int, choices=[1, 8, 96], default=1) parser.add_argument("--tip", type=int, choices=[0, 50, 200, 1000], default=0) - parser.add_argument("--trials", type=int, default=0) + parser.add_argument("--probe-seconds-before-contact", type=float, default=1.0) parser.add_argument("--return-tip", action="store_true") - parser.add_argument("--skip-labware-offsets", action="store_true") - parser.add_argument( - "--liquid", type=str, choices=["water", "glycerol", "alchohol"], default="water" - ) - parser.add_argument("--z-speed", type=float, default=5) - parser.add_argument( - "--plunger-direction", - type=str, - choices=["aspirate", "dispense"], - default="aspirate", - ) - parser.add_argument("--labware-type", type=str, default="nest_1_reservoir_195ml") + parser.add_argument("--trials", type=int, default=7) + parser.add_argument("--trials-before-jog", type=int, default=7) + parser.add_argument("--z-speed", type=float, default=1) + parser.add_argument("--aspirate", action="store_true") parser.add_argument("--plunger-speed", type=float, default=-1.0) - parser.add_argument("--isolate-plungers", action="store_true") - parser.add_argument("--start-height-offset", type=float, default=0) - parser.add_argument("--ignore-scale", action="store_true") - parser.add_argument("--ignore-env", action="store_true") - parser.add_argument("--ignore-dial", action="store_true") - parser.add_argument("--trials-before-jog", type=int, default=10) parser.add_argument("--multi-passes", type=int, default=1) + parser.add_argument("--starting-tip", type=str, default="A1") + parser.add_argument("--google-sheet-name", type=str, default="LLD-Shared-Data") + parser.add_argument( + "--gd-parent-folder", type=str, default="1b2V85fDPA0tNqjEhyHOGCWRZYgn8KsGf" + ) + parser.add_argument("--liquid", type=str, default="unknown") + parser.add_argument("--skip-labware-offsets", action="store_true") args = parser.parse_args() + + assert ( + 0.0 < args.probe_seconds_before_contact <= MAX_PROBE_SECONDS + ), f"'--probe-seconds-before-contact' must be between 0.0-{MAX_PROBE_SECONDS}" run_args = RunArgs.build_run_args(args) - exit_error = os.EX_OK + exit_error = 0 + serial_logger: Optional[subprocess.Popen] = None + data_dir = get_testing_data_directory() + data_file = f"/{data_dir}/{run_args.name}/{run_args.run_id}/serial.log" try: if not run_args.ctx.is_simulating(): - data_dir = get_testing_data_directory() - data_file = f"/{data_dir}/{run_args.name}/{run_args.run_id}/serial.log" ui.print_info(f"logging can data to {data_file}") serial_logger = subprocess.Popen( [f"python3 -m opentrons_hardware.scripts.can_mon > {data_file}"], shell=True, ) sleep(1) + # Connect to Google Sheet + ui.print_info(f"robot has credentials: {os.path.exists(CREDENTIALS_PATH)}") + google_sheet: Optional[ + google_sheets_tool.google_sheet + ] = google_sheets_tool.google_sheet( + CREDENTIALS_PATH, args.google_sheet_name, 0 + ) + sheet_id = google_sheet.create_worksheet(run_args.run_id) # type: ignore[union-attr] + try: + sys.path.insert(0, "/var/lib/jupyter/notebooks/") + import google_drive_tool # type: ignore[import] + + google_drive: Optional[ + google_drive_tool.google_drive + ] = google_drive_tool.google_drive( + CREDENTIALS_PATH, + args.gd_parent_folder, + "rhyann.clarke@opentrons.com", + ) + except ImportError: + raise ImportError( + "Run on robot. Make sure google_drive_tool.py is in jupyter notebook." + ) + else: + google_sheet = None + sheet_id = None + google_drive = None hw = run_args.ctx._core.get_hardware() - if not run_args.ctx.is_simulating(): - ui.get_user_ready("CLOSE the door, and MOVE AWAY from machine") ui.print_info("homing...") run_args.ctx.home() for tip in run_args.tip_volumes: - if args.channels == 96 and not run_args.ctx.is_simulating(): - ui.alert_user_ready(f"prepare the {tip}ul tipracks", hw) - execute.run(tip, run_args) + execute.run(tip, run_args, google_sheet, sheet_id, args.starting_tip) except Exception as e: - ui.print_info(f"got error {e}") - ui.print_info(traceback.format_exc()) + ui.print_error(f"got error {e}") + ui.print_error(traceback.format_exc()) exit_error = 1 finally: if run_args.recorder is not None: ui.print_info("ending recording") - run_args.recorder.stop() - run_args.recorder.deactivate() - if not run_args.ctx.is_simulating(): + if not run_args.ctx.is_simulating() and serial_logger: ui.print_info("killing serial log") serial_logger.terminate() if run_args.dial_indicator is not None: @@ -310,11 +344,43 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": run_args.test_report.print_results() ui.print_info("done\n\n") if not run_args.ctx.is_simulating(): + new_folder_name = ( + f"MS{args.z_speed}_PS{args.plunger_speed}_{run_args.run_id}" + ) process_csv_directory( f"{data_dir}/{run_args.name}/{run_args.run_id}", run_args.tip_volumes, run_args.trials, + google_sheet, + google_drive, + run_args.run_id, + sheet_id, + new_folder_name, + make_graph=True, ) + # Log to Google Sheet + if args.aspirate is False: + plunger_direction = "dispense" + else: + plunger_direction = "aspirate" + test_info = [ + run_args.run_id, + run_args.pipette_tag, + args.pipette, + args.tip, + args.z_speed, + args.plunger_speed, + "threshold", + plunger_direction, + ] + try: + process_google_sheet(google_sheet, run_args, test_info, sheet_id) + except Exception as e: + ui.print_error("error making graphs or logging data on google sheet") + ui.print_error(f"got error {e}") + ui.print_error(traceback.format_exc()) + exit_error = 2 + run_args.ctx.cleanup() if not args.simulate: helpers_ot3.restart_server_ot3() diff --git a/hardware-testing/hardware_testing/liquid_sense/execute.py b/hardware-testing/hardware_testing/liquid_sense/execute.py index 05be865015f..53193c8ad8a 100644 --- a/hardware-testing/hardware_testing/liquid_sense/execute.py +++ b/hardware-testing/hardware_testing/liquid_sense/execute.py @@ -28,6 +28,16 @@ from opentrons_shared_data.errors.exceptions import LiquidNotFoundError +try: + from abr_testing.automation import google_sheets_tool +except ImportError: + ui.print_error( + "Unable to import abr repo if this isn't a simulation push the abr_testing package" + ) + from . import google_sheets_tool # type: ignore[no-redef] + + pass + PROBE_MAX_TIME: Dict[int, float] = { 1: 2.75, @@ -165,17 +175,29 @@ def _load_scale( return recorder -def run(tip: int, run_args: RunArgs) -> None: +def run( + tip: int, + run_args: RunArgs, + google_sheet: Optional[google_sheets_tool.google_sheet], + sheet_id: Optional[str], + starting_tip: str = "A1", +) -> None: """Run a liquid probe test.""" test_labware: Labware = _load_test_well(run_args) dial_indicator: Labware = _load_dial_indicator(run_args) dial_well: Well = dial_indicator["A1"] + liquid_height: float = 0.0 + liquid_height_from_deck: float = 0.0 hw_api = get_sync_hw_api(run_args.ctx) test_well: Well = test_labware["A1"] _load_tipracks(run_args.ctx, run_args.pipette_channels, run_args.protocol_cfg, tip) tips: List[Well] = get_unused_tips( ctx=run_args.ctx, tip_volume=tip, pipette_mount="" ) + row = "ABCDEFGH".index(starting_tip[0]) + column = int(starting_tip[1:]) - 1 + num_of_tips_to_skip = (column * 8) + row + del tips[:num_of_tips_to_skip] assert len(tips) >= run_args.trials results: List[float] = [] adjusted_results: List[float] = [] @@ -193,100 +215,119 @@ def _get_tip_offset() -> float: run_args.pipette._retract() return tip_offset - def _get_target_height() -> float: + def _get_target_height() -> None: + nonlocal liquid_height, liquid_height_from_deck run_args.pipette.pick_up_tip(tips[0]) del tips[: run_args.pipette_channels] liquid_height = _jog_to_find_liquid_height( run_args.ctx, run_args.pipette, test_well ) - target_height = test_well.bottom(liquid_height).point.z + liquid_height_from_deck = test_well.bottom(liquid_height).point.z run_args.pipette._retract() - return target_height - target_height = _get_target_height() + _get_target_height() tip_offset = _get_tip_offset() if run_args.return_tip: run_args.pipette.return_tip() else: run_args.pipette.drop_tip() + run_args.pipette._retract() env_data = run_args.environment_sensor.get_reading() store_baseline_trial( run_args.test_report, tip, - target_height, + liquid_height_from_deck, env_data.relative_humidity, env_data.temperature, - test_well.top().point.z - target_height, + test_well.top().point.z - liquid_height_from_deck, tip_offset - lpc_offset, + google_sheet, + run_args.run_id, ) trials_before_jog = run_args.trials_before_jog - for trial in range(run_args.trials): - if trial > 0 and trial % trials_before_jog == 0: - target_height = _get_target_height() + try: + for trial in range(run_args.trials): + if trial > 0 and trial % trials_before_jog == 0: + _get_target_height() + if run_args.return_tip: + run_args.pipette.return_tip() + else: + run_args.pipette.drop_tip() + run_args.pipette._retract() + + ui.print_info(f"Picking up {tip}ul tip") + run_args.pipette.pick_up_tip(tips[0]) + del tips[: run_args.pipette_channels] + # operator defines num of seconds btwn start of the probe movement + # and meniscus contact calculating ideal starting position is then + # easy bc no acceleration is involved during probe + starting_mm_above_liquid = ( + run_args.probe_seconds_before_contact * run_args.z_speed + ) + starting_mount_height = ( + test_well.bottom(z=liquid_height).point.z + starting_mm_above_liquid + ) + run_args.pipette.move_to( + test_well.bottom(z=(liquid_height + starting_mm_above_liquid)) + ) + start_pos = hw_api.current_position_ot3(OT3Mount.LEFT) + height = _run_trial(run_args, tip, test_well, trial, starting_mount_height) + end_pos = hw_api.current_position_ot3(OT3Mount.LEFT) + run_args.pipette.blow_out() + tip_length_offset = 0.0 + if run_args.dial_indicator is not None: + run_args.pipette._retract() + run_args.pipette.move_to(dial_well.top()) + tip_length_offset = tip_offset - run_args.dial_indicator.read_stable() + run_args.pipette._retract() + ui.print_info(f"Tip Offset {tip_length_offset}") + + ui.print_info("Dropping tip") if run_args.return_tip: run_args.pipette.return_tip() else: run_args.pipette.drop_tip() - - ui.print_info(f"Picking up {tip}ul tip") - run_args.pipette.pick_up_tip(tips[0]) - del tips[: run_args.pipette_channels] - run_args.pipette.move_to(test_well.top()) - - start_pos = hw_api.current_position_ot3(OT3Mount.LEFT) - height = _run_trial(run_args, tip, test_well, trial) - end_pos = hw_api.current_position_ot3(OT3Mount.LEFT) - run_args.pipette.blow_out() - tip_length_offset = 0.0 - if run_args.dial_indicator is not None: - run_args.pipette._retract() - run_args.pipette.move_to(dial_well.top()) - tip_length_offset = tip_offset - run_args.dial_indicator.read_stable() - run_args.pipette._retract() - ui.print_info(f"Tip Offset {tip_length_offset}") - - ui.print_info("Droping tip") - if run_args.return_tip: - run_args.pipette.return_tip() - else: - run_args.pipette.drop_tip() - results.append(height) - adjusted_results.append(height + tip_length_offset) - env_data = run_args.environment_sensor.get_reading() - hw_pipette = hw_api.hardware_pipettes[top_types.Mount.LEFT] - plunger_start = ( - hw_pipette.plunger_positions.bottom - if run_args.aspirate - else hw_pipette.plunger_positions.top - ) - store_trial( - run_args.test_report, - trial, - tip, - height, - end_pos[Axis.P_L], - env_data.relative_humidity, - env_data.temperature, - start_pos[Axis.Z_L] - end_pos[Axis.Z_L], - plunger_start - end_pos[Axis.P_L], - tip_length_offset, - target_height, - ) - ui.print_info( - f"\n\n Z axis start pos {start_pos[Axis.Z_L]} end pos {end_pos[Axis.Z_L]}" - ) - ui.print_info( - f"plunger start pos {plunger_start} end pos {end_pos[Axis.P_L]}\n\n" - ) - - ui.print_info(f"RESULTS: \n{results}") - ui.print_info(f"Adjusted RESULTS: \n{adjusted_results}") - store_tip_results(run_args.test_report, tip, results, adjusted_results) + run_args.pipette._retract() + results.append(height) + adjusted_results.append(height + tip_length_offset) + env_data = run_args.environment_sensor.get_reading() + hw_pipette = hw_api.hardware_pipettes[top_types.Mount.LEFT] + plunger_start = ( + hw_pipette.plunger_positions.bottom + if run_args.aspirate + else hw_pipette.plunger_positions.top + ) + store_trial( + run_args.test_report, + trial, + tip, + height, + end_pos[Axis.P_L], + env_data.relative_humidity, + env_data.temperature, + start_pos[Axis.Z_L] - height, + plunger_start - end_pos[Axis.P_L], + tip_length_offset, + liquid_height_from_deck, + google_sheet, + run_args.run_id, + sheet_id, + ) + ui.print_info( + f"\n\n Z axis start pos {start_pos[Axis.Z_L]} end pos {end_pos[Axis.Z_L]}" + ) + ui.print_info( + f"plunger start pos {plunger_start} end pos {end_pos[Axis.P_L]}\n\n" + ) + finally: + ui.print_info(f"RESULTS: \n{results}") + ui.print_info(f"Adjusted RESULTS: \n{adjusted_results}") + store_tip_results(run_args.test_report, tip, results, adjusted_results) def get_plunger_travel(run_args: RunArgs) -> float: @@ -299,7 +340,11 @@ def get_plunger_travel(run_args: RunArgs) -> float: def find_max_z_distances( - run_args: RunArgs, tip: int, well: Well, p_speed: float + run_args: RunArgs, + tip: int, + well: Well, + p_speed: float, + starting_mount_height: float, ) -> List[float]: """Returns a list of max z distances for each probe. @@ -310,11 +355,14 @@ def find_max_z_distances( truncated to avoid collisions. """ z_speed = run_args.z_speed - max_z_distance = well.depth + run_args.start_height_offset + max_z_distance = starting_mount_height - well.bottom().point.z plunger_travel = get_plunger_travel(run_args) - p_travel_time = min( - plunger_travel / p_speed, PROBE_MAX_TIME[run_args.pipette_channels] - ) + if p_speed == 0: + p_travel_time = PROBE_MAX_TIME[run_args.pipette_channels] + else: + p_travel_time = min( + plunger_travel / p_speed, PROBE_MAX_TIME[run_args.pipette_channels] + ) z_travels: List[float] = [] while max_z_distance > 0: @@ -324,7 +372,9 @@ def find_max_z_distances( return z_travels -def _run_trial(run_args: RunArgs, tip: int, well: Well, trial: int) -> float: +def _run_trial( + run_args: RunArgs, tip: int, well: Well, trial: int, starting_mount_height: float +) -> float: hw_api = get_sync_hw_api(run_args.ctx) lqid_cfg: Dict[str, int] = LIQUID_PROBE_SETTINGS[run_args.pipette_volume][ run_args.pipette_channels @@ -348,9 +398,12 @@ def _run_trial(run_args: RunArgs, tip: int, well: Well, trial: int) -> float: else run_args.plunger_speed ) - z_distances: List[float] = find_max_z_distances(run_args, tip, well, plunger_speed) + start_height = starting_mount_height + height = 2 * start_height + z_distances: List[float] = find_max_z_distances( + run_args, tip, well, plunger_speed, starting_mount_height + ) z_distances = z_distances[: run_args.multi_passes] - start_height = well.top().point.z + run_args.start_height_offset for z_dist in z_distances: lps = LiquidProbeSettings( starting_mount_height=start_height, diff --git a/hardware-testing/hardware_testing/liquid_sense/google_sheets_tool.py b/hardware-testing/hardware_testing/liquid_sense/google_sheets_tool.py new file mode 100644 index 00000000000..215b5ae7c52 --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/google_sheets_tool.py @@ -0,0 +1,13 @@ +"""Dummy google_sheets_tool object.""" + + +class google_interaction_error(Exception): + """Dummy exception.""" + + pass + + +class google_sheet: + """Dummy class.""" + + pass diff --git a/hardware-testing/hardware_testing/liquid_sense/post_process.py b/hardware-testing/hardware_testing/liquid_sense/post_process.py index a5b7c7f47cb..680f8c7cdc4 100644 --- a/hardware-testing/hardware_testing/liquid_sense/post_process.py +++ b/hardware-testing/hardware_testing/liquid_sense/post_process.py @@ -1,8 +1,22 @@ """Post process script csvs.""" import csv +import math import os -from typing import List, Dict, Tuple -from math import isclose +import statistics +import traceback +from typing import List, Dict, Tuple, Any, Optional + +from hardware_testing.data import ui + +try: + from abr_testing.automation import google_sheets_tool +except ImportError: + ui.print_error( + "Unable to import abr repo if this isn't a simulation push the abr_testing package" + ) + from . import google_sheets_tool # type: ignore[no-redef] + + pass COL_TRIAL_CONVERSION = { 1: "E", @@ -41,7 +55,15 @@ def _get_pressure_results(result_file: str) -> Tuple[float, float, float, List[f def process_csv_directory( # noqa: C901 - data_directory: str, tips: List[int], trials: int, make_graph: bool = False + data_directory: str, + tips: List[int], + trials: int, + google_sheet: Optional[Any], + google_drive: Optional[Any], + sheet_name: str, + sheet_id: Optional[str], + new_folder_name: Optional[str], + make_graph: bool = False, ) -> None: """Post process script csvs.""" csv_files: List[str] = os.listdir(data_directory) @@ -148,7 +170,17 @@ def process_csv_directory( # noqa: C901 f"p_travel T{i+1}", ] ) - + # Add header to google sheet + if google_sheet: + try: + pressure_header_for_google_sheet = [ + [x] for x in pressure_header_row + ] + google_sheet.batch_update_cells( + pressure_header_for_google_sheet, "H", 10, sheet_id + ) + except google_sheets_tool.google_interaction_error: + ui.print_error("Header did not write on google sheet.") # we want to line up the z height's of each trial at time==0 # to do this we drop the results at the beginning of each of the trials # except for one with the longest tip (lower tip offset are longer tips) @@ -185,9 +217,10 @@ def process_csv_directory( # noqa: C901 meniscus_time = (meniscus_travel + min_tip_offset) / results_settings[ tip ][0][0] + pressure_rows = [] for i in range(max_results_len): pressure_row: List[str] = [f"{time}"] - if isclose( + if math.isclose( time, meniscus_time, rel_tol=0.001, @@ -209,8 +242,289 @@ def process_csv_directory( # noqa: C901 f"{abs(results_settings[tip][trial][1]) * time + p_offsets[tip][trial]}" ) final_report_writer.writerow(pressure_row) + # Add pressure to google sheet + pressure_rows.append(pressure_row) time += 0.001 + if google_sheet: + transposed_pressure_rows = list(map(list, zip(*pressure_rows))) + try: + google_sheet.batch_update_cells( + sheet_name, transposed_pressure_rows, "H", 11, sheet_id + ) + except google_sheets_tool.google_interaction_error: + ui.print_error("Did not write pressure data to google sheet.") + if google_drive: + new_folder_id = google_drive.create_folder(new_folder_name) + google_drive.upload_file(final_report_file, new_folder_id) + + +def process_google_sheet( + google_sheet: Optional[Any], + run_args: Any, + test_info: List, + sheet_id: Optional[str], +) -> None: + """Write results and graphs to google sheet.""" + if not google_sheet: + return + sheet_name = run_args.run_id # type: ignore[attr-defined] + test_parameters = [ + [ + "Run ID", + "Serial Number", + "Pipette Type", + "Tip Size", + "Z Speed (mm/s)", + "Plunger Speed (mm/s)", + "Threshold (pascal)", + "Direction", + "Target Height (mm)", + ], + test_info, + ] + num_of_trials = run_args.trials # type: ignore[attr-defined] + google_sheet.batch_update_cells(sheet_name, test_parameters, "A", 1, sheet_id) + target_height = google_sheet.get_cell(sheet_name, "B9") + ui.print_info(target_height) + last_trial_row = 10 + num_of_trials + adjusted_height_range = "E11:E" + str(last_trial_row) + adjusted_height = google_sheet.get_single_col_range( + sheet_name, adjusted_height_range + ) + normalized_height = [ + float(height) - float(target_height) for height in adjusted_height + ] + google_sheet.batch_update_cells(sheet_name, [normalized_height], "F", 11, sheet_id) + # Find accuracy, precision, repeatability + try: + accuracy = statistics.mean(normalized_height) + precision = (max(normalized_height) - min(normalized_height)) / 2 + repeatability_error = statistics.stdev(normalized_height) / math.sqrt( + len(normalized_height) + ) + summary = [ + ["Accuracy (mm)", "Precision (+/- mm)", "Repeatability (%)"], + [accuracy, precision, 100.0 - 100.0 * repeatability_error], + ] + google_sheet.batch_update_cells(sheet_name, summary, "D", 2, sheet_id) + except google_sheets_tool.google_interaction_error: + ui.print_error("stats didn't work.") + + # Create Graphs + # 1. Create pressure vs time graph zoomed out + titles = ["Pressure vs Time", "Time (s)", "Pressure (P)", ""] + axis_pressure_vs_time = [ + {"position": "BOTTOM_AXIS", "title": titles[1]}, + {"position": "LEFT_AXIS", "title": titles[2]}, + {"position": "RIGHT_AXIS", "title": titles[3]}, + ] + # TODO: Create less hard coded zoom in + ui.print_info("starting to make graphs") + domains_pressure = [ + { + "domain": { + "sourceRange": { + "sources": [ + { + "sheetId": sheet_id, + "startRowIndex": 9, + "endRowIndex": 1494, + "startColumnIndex": 7, + "endColumnIndex": 8, + } + ] + } + } + } + ] + series_pressure = [] + for i in range(num_of_trials): + series_dict = { + "series": { + "sourceRange": { + "sources": [ + { + "sheetId": sheet_id, + "startRowIndex": 9, + "endRowIndex": 1494, + "startColumnIndex": 9 + 4 * i, + "endColumnIndex": 10 + 4 * i, + } + ] + } + }, + "targetAxis": "LEFT_AXIS", + } + series_pressure.append(series_dict) + try: + google_sheet.create_line_chart( + titles, + series_pressure, + domains_pressure, + axis_pressure_vs_time, + 0, + sheet_id, + ) + except Exception as e: + ui.print_error("did not make pressure vs time graph.") + ui.print_error(f"got error {e}") + ui.print_error(traceback.format_exc()) + + # 2. Height vs Offset Comparison + heights_range = "B11:B" + str(last_trial_row) + heights = google_sheet.get_single_col_range(sheet_name, heights_range) + axis = [ + {"position": "BOTTOM_AXIS", "title": titles[1]}, + { + "position": "LEFT_AXIS", + "title": titles[2], + "viewWindowOptions": { + "viewWindowMin": float(min(heights)) - 1, + "viewWindowMax": float(max(heights)) + 1, + }, + }, + {"position": "RIGHT_AXIS", "title": titles[3]}, + ] + domain_trials = [ + { + "domain": { + "sourceRange": { + "sources": [ + { + "sheetId": sheet_id, + "startRowIndex": 9, + "endRowIndex": last_trial_row, + "startColumnIndex": 0, + "endColumnIndex": 1, + } + ] + } + } + } + ] + series_offsets = [ + { + "series": { + "sourceRange": { + "sources": [ + { + "sheetId": sheet_id, + "startRowIndex": 9, + "endRowIndex": last_trial_row, + "startColumnIndex": 1, + "endColumnIndex": 2, + } + ] + } + }, + "targetAxis": "LEFT_AXIS", + "lineStyle": {"type": "MEDIUM_DASHED"}, + "pointStyle": {"size": 5}, + }, + { + "series": { + "sourceRange": { + "sources": [ + { + "sheetId": sheet_id, + "startRowIndex": 9, + "endRowIndex": last_trial_row, + "startColumnIndex": 3, + "endColumnIndex": 4, + } + ] + } + }, + "targetAxis": "RIGHT_AXIS", + "lineStyle": {"type": "MEDIUM_DASHED"}, + "pointStyle": {"size": 5}, + }, + ] + titles = [ + "Height & Offset Comparison", + "Trials", + "Measured Height (mm)", + "Tip Length Offset (mm)", + ] + try: + google_sheet.create_line_chart( + titles, series_offsets, domain_trials, axis, 14, sheet_id + ) + except Exception as e: + ui.print_error("did not make height vs offset graph.") + ui.print_error(f"got error {e}") + ui.print_error(traceback.format_exc()) + + # 3. Liquid Level Detection + lld_titles = ["Liquid Level Detection", "Trials", "Normalized Height", ""] + series_normalized_height = [ + { + "series": { + "sourceRange": { + "sources": [ + { + "sheetId": sheet_id, + "startRowIndex": 9, + "endRowIndex": last_trial_row, + "startColumnIndex": 5, + "endColumnIndex": 6, + } + ] + } + }, + "targetAxis": "LEFT_AXIS", + "lineStyle": {"type": "MEDIUM_DASHED"}, + "pointStyle": {"size": 5}, + } + ] + normalized_axis = [ + {"position": "BOTTOM_AXIS", "title": titles[1]}, + { + "position": "LEFT_AXIS", + "title": titles[2], + "viewWindowOptions": { + "viewWindowMin": float(min(normalized_height)) - 0.5, + "viewWindowMax": float(max(normalized_height)) + 0.5, + }, + }, + {"position": "RIGHT_AXIS", "title": titles[3]}, + ] + try: + google_sheet.create_line_chart( + lld_titles, + series_normalized_height, + domain_trials, + normalized_axis, + 21, + sheet_id, + ) + except Exception as e: + ui.print_error("did not make lld graph.") + ui.print_error(f"got error {e}") + ui.print_error(traceback.format_exc()) + + # TODO: create a better way to zoom into graph based on slope change + axis_zoomed = [ + { + "position": "BOTTOM_AXIS", + "title": titles[1], + "viewWindowOptions": {"viewWindowMin": 0.75, "viewWindowMax": 1.5}, + }, + {"position": "LEFT_AXIS", "title": titles[2]}, + {"position": "RIGHT_AXIS", "title": titles[3]}, + ] + titles_zoomed = ["Pressure vs Time Zoomed", "Time (s)", "Pressure (P)", ""] + try: + google_sheet.create_line_chart( + titles_zoomed, series_pressure, domains_pressure, axis_zoomed, 7, sheet_id + ) + except Exception as e: + ui.print_error("did not make zoomed in pressure chart.") + ui.print_error(f"got error {e}") + ui.print_error(traceback.format_exc()) + -if __name__ == "__main__": - process_csv_directory("/home/ryan/testdata", [50], 10) +# +# if __name__ == "__main__": +# process_csv_directory("/home/ryan/testdata", [50], 10) diff --git a/hardware-testing/hardware_testing/liquid_sense/report.py b/hardware-testing/hardware_testing/liquid_sense/report.py index 84d5141fd8b..ba7332077a6 100644 --- a/hardware-testing/hardware_testing/liquid_sense/report.py +++ b/hardware-testing/hardware_testing/liquid_sense/report.py @@ -1,13 +1,27 @@ """Format the csv report for a liquid-sense run.""" import statistics +from typing import List, Union, Optional + +from hardware_testing.data import ui + +try: + from abr_testing.automation import google_sheets_tool +except ImportError: + ui.print_error( + "Unable to import abr repo if this isn't a simulation push the abr_testing package" + ) + from . import google_sheets_tool # type: ignore[no-redef] + + pass + + from hardware_testing.data.csv_report import ( CSVReport, CSVSection, CSVLine, CSVLineRepeating, ) -from typing import List, Union """ CSV Test Report: @@ -62,7 +76,7 @@ def build_config_section() -> CSVSection: CSVLine("liquid", [str]), CSVLine("labware_type", [str]), CSVLine("speed", [str]), - CSVLine("start_height_offset", [str]), + CSVLine("probe_seconds_before_contact", [str]), ], ) @@ -137,7 +151,7 @@ def store_config( liquid: str, labware_type: str, speed: str, - start_height_offset: str, + probe_seconds_before_contact: str, ) -> None: """Report config.""" report("CONFIG", "protocol_name", [protocol_name]) @@ -152,7 +166,7 @@ def store_config( report("CONFIG", "liquid", [liquid]) report("CONFIG", "labware_type", [labware_type]) report("CONFIG", "speed", [speed]) - report("CONFIG", "start_height_offset", [start_height_offset]) + report("CONFIG", "probe_seconds_before_contact", [probe_seconds_before_contact]) def store_baseline_trial( @@ -163,8 +177,15 @@ def store_baseline_trial( temp: float, z_travel: float, measured_error: float, + google_sheet: Optional[google_sheets_tool.google_sheet], + sheet_title: str, ) -> None: """Report Trial.""" + if google_sheet: + try: + google_sheet.update_cell(sheet_title, 9, 2, height) + except google_sheets_tool.google_interaction_error: + ui.print_error("did not store baseline trial on google sheet.") report( "TRIALS", f"trial-baseline-{tip}ul", @@ -193,6 +214,9 @@ def store_trial( plunger_travel: float, tip_length_offset: float, target_height: float, + google_sheet: Optional[google_sheets_tool.google_sheet], + sheet_name: str, + sheet_id: Optional[str], ) -> None: """Report Trial.""" report( @@ -210,6 +234,32 @@ def store_trial( target_height, ], ) + if google_sheet is not None and sheet_id is not None: + # Write trial to google sheet + if trial == 0: + # Write header + gs_header: List[List[str]] = [ + ["Trial"], + ["Height"], + ["Plunger Position"], + ["Tip Length Offset"], + ["Adjusted Height"], + ["Normalized Height"], + ] + google_sheet.batch_update_cells(gs_header, "A", 10, sheet_id) + try: + trial_for_google_sheet: List[List[str]] = [ + [f"{trial + 1}"], + [f"{height}"], + [f"{plunger_pos}"], + [f"{tip_length_offset}"], + [f"{height + tip_length_offset}"], + ] + google_sheet.batch_update_cells( + trial_for_google_sheet, "A", 11 + int(trial), sheet_id + ) + except google_sheets_tool.google_interaction_error: + ui.print_error(f"did not log trial {trial+1} to google sheet.") def store_tip_results( diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py deleted file mode 100644 index 09aa4954958..00000000000 --- a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Liquid sense OT3 P1000.""" -from opentrons.protocol_api import ProtocolContext - -metadata = {"protocolName": "liquid-sense-ot3-p1000-96"} -requirements = {"robotType": "Flex", "apiLevel": "2.15"} - -SLOT_SCALE = 4 -SLOT_DIAL = 5 -SLOTS_TIPRACK = { - # TODO: add slot 12 when tipracks are disposable - 50: [1, 2, 3, 6, 7, 8, 9, 10, 11], - 200: [1, 2, 3, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration - 1000: [1, 2, 3, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration -} - -LABWARE_ON_SCALE = "nest_1_reservoir_195ml" - - -def run(ctx: ProtocolContext) -> None: - """Run.""" - tipracks = [ - ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL_adp", slot) - for size, slots in SLOTS_TIPRACK.items() - for slot in slots - if size == 50 # only calibrate 50ul tip-racks - ] - scale_labware = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) - pipette = ctx.load_instrument("p1000_96", "left") - for rack in tipracks: - pipette.pick_up_tip(rack["A1"]) - pipette.aspirate(10, scale_labware["A1"].top()) - pipette.dispense(10, scale_labware["A1"].top()) - pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py deleted file mode 100644 index d2b806d1229..00000000000 --- a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py +++ /dev/null @@ -1,26 +0,0 @@ -"""LiquidSense OT3 P1000.""" -from opentrons.protocol_api import ProtocolContext - -metadata = {"protocolName": "liquid-sense-ot3-p1000-multi"} -requirements = {"robotType": "Flex", "apiLevel": "2.15"} - -SLOT_SCALE = 4 -SLOT_DIAL = 5 -SLOTS_TIPRACK = {50: [2], 200: [3], 1000: [6]} -LABWARE_ON_SCALE = "nest_1_reservoir_195ml" - - -def run(ctx: ProtocolContext) -> None: - """Run.""" - tipracks = [ - ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) - for size, slots in SLOTS_TIPRACK.items() - for slot in slots - ] - vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) - pipette = ctx.load_instrument("flex_8channel_1000", "left") - for rack in tipracks: - pipette.pick_up_tip(rack["A1"]) - pipette.aspirate(10, vial["A1"].top()) - pipette.dispense(10, vial["A1"].top()) - pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py deleted file mode 100644 index 4e8fcc177f4..00000000000 --- a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Liquid Sense OT3 P1000.""" -from opentrons.protocol_api import ProtocolContext - -metadata = {"protocolName": "liquid-sense-ot3-p1000-single"} -requirements = {"robotType": "Flex", "apiLevel": "2.15"} - -SLOT_SCALE = 4 -SLOT_DIAL = 5 -SLOTS_TIPRACK = { - 50: [3], - 200: [6], - 1000: [9], -} -LABWARE_ON_SCALE = "nest_1_reservoir_195ml" - - -def run(ctx: ProtocolContext) -> None: - """Run.""" - tipracks = [ - ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) - for size, slots in SLOTS_TIPRACK.items() - for slot in slots - ] - vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) - dial = ctx.load_labware("dial_indicator", SLOT_DIAL) - pipette = ctx.load_instrument("flex_1channel_1000", "left") - for rack in tipracks: - pipette.pick_up_tip(rack["A1"]) - pipette.aspirate(10, vial["A1"].top()) - pipette.dispense(10, vial["A1"].top()) - pipette.aspirate(1, dial["A1"].top()) - pipette.dispense(1, dial["A1"].top()) - pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_vial.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_vial.py new file mode 100644 index 00000000000..d760f8da0ed --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_vial.py @@ -0,0 +1,32 @@ +"""Liquid Sense OT3.""" +from opentrons.protocol_api import ProtocolContext, OFF_DECK + +metadata = {"protocolName": "liquid-sense-ot3-p1000-single-vial"} +requirements = {"robotType": "Flex", "apiLevel": "2.17"} + +SLOT_SCALE = 1 +SLOT_DIAL = 9 +SLOTS_TIPRACK = { + 50: [3], + 200: [3], + 1000: [3], +} +LABWARE_ON_SCALE = "radwag_pipette_calibration_vial" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + trash = ctx.load_trash_bin("A3") + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_1channel_1000", "left") + for size, slots in SLOTS_TIPRACK.items(): + for slot in slots: + rack = ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(10, dial["A1"].top()) + pipette.dispense(10, dial["A1"].top()) + pipette.drop_tip(trash) + ctx.move_labware(rack, OFF_DECK) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py deleted file mode 100644 index 34f83cd4cf7..00000000000 --- a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Liquid Sense OT3.""" -from opentrons.protocol_api import ProtocolContext - -metadata = {"protocolName": "liquid_sense-ot3-p50-multi-50ul-tip"} -requirements = {"robotType": "Flex", "apiLevel": "2.15"} - -SLOT_SCALE = 4 -SLOT_DIAL = 5 -SLOTS_TIPRACK = { - 50: [3], -} -LABWARE_ON_SCALE = "nest_1_reservoir_195ml" - - -def run(ctx: ProtocolContext) -> None: - """Run.""" - tipracks = [ - ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) - for size, slots in SLOTS_TIPRACK.items() - for slot in slots - ] - vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) - pipette = ctx.load_instrument("flex_8channel_50", "left") - for rack in tipracks: - pipette.pick_up_tip(rack["A1"]) - pipette.aspirate(pipette.min_volume, vial["A1"].top()) - pipette.dispense(pipette.min_volume, vial["A1"].top()) - pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py deleted file mode 100644 index 8e9d65a72e2..00000000000 --- a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Liquid Sense OT3.""" -from opentrons.protocol_api import ProtocolContext - -metadata = {"protocolName": "liquid-sense-ot3-p50-single"} -requirements = {"robotType": "Flex", "apiLevel": "2.15"} - -SLOT_SCALE = 4 -SLOT_DIAL = 5 -SLOTS_TIPRACK = { - 50: [3], -} -LABWARE_ON_SCALE = "radwag_pipette_calibration_vial" - - -def run(ctx: ProtocolContext) -> None: - """Run.""" - tipracks = [ - ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) - for size, slots in SLOTS_TIPRACK.items() - for slot in slots - ] - vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) - dial = ctx.load_labware("dial_indicator", SLOT_DIAL) - pipette = ctx.load_instrument("flex_1channel_50", "left") - for rack in tipracks: - pipette.pick_up_tip(rack["A1"]) - pipette.aspirate(10, vial["A1"].top()) - pipette.dispense(10, vial["A1"].top()) - pipette.aspirate(1, dial["A1"].top()) - pipette.dispense(1, dial["A1"].top()) - pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_vial.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_vial.py new file mode 100644 index 00000000000..7a7e607d08e --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_vial.py @@ -0,0 +1,30 @@ +"""Liquid Sense OT3.""" +from opentrons.protocol_api import ProtocolContext, OFF_DECK + +metadata = {"protocolName": "liquid-sense-ot3-p50-single-vial"} +requirements = {"robotType": "Flex", "apiLevel": "2.17"} + +SLOT_SCALE = 1 +SLOT_DIAL = 9 +SLOTS_TIPRACK = { + 50: [3], +} +LABWARE_ON_SCALE = "radwag_pipette_calibration_vial" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + trash = ctx.load_trash_bin("A3") + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_1channel_50", "left") + for size, slots in SLOTS_TIPRACK.items(): + for slot in slots: + rack = ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(10, dial["A1"].top()) + pipette.dispense(10, dial["A1"].top()) + pipette.drop_tip(trash) + ctx.move_labware(rack, OFF_DECK) diff --git a/scripts/python_build_utils.py b/scripts/python_build_utils.py index d55ece0e0c8..a0daeabc8b9 100644 --- a/scripts/python_build_utils.py +++ b/scripts/python_build_utils.py @@ -23,6 +23,7 @@ CWD = HERE or '.' package_entries = { + 'abr-testing': PackageEntry('abr_testing'), 'api': PackageEntry('opentrons_api'), 'update-server': PackageEntry('update_server'), 'robot-server': PackageEntry('robot_server'),