From ab157370079727719bdee872d252925541a6b0ca Mon Sep 17 00:00:00 2001 From: Shlok Amin Date: Mon, 13 Nov 2023 16:40:24 -0500 Subject: [PATCH 01/46] feat(shared-data): add 8 moveable trashes (#13959) closes RSS-397 --- .../deck/definitions/4/ot3_standard.json | 109 ++++++++++++++++-- 1 file changed, 100 insertions(+), 9 deletions(-) diff --git a/shared-data/deck/definitions/4/ot3_standard.json b/shared-data/deck/definitions/4/ot3_standard.json index 333fd6d59c3..87ba7e78e14 100644 --- a/shared-data/deck/definitions/4/ot3_standard.json +++ b/shared-data/deck/definitions/4/ot3_standard.json @@ -250,7 +250,98 @@ "compatibleModuleTypes": [] }, { - "id": "movableTrash", + "id": "movableTrashD1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashC1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashB1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashA1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashD3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashC3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashB3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashA3", "areaType": "movableTrash", "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], "boundingBox": { @@ -433,14 +524,14 @@ ], "displayName": "Slot With Movable Trash", "providesAddressableAreas": { - "cutoutD1": ["movableTrash"], - "cutoutC1": ["movableTrash"], - "cutoutB1": ["movableTrash"], - "cutoutA1": ["movableTrash"], - "cutoutD3": ["movableTrash"], - "cutoutC3": ["movableTrash"], - "cutoutB3": ["movableTrash"], - "cutoutA3": ["movableTrash"] + "cutoutD1": ["movableTrashD1"], + "cutoutC1": ["movableTrashC1"], + "cutoutB1": ["movableTrashB1"], + "cutoutA1": ["movableTrashA1"], + "cutoutD3": ["movableTrashD3"], + "cutoutC3": ["movableTrashC3"], + "cutoutB3": ["movableTrashB3"], + "cutoutA3": ["movableTrashA3"] } }, { From 72bad58e50c21982ef04babc637c6fdfd147283a Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Mon, 13 Nov 2023 16:51:20 -0500 Subject: [PATCH 02/46] chore(app-shell): Fix internal release updates (#13966) Since ac0304a5c61462c2c31174308f88eed5404cfda8 we were not properly setting the folder-part of the app update flow in internal-releases which meant the app couldn't update. Closes RQA-1882 --- .github/workflows/app-test-build-deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/app-test-build-deploy.yaml b/.github/workflows/app-test-build-deploy.yaml index f50e4c0ad9e..dc93eae9c9a 100644 --- a/.github/workflows/app-test-build-deploy.yaml +++ b/.github/workflows/app-test-build-deploy.yaml @@ -230,7 +230,7 @@ jobs: echo "Configuring project, bucket, and folder for ot3" echo "project=ot3" >> $GITHUB_OUTPUT echo "bucket=${{env._APP_DEPLOY_BUCKET_OT3}}" >> $GITHUB_OUTPUT - echo "folder=${{env._APP_DEPLOY_BUCKET_OT3}}" >> $GITHUB_OUTPUT + echo "folder=${{env._APP_DEPLOY_FOLDER_OT3}}" >> $GITHUB_OUTPUT fi - uses: 'actions/checkout@v3' with: From be2ac8e55139d9945bfff277f9758dc0add14a47 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Mon, 13 Nov 2023 17:19:13 -0500 Subject: [PATCH 03/46] chore: update internal release notes (#13967) --- api/release-notes-internal.md | 47 +++-------------------- app-shell/build/release-notes-internal.md | 18 +++------ 2 files changed, 11 insertions(+), 54 deletions(-) diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index 07c68ac4749..426958f061b 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -4,52 +4,17 @@ For more details about this release, please see the full [technical change log][ --- -# Internal Release 0.14.0 +# Internal Release 1.0.0 ## New Stuff In This Release -- Return tip heights and some other pipette behaviors are now properly executed based on the kind of tip being used -- Release Flex robot software builds are now cryptographically signed. If you run a release build, you can only install other properly signed release builds. Note that if the robot was previously on a non-release build this won't latch; remove the update server config file at ``/var/lib/otupdate/config.json`` to go back to signed builds only. -- Error handling has been overhauled; all errors now display with an error code for easier reporting. Many of those error codes are the 4000 catchall still but this will improve over time. -- If there's an error during the post-run cleanup steps, where the robot homes and drops tips, the run should no longer get stuck in a permanent "finishing" state. It should get marked as failed. -- Further updates to Flex motion control parameters from hardware testing for both gantry and plunger speeds and acceleration -- Pipette overpressure detection is now integrated. -- All instrument flows should now show errors if they occur instead of skipping a step -- Fixes to several incorrect status displays in ODD (i.e. protocols skipping the full-color outcome splash) -- Robot can now handle json protocol v7 -- Support for PVT (v1.1) grippers -- Update progress should get displayed after restart for firmware updates -- Removed `use_pick_up_location_lpc_offset` and `use_drop_location_lpc_offset` from `protocol_context.move_labware` arguments. So they should be removed from any protocols that used them. This change also requires resetting the protocol run database on the robot. -- Added 'contextual' gripper offsets to deck, labware and module definitions. So, any labware movement offsets that were previously being specified in the protocol should now be removed or adjusted or they will get added twice. +- Fixed an issue where the robot wasn't actually checking for updates; you will now correctly get prompted to update your robot from the settings tab of the ODD when an update is available, including during onboarding +- You can update the robot by putting a system update (ot3-system.zip) on a flash drive and plugging it in the front USB port, then going to robot settings +- Support for 96-channel pipettes in protocols +- Early provisional support for deck configuration and trash chutes in protocols ## Big Things That Don't Work Yet So Don't Report Bugs About Them ### Robot Control -- Pipette pressure sensing for liquid-level sensing purposes -- Labware pick up failure with gripper -- E-stop integrated handling especially with modules - -## Big Things That Do Work Please Do Report Bugs About Them -### Robot Control -- Protocol behavior -- Labware movement between slots/modules, both manual and with gripper, from python protocols -- Labware drop/gripper crash errors, but they're very insensitive -- Pipette and gripper automated offset calibration -- Network connectivity and discoverability -- Firmware update for all devices -- Cancelling a protocol run. We're even more sure we fixed this so definitely tell us if it's not. -- USB connectivity -- Stall detection firing basically ever unless you clearly ran into something - -### ODD -- Protocol execution including end-of-protocol screen -- Protocol run monitoring -- Attach and calibrate -- Network connection management, including viewing IP addresses and connecting to wifi networks -- Automatic updates of robot software when new internal releases are created -- Chrome remote devtools - if you enable them and then use Chrome to go to robotip:9223 you'll get devtools -- After a while, the ODD should go into idle; if you touch it, it will come back online - - - +- Pipette partial tip pickup is present but not fully validated or developed yet. Partial tip pickup on 96 channel pipettes will not use correct motion parameters; using the front channel of a pipette in partial tip pickup does not work. diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md index 08663572808..3c53342b57c 100644 --- a/app-shell/build/release-notes-internal.md +++ b/app-shell/build/release-notes-internal.md @@ -3,26 +3,18 @@ For more details about this release, please see the full [technical changelog][] --- -# Internal Release 0.14.0 +# Internal Release 1.0.0 -This is 0.14.0, an internal release for the app supporting the Opentrons Flex. +This is 1.0.0, an internal release for the app supporting the Opentrons Flex. This is still pretty early in the process, so some things are known not to work, and are listed below. Some things that may surprise you do work, and are also listed below. There may also be some littler things of note, and those are at the bottom. ## New Stuff In This Release -- All instrument flows should display errors properly now -- Update robot flows don't say OT-2s anymore -- There should be fewer surprise scroll bars on Windows -- The configuration of the on-device display can be factory-reset, which lets you go back to the first-time setup flow +- Support for running labware position check using the calibration adapter for added accuracy (note: not an automated flow, just uses the adapter instead of a tip) +- Support for 96-channel pipettes in protocols +- Early provisional support for deck configuration and trash chutes in protocols -## Big Things That Do Work Please Do Report Bugs About Them -- Connecting to a Flex, including via USB -- Running protocols on those Flexs including simulate, play/pause, labware position check - - Except for gripper; no LPC for gripper -- Attach, detach, and calibration flows for pipettes and gripper when started from the device page -- Automatic updates of app and robot when new internal-releases are created - From 10bc0fd28e11ccb5aa5d37d3651466eadba56f3b Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Mon, 13 Nov 2023 17:37:54 -0500 Subject: [PATCH 04/46] refactor(api): Delete unused opentrons.containers module #13973 --- .../opentrons/config/containers/__init__.py | 0 .../config/containers/default-containers.json | 27097 ---------------- 2 files changed, 27097 deletions(-) delete mode 100644 api/src/opentrons/config/containers/__init__.py delete mode 100644 api/src/opentrons/config/containers/default-containers.json diff --git a/api/src/opentrons/config/containers/__init__.py b/api/src/opentrons/config/containers/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/api/src/opentrons/config/containers/default-containers.json b/api/src/opentrons/config/containers/default-containers.json deleted file mode 100644 index 44824a024a4..00000000000 --- a/api/src/opentrons/config/containers/default-containers.json +++ /dev/null @@ -1,27097 +0,0 @@ -{ - "containers": { - "temperature-plate": { - "origin-offset": { - "x": 11.24, - "y": 14.34, - "z": 97 - }, - "locations":{} - }, - - "tube-rack-5ml-96": { - "locations": { - "A1": { - "y": 0, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B1": { - "y": 0, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C1": { - "y": 0, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D1": { - "y": 0, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E1": { - "y": 0, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F1": { - "y": 0, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G1": { - "y": 0, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H1": { - "y": 0, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A2": { - "y": 18, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B2": { - "y": 18, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C2": { - "y": 18, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D2": { - "y": 18, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E2": { - "y": 18, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F2": { - "y": 18, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G2": { - "y": 18, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H2": { - "y": 18, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A3": { - "y": 36, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B3": { - "y": 36, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C3": { - "y": 36, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D3": { - "y": 36, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E3": { - "y": 36, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F3": { - "y": 36, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G3": { - "y": 36, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H3": { - "y": 36, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A4": { - "y": 54, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B4": { - "y": 54, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C4": { - "y": 54, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D4": { - "y": 54, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E4": { - "y": 54, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F4": { - "y": 54, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G4": { - "y": 54, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H4": { - "y": 54, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A5": { - "y": 72, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B5": { - "y": 72, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C5": { - "y": 72, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D5": { - "y": 72, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E5": { - "y": 72, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F5": { - "y": 72, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G5": { - "y": 72, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H5": { - "y": 72, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A6": { - "y": 90, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B6": { - "y": 90, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C6": { - "y": 90, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D6": { - "y": 90, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E6": { - "y": 90, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F6": { - "y": 90, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G6": { - "y": 90, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H6": { - "y": 90, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A7": { - "y": 108, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B7": { - "y": 108, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C7": { - "y": 108, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D7": { - "y": 108, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E7": { - "y": 108, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F7": { - "y": 108, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G7": { - "y": 108, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H7": { - "y": 108, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A8": { - "y": 126, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B8": { - "y": 126, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C8": { - "y": 126, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D8": { - "y": 126, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E8": { - "y": 126, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F8": { - "y": 126, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G8": { - "y": 126, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H8": { - "y": 126, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A9": { - "y": 144, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B9": { - "y": 144, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C9": { - "y": 144, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D9": { - "y": 144, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E9": { - "y": 144, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F9": { - "y": 144, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G9": { - "y": 144, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H9": { - "y": 144, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A10": { - "y": 162, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B10": { - "y": 162, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C10": { - "y": 162, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D10": { - "y": 162, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E10": { - "y": 162, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F10": { - "y": 162, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G10": { - "y": 162, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H10": { - "y": 162, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A11": { - "y": 180, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B11": { - "y": 180, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C11": { - "y": 180, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D11": { - "y": 180, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E11": { - "y": 180, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F11": { - "y": 180, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G11": { - "y": 180, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H11": { - "y": 180, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A12": { - "y": 198, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B12": { - "y": 198, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C12": { - "y": 198, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D12": { - "y": 198, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E12": { - "y": 198, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F12": { - "y": 198, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G12": { - "y": 198, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H12": { - "y": 198, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - } - - } - }, - - "tube-rack-2ml-9x9": { - "locations": { - "A1": { - "y": 0, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B1": { - "y": 0, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C1": { - "y": 0, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D1": { - "y": 0, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E1": { - "y": 0, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F1": { - "y": 0, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G1": { - "y": 0, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H1": { - "y": 0, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I1": { - "y": 0, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A2": { - "y": 14.75, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B2": { - "y": 14.75, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C2": { - "y": 14.75, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D2": { - "y": 14.75, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E2": { - "y": 14.75, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F2": { - "y": 14.75, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G2": { - "y": 14.75, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H2": { - "y": 14.75, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I2": { - "y": 14.75, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A3": { - "y": 29.5, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B3": { - "y": 29.5, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C3": { - "y": 29.5, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D3": { - "y": 29.5, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E3": { - "y": 29.5, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F3": { - "y": 29.5, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G3": { - "y": 29.5, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H3": { - "y": 29.5, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I3": { - "y": 29.5, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A4": { - "y": 44.25, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B4": { - "y": 44.25, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C4": { - "y": 44.25, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D4": { - "y": 44.25, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E4": { - "y": 44.25, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F4": { - "y": 44.25, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G4": { - "y": 44.25, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H4": { - "y": 44.25, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I4": { - "y": 44.25, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A5": { - "y": 59, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B5": { - "y": 59, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C5": { - "y": 59, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D5": { - "y": 59, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E5": { - "y": 59, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F5": { - "y": 59, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G5": { - "y": 59, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H5": { - "y": 59, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I5": { - "y": 59, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A6": { - "y": 73.75, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B6": { - "y": 73.75, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C6": { - "y": 73.75, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D6": { - "y": 73.75, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E6": { - "y": 73.75, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F6": { - "y": 73.75, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G6": { - "y": 73.75, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H6": { - "y": 73.75, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I6": { - "y": 73.75, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A7": { - "y": 88.5, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B7": { - "y": 88.5, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C7": { - "y": 88.5, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D7": { - "y": 88.5, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E7": { - "y": 88.5, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F7": { - "y": 88.5, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G7": { - "y": 88.5, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H7": { - "y": 88.5, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I7": { - "y": 88.5, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A8": { - "y": 103.25, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B8": { - "y": 103.25, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C8": { - "y": 103.25, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D8": { - "y": 103.25, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E8": { - "y": 103.25, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F8": { - "y": 103.25, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G8": { - "y": 103.25, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H8": { - "y": 103.25, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I8": { - "y": 103.25, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A9": { - "y": 118, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B9": { - "y": 118, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C9": { - "y": 118, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D9": { - "y": 118, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E9": { - "y": 118, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F9": { - "y": 118, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G9": { - "y": 118, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H9": { - "y": 118, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I9": { - "y": 118, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - } - } - }, - "96-well-plate-20mm": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - } - } - }, - "6-well-plate": { - "origin-offset": { - "x": 23.16, - "y": 24.76 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 22.5, - "total-liquid-volume": 16800 - }, - "B1": { - "x": 39.12, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 22.5, - "total-liquid-volume": 16800 - }, - "A2": { - "x": 0, - "y": 39.12, - "z": 0, - "depth": 17.4, - "diameter": 22.5, - "total-liquid-volume": 16800 - }, - "B2": { - "x": 39.12, - "y": 39.12, - "z": 0, - "depth": 17.4, - "diameter": 22.5, - "total-liquid-volume": 16800 - }, - "A3": { - "x": 0, - "y": 78.24, - "z": 0, - "depth": 17.4, - "diameter": 22.5, - "total-liquid-volume": 16800 - }, - "B3": { - "x": 39.12, - "y": 78.24, - "z": 0, - "depth": 17.4, - "diameter": 22.5, - "total-liquid-volume": 16800 - } - } - }, - "12-well-plate": { - "origin-offset": { - "x": 16.79, - "y": 24.94 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "B1": { - "x": 26.01, - "y": 0, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "C1": { - "x": 52.02, - "y": 0, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "A2": { - "x": 0, - "y": 26.01, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "B2": { - "x": 26.01, - "y": 26.01, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "C2": { - "x": 52.02, - "y": 26.01, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "A3": { - "x": 0, - "y": 52.02, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "B3": { - "x": 26.01, - "y": 52.02, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "C3": { - "x": 52.02, - "y": 52.02, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "A4": { - "x": 0, - "y": 78.03, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "B4": { - "x": 26.01, - "y": 78.03, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "C4": { - "x": 52.02, - "y": 78.03, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - } - } - }, - "24-well-plate": { - "origin-offset": { - "x": 13.67, - "y": 15 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "B1": { - "x": 19.3, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "C1": { - "x": 38.6, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "D1": { - "x": 57.9, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "A2": { - "x": 0, - "y": 19.3, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "B2": { - "x": 19.3, - "y": 19.3, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "C2": { - "x": 38.6, - "y": 19.3, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "D2": { - "x": 57.9, - "y": 19.3, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "A3": { - "x": 0, - "y": 38.6, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "B3": { - "x": 19.3, - "y": 38.6, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "C3": { - "x": 38.6, - "y": 38.6, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "D3": { - "x": 57.9, - "y": 38.6, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "A4": { - "x": 0, - "y": 57.9, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "B4": { - "x": 19.3, - "y": 57.9, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "C4": { - "x": 38.6, - "y": 57.9, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "D4": { - "x": 57.9, - "y": 57.9, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "A5": { - "x": 0, - "y": 77.2, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "B5": { - "x": 19.3, - "y": 77.2, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "C5": { - "x": 38.6, - "y": 77.2, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "D5": { - "x": 57.9, - "y": 77.2, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "A6": { - "x": 0, - "y": 96.5, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "B6": { - "x": 19.3, - "y": 96.5, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "C6": { - "x": 38.6, - "y": 96.5, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "D6": { - "x": 57.9, - "y": 96.5, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - } - } - }, - "48-well-plate": { - "origin-offset": { - "x": 10.08, - "y": 18.16 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B1": { - "x": 13.08, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C1": { - "x": 26.16, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D1": { - "x": 39.24, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E1": { - "x": 52.32, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F1": { - "x": 65.4, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A2": { - "x": 0, - "y": 13.08, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B2": { - "x": 13.08, - "y": 13.08, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C2": { - "x": 26.16, - "y": 13.08, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D2": { - "x": 39.24, - "y": 13.08, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E2": { - "x": 52.32, - "y": 13.08, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F2": { - "x": 65.4, - "y": 13.08, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A3": { - "x": 0, - "y": 26.16, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B3": { - "x": 13.08, - "y": 26.16, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C3": { - "x": 26.16, - "y": 26.16, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D3": { - "x": 39.24, - "y": 26.16, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E3": { - "x": 52.32, - "y": 26.16, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F3": { - "x": 65.4, - "y": 26.16, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A4": { - "x": 0, - "y": 39.24, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B4": { - "x": 13.08, - "y": 39.24, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C4": { - "x": 26.16, - "y": 39.24, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D4": { - "x": 39.24, - "y": 39.24, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E4": { - "x": 52.32, - "y": 39.24, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F4": { - "x": 65.4, - "y": 39.24, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A5": { - "x": 0, - "y": 52.32, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B5": { - "x": 13.08, - "y": 52.32, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C5": { - "x": 26.16, - "y": 52.32, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D5": { - "x": 39.24, - "y": 52.32, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E5": { - "x": 52.32, - "y": 52.32, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F5": { - "x": 65.4, - "y": 52.32, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A6": { - "x": 0, - "y": 65.4, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B6": { - "x": 13.08, - "y": 65.4, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C6": { - "x": 26.16, - "y": 65.4, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D6": { - "x": 39.24, - "y": 65.4, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E6": { - "x": 52.32, - "y": 65.4, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F6": { - "x": 65.4, - "y": 65.4, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A7": { - "x": 0, - "y": 78.48, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B7": { - "x": 13.08, - "y": 78.48, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C7": { - "x": 26.16, - "y": 78.48, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D7": { - "x": 39.24, - "y": 78.48, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E7": { - "x": 52.32, - "y": 78.48, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F7": { - "x": 65.4, - "y": 78.48, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A8": { - "x": 0, - "y": 91.56, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B8": { - "x": 13.08, - "y": 91.56, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C8": { - "x": 26.16, - "y": 91.56, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D8": { - "x": 39.24, - "y": 91.56, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E8": { - "x": 52.32, - "y": 91.56, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F8": { - "x": 65.4, - "y": 91.56, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - } - } - }, - "trough-1row-25ml": { - "locations": { - "A1": { - "x": 42.75, - "y": 63.875, - "z": 0, - "depth": 26, - "diameter": 10, - "total-liquid-volume": 25000 - } - } - }, - "trough-1row-test": { - "locations": { - "A1": { - "x": 42.75, - "y": 63.875, - "z": 0, - "depth": 26, - "diameter": 10, - "total-liquid-volume": 25000 - } - } - }, - "hampton-1ml-deep-block": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - } - } - }, - - "rigaku-compact-crystallization-plate": { - "origin-offset": { - "x": 9, - "y": 11 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A13": { - "x": 3.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B13": { - "x": 12.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C13": { - "x": 21.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D13": { - "x": 30.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E13": { - "x": 39.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F13": { - "x": 48.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G13": { - "x": 57.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H13": { - "x": 66.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A14": { - "x": 3.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B14": { - "x": 12.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C14": { - "x": 21.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D14": { - "x": 30.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E14": { - "x": 39.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F14": { - "x": 48.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G14": { - "x": 57.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H14": { - "x": 66.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A15": { - "x": 3.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B15": { - "x": 12.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C15": { - "x": 21.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D15": { - "x": 30.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E15": { - "x": 39.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F15": { - "x": 48.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G15": { - "x": 57.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H15": { - "x": 66.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A16": { - "x": 3.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B16": { - "x": 12.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C16": { - "x": 21.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D16": { - "x": 30.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E16": { - "x": 39.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F16": { - "x": 48.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G16": { - "x": 57.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H16": { - "x": 66.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A17": { - "x": 3.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B17": { - "x": 12.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C17": { - "x": 21.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D17": { - "x": 30.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E17": { - "x": 39.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F17": { - "x": 48.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G17": { - "x": 57.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H17": { - "x": 66.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A18": { - "x": 3.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B18": { - "x": 12.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C18": { - "x": 21.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D18": { - "x": 30.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E18": { - "x": 39.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F18": { - "x": 48.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G18": { - "x": 57.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H18": { - "x": 66.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A19": { - "x": 3.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B19": { - "x": 12.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C19": { - "x": 21.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D19": { - "x": 30.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E19": { - "x": 39.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F19": { - "x": 48.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G19": { - "x": 57.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H19": { - "x": 66.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A20": { - "x": 3.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B20": { - "x": 12.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C20": { - "x": 21.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D20": { - "x": 30.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E20": { - "x": 39.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F20": { - "x": 48.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G20": { - "x": 57.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H20": { - "x": 66.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A21": { - "x": 3.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B21": { - "x": 12.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C21": { - "x": 21.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D21": { - "x": 30.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E21": { - "x": 39.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F21": { - "x": 48.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G21": { - "x": 57.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H21": { - "x": 66.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A22": { - "x": 3.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B22": { - "x": 12.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C22": { - "x": 21.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D22": { - "x": 30.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E22": { - "x": 39.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F22": { - "x": 48.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G22": { - "x": 57.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H22": { - "x": 66.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A23": { - "x": 3.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B23": { - "x": 12.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C23": { - "x": 21.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D23": { - "x": 30.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E23": { - "x": 39.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F23": { - "x": 48.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G23": { - "x": 57.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H23": { - "x": 66.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A24": { - "x": 3.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B24": { - "x": 12.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C24": { - "x": 21.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D24": { - "x": 30.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E24": { - "x": 39.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F24": { - "x": 48.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G24": { - "x": 57.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H24": { - "x": 66.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - } - } - }, - "alum-block-pcr-strips": { - - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A2": { - "x": 0, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B2": { - "x": 9, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C2": { - "x": 18, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D2": { - "x": 27, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E2": { - "x": 36, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F2": { - "x": 45, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G2": { - "x": 54, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H2": { - "x": 63, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - } - } - }, - "T75-flask": { - "origin-offset": { - "x": 42.75, - "y": 63.875 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 163, - "diameter": 25, - "total-liquid-volume": 75000 - } - } - }, - "T25-flask": { - "origin-offset": { - "x": 42.75, - "y": 63.875 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 99, - "diameter": 18, - "total-liquid-volume": 25000 - } - } - }, - "magdeck": { - "origin-offset": { - "x": 0, - "y": 0 - }, - "locations": { - "A1": { - "x": 63.9, - "y": 42.8, - "z": 0, - "depth": 82.25 - } - } - }, - "tempdeck": { - "origin-offset": { - "x": 0, - "y": 0 - }, - "locations": { - "A1": { - "x": 63.9, - "y": 42.8, - "z": 0, - "depth": 80.09 - } - } - }, - "trash-box": { - "origin-offset": { - "x": 42.75, - "y": 63.875 - }, - "locations": { - "A1": { - "x": 60, - "y": 45, - "z": 0, - "depth": 40 - } - } - }, - "fixed-trash": { - "origin-offset": { - "x": 0, - "y": 0 - }, - "locations": { - "A1": { - "x": 80, - "y": 80, - "z": 5, - "depth": 58 - } - } - }, - "tall-fixed-trash": { - "origin-offset": { - "x": 0, - "y": 0 - }, - "locations": { - "A1": { - "x": 80, - "y": 80, - "z": 5, - "depth": 80 - } - } - }, - "wheaton_vial_rack": { - "origin-offset": { - "x": 9, - "y": 9 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 35, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 70, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 105, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "E1": { - "x": 140, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "F1": { - "x": 175, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "G1": { - "x": 210, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "H1": { - "x": 245, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "I1": { - "x": 280, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "J1": { - "x": 315, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - - "A2": { - "x": 0, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 35, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 70, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 105, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "E2": { - "x": 140, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "F2": { - "x": 175, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "G2": { - "x": 210, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "H2": { - "x": 245, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "I2": { - "x": 280, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "J2": { - "x": 315, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - - "A3": { - "x": 0, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 35, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 70, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 105, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "E3": { - "x": 140, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "F3": { - "x": 175, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "G3": { - "x": 210, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "H3": { - "x": 245, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "I3": { - "x": 280, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "J3": { - "x": 315, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - - "A4": { - "x": 0, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 35, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 70, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 105, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "E4": { - "x": 140, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "F4": { - "x": 175, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "G4": { - "x": 210, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "H4": { - "x": 245, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "I4": { - "x": 280, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "J4": { - "x": 315, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - - "A5": { - "x": 0, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 35, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 70, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 105, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "E5": { - "x": 140, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "F5": { - "x": 175, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "G5": { - "x": 210, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "H5": { - "x": 245, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "I5": { - "x": 280, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "J5": { - "x": 315, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - } - } - }, - "tube-rack-80well": { - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 13.2, - "y": 0, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 26.5, - "y": 0, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 39.7, - "y": 0, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E1": { - "x": 52.9, - "y": 0, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 13.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 13.2, - "y": 13.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 26.5, - "y": 13.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 39.7, - "y": 13.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E2": { - "x": 52.9, - "y": 13.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 26.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 13.2, - "y": 26.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 26.5, - "y": 26.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 39.7, - "y": 26.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E3": { - "x": 52.9, - "y": 26.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 39.7, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 13.2, - "y": 39.7, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 26.5, - "y": 39.7, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 39.7, - "y": 39.7, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E4": { - "x": 52.9, - "y": 39.7, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 52.9, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 13.2, - "y": 52.9, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 26.5, - "y": 52.9, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 39.7, - "y": 52.9, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E5": { - "x": 52.9, - "y": 52.9, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 66.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 13.2, - "y": 66.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 26.5, - "y": 66.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 39.7, - "y": 66.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E6": { - "x": 52.9, - "y": 66.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A7": { - "x": 0, - "y": 79.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B7": { - "x": 13.2, - "y": 79.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C7": { - "x": 26.5, - "y": 79.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D7": { - "x": 39.7, - "y": 79.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E7": { - "x": 52.9, - "y": 79.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A8": { - "x": 0, - "y": 92.6, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B8": { - "x": 13.2, - "y": 92.6, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C8": { - "x": 26.5, - "y": 92.6, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D8": { - "x": 39.7, - "y": 92.6, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E8": { - "x": 52.9, - "y": 92.6, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A9": { - "x": 0, - "y": 105.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B9": { - "x": 13.2, - "y": 105.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C9": { - "x": 26.5, - "y": 105.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D9": { - "x": 39.7, - "y": 105.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E9": { - "x": 52.9, - "y": 105.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A10": { - "x": 0, - "y": 119.1, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B10": { - "x": 13.2, - "y": 119.1, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C10": { - "x": 26.5, - "y": 119.1, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D10": { - "x": 39.7, - "y": 119.1, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E10": { - "x": 52.9, - "y": 119.1, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A11": { - "x": 0, - "y": 132.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B11": { - "x": 13.2, - "y": 132.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C11": { - "x": 26.5, - "y": 132.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D11": { - "x": 39.7, - "y": 132.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E11": { - "x": 52.9, - "y": 132.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A12": { - "x": 0, - "y": 145.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B12": { - "x": 13.2, - "y": 145.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C12": { - "x": 26.5, - "y": 145.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D12": { - "x": 39.7, - "y": 145.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E12": { - "x": 52.9, - "y": 145.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A13": { - "x": 0, - "y": 158.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B13": { - "x": 13.2, - "y": 158.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C13": { - "x": 26.5, - "y": 158.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D13": { - "x": 39.7, - "y": 158.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E13": { - "x": 52.9, - "y": 158.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A14": { - "x": 0, - "y": 172, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B14": { - "x": 13.2, - "y": 172, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C14": { - "x": 26.5, - "y": 172, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D14": { - "x": 39.7, - "y": 172, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E14": { - "x": 52.9, - "y": 172, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A15": { - "x": 0, - "y": 185.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B15": { - "x": 13.2, - "y": 185.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C15": { - "x": 26.5, - "y": 185.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D15": { - "x": 39.7, - "y": 185.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E15": { - "x": 52.9, - "y": 185.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A16": { - "x": 0, - "y": 198.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B16": { - "x": 13.2, - "y": 198.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C16": { - "x": 26.5, - "y": 198.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D16": { - "x": 39.7, - "y": 198.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E16": { - "x": 52.9, - "y": 198.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - } - } - }, - "point": { - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 0, - "total-liquid-volume": 1 - } - } - }, - "tiprack-10ul": { - "origin-offset": { - "x": 14.24, - "y": 14.54 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - } - } - }, - - "tiprack-10ul-H": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 60, - "diameter": 6.4 - } - } - }, - - "tiprack-200ul": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - } - } - }, - "opentrons-tiprack-10ul": { - "origin-offset": { - "x": 10.77, - "y": 11.47 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B1": { - "x": 9, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C1": { - "x": 18, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D1": { - "x": 27, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E1": { - "x": 36, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F1": { - "x": 45, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G1": { - "x": 54, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H1": { - "x": 63, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A2": { - "x": 0, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B2": { - "x": 9, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C2": { - "x": 18, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D2": { - "x": 27, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E2": { - "x": 36, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F2": { - "x": 45, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G2": { - "x": 54, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H2": { - "x": 63, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A3": { - "x": 0, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B3": { - "x": 9, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C3": { - "x": 18, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D3": { - "x": 27, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E3": { - "x": 36, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F3": { - "x": 45, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G3": { - "x": 54, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H3": { - "x": 63, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A4": { - "x": 0, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B4": { - "x": 9, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C4": { - "x": 18, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D4": { - "x": 27, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E4": { - "x": 36, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F4": { - "x": 45, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G4": { - "x": 54, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H4": { - "x": 63, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A5": { - "x": 0, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B5": { - "x": 9, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C5": { - "x": 18, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D5": { - "x": 27, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E5": { - "x": 36, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F5": { - "x": 45, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G5": { - "x": 54, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H5": { - "x": 63, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A6": { - "x": 0, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B6": { - "x": 9, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C6": { - "x": 18, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D6": { - "x": 27, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E6": { - "x": 36, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F6": { - "x": 45, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G6": { - "x": 54, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H6": { - "x": 63, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A7": { - "x": 0, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B7": { - "x": 9, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C7": { - "x": 18, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D7": { - "x": 27, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E7": { - "x": 36, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F7": { - "x": 45, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G7": { - "x": 54, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H7": { - "x": 63, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A8": { - "x": 0, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B8": { - "x": 9, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C8": { - "x": 18, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D8": { - "x": 27, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E8": { - "x": 36, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F8": { - "x": 45, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G8": { - "x": 54, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H8": { - "x": 63, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A9": { - "x": 0, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B9": { - "x": 9, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C9": { - "x": 18, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D9": { - "x": 27, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E9": { - "x": 36, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F9": { - "x": 45, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G9": { - "x": 54, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H9": { - "x": 63, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A10": { - "x": 0, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B10": { - "x": 9, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C10": { - "x": 18, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D10": { - "x": 27, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E10": { - "x": 36, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F10": { - "x": 45, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G10": { - "x": 54, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H10": { - "x": 63, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A11": { - "x": 0, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B11": { - "x": 9, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C11": { - "x": 18, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D11": { - "x": 27, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E11": { - "x": 36, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F11": { - "x": 45, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G11": { - "x": 54, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H11": { - "x": 63, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A12": { - "x": 0, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B12": { - "x": 9, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C12": { - "x": 18, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D12": { - "x": 27, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E12": { - "x": 36, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F12": { - "x": 45, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G12": { - "x": 54, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H12": { - "x": 63, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - } - } - }, - "opentrons-tiprack-300ul": { - "origin-offset": { - "x": 12.59, - "y": 14.85 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B1": { - "x": 9, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C1": { - "x": 18, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D1": { - "x": 27, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E1": { - "x": 36, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F1": { - "x": 45, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G1": { - "x": 54, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H1": { - "x": 63, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A2": { - "x": 0, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B2": { - "x": 9, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C2": { - "x": 18, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D2": { - "x": 27, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E2": { - "x": 36, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F2": { - "x": 45, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G2": { - "x": 54, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H2": { - "x": 63, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A3": { - "x": 0, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B3": { - "x": 9, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C3": { - "x": 18, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D3": { - "x": 27, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E3": { - "x": 36, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F3": { - "x": 45, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G3": { - "x": 54, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H3": { - "x": 63, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A4": { - "x": 0, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B4": { - "x": 9, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C4": { - "x": 18, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D4": { - "x": 27, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E4": { - "x": 36, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F4": { - "x": 45, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G4": { - "x": 54, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H4": { - "x": 63, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A5": { - "x": 0, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B5": { - "x": 9, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C5": { - "x": 18, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D5": { - "x": 27, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E5": { - "x": 36, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F5": { - "x": 45, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G5": { - "x": 54, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H5": { - "x": 63, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A6": { - "x": 0, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B6": { - "x": 9, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C6": { - "x": 18, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D6": { - "x": 27, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E6": { - "x": 36, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F6": { - "x": 45, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G6": { - "x": 54, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H6": { - "x": 63, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A7": { - "x": 0, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B7": { - "x": 9, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C7": { - "x": 18, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D7": { - "x": 27, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E7": { - "x": 36, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F7": { - "x": 45, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G7": { - "x": 54, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H7": { - "x": 63, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A8": { - "x": 0, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B8": { - "x": 9, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C8": { - "x": 18, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D8": { - "x": 27, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E8": { - "x": 36, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F8": { - "x": 45, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G8": { - "x": 54, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H8": { - "x": 63, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A9": { - "x": 0, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B9": { - "x": 9, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C9": { - "x": 18, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D9": { - "x": 27, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E9": { - "x": 36, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F9": { - "x": 45, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G9": { - "x": 54, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H9": { - "x": 63, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A10": { - "x": 0, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B10": { - "x": 9, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C10": { - "x": 18, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D10": { - "x": 27, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E10": { - "x": 36, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F10": { - "x": 45, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G10": { - "x": 54, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H10": { - "x": 63, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A11": { - "x": 0, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B11": { - "x": 9, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C11": { - "x": 18, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D11": { - "x": 27, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E11": { - "x": 36, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F11": { - "x": 45, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G11": { - "x": 54, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H11": { - "x": 63, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A12": { - "x": 0, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B12": { - "x": 9, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C12": { - "x": 18, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D12": { - "x": 27, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E12": { - "x": 36, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F12": { - "x": 45, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G12": { - "x": 54, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H12": { - "x": 63, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - } - } - }, - - "tiprack-1000ul": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - } - } - }, - "opentrons-tiprack-1000ul": { - "origin-offset": { - "x": 8.5, - "y": 11.18 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - } - } - }, - "tube-rack-.75ml": { - "origin-offset": { - "x": 13.5, - "y": 15, - "z": 55 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "B1": { - "x": 19.5, - "y": 0, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "C1": { - "x": 39, - "y": 0, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "D1": { - "x": 58.5, - "y": 0, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "A2": { - "x": 0, - "y": 19.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "B2": { - "x": 19.5, - "y": 19.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "C2": { - "x": 39, - "y": 19.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "D2": { - "x": 58.5, - "y": 19.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "A3": { - "x": 0, - "y": 39, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "B3": { - "x": 19.5, - "y": 39, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "C3": { - "x": 39, - "y": 39, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "D3": { - "x": 58.5, - "y": 39, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "A4": { - "x": 0, - "y": 58.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "B4": { - "x": 19.5, - "y": 58.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "C4": { - "x": 39, - "y": 58.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "D4": { - "x": 58.5, - "y": 58.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "A5": { - "x": 0, - "y": 78, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "B5": { - "x": 19.5, - "y": 78, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "C5": { - "x": 39, - "y": 78, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "D5": { - "x": 58.5, - "y": 78, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "A6": { - "x": 0, - "y": 97.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "B6": { - "x": 19.5, - "y": 97.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "C6": { - "x": 39, - "y": 97.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "D6": { - "x": 58.5, - "y": 97.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - } - } - }, - - "tube-rack-2ml": { - "origin-offset": { - "x": 13, - "y": 16, - "z": 52 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 19.5, - "y": 0, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 39, - "y": 0, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 58.5, - "y": 0, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 19.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 19.5, - "y": 19.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 39, - "y": 19.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 58.5, - "y": 19.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 39, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 19.5, - "y": 39, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 39, - "y": 39, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 58.5, - "y": 39, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 58.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 19.5, - "y": 58.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 39, - "y": 58.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 58.5, - "y": 58.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 78, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 19.5, - "y": 78, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 39, - "y": 78, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 58.5, - "y": 78, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 97.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 19.5, - "y": 97.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 39, - "y": 97.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 58.5, - "y": 97.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - } - } - }, - - "tube-rack-15_50ml": { - "origin-offset": { - "x": 11, - "y": 19 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 5, - "depth": 115, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B1": { - "x": 31.5, - "y": 0, - "z": 5, - "depth": 115, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C1": { - "x": 63, - "y": 0, - "z": 5, - "depth": 115, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "A2": { - "x": 0, - "y": 22.7, - "z": 5, - "depth": 115, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B2": { - "x": 31.5, - "y": 22.7, - "z": 5, - "depth": 115, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C2": { - "x": 63, - "y": 22.7, - "z": 0, - "depth": 115, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "A3": { - "x": 5.9, - "y": 51.26, - "z": 5, - "depth": 115, - "diameter": 30, - "total-liquid-volume": 50000 - }, - "B3": { - "x": 51.26, - "y": 51.26, - "z": 5, - "depth": 115, - "diameter": 30, - "total-liquid-volume": 50000 - }, - "A4": { - "x": 5.9, - "y": 87.1, - "z": 5, - "depth": 115, - "diameter": 30, - "total-liquid-volume": 50000 - }, - "B4": { - "x": 51.26, - "y": 87.1, - "z": 5, - "depth": 115, - "diameter": 30, - "total-liquid-volume": 50000 - } - } - }, - - "trough-12row": { - "origin-offset": { - "x": 7.75, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A2": { - "x": 0, - "y": 9, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A3": { - "x": 0, - "y": 18, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A4": { - "x": 0, - "y": 27, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A5": { - "x": 0, - "y": 36, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A6": { - "x": 0, - "y": 45, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A7": { - "x": 0, - "y": 54, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A8": { - "x": 0, - "y": 63, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A9": { - "x": 0, - "y": 72, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A10": { - "x": 0, - "y": 81, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A11": { - "x": 0, - "y": 90, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A12": { - "x": 0, - "y": 99, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - } - } - }, - - "trough-12row-short": { - "origin-offset": { - "x": 7.75, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - } - } - }, - - "24-vial-rack": { - "origin-offset": { - "x": 13.67, - "y": 16 - }, - "locations": { - "A1": { - "x": 0.0, - "total-liquid-volume": 3400, - "y": 0.0, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "A2": { - "x": 0.0, - "total-liquid-volume": 3400, - "y": 19.3, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "A3": { - "x": 0.0, - "total-liquid-volume": 3400, - "y": 38.6, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "A4": { - "x": 0.0, - "total-liquid-volume": 3400, - "y": 57.9, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "A5": { - "x": 0.0, - "total-liquid-volume": 3400, - "y": 77.2, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "A6": { - "x": 0.0, - "total-liquid-volume": 3400, - "y": 96.5, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "B1": { - "x": 19.3, - "total-liquid-volume": 3400, - "y": 0.0, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "B2": { - "x": 19.3, - "total-liquid-volume": 3400, - "y": 19.3, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "B3": { - "x": 19.3, - "total-liquid-volume": 3400, - "y": 38.6, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "B4": { - "x": 19.3, - "total-liquid-volume": 3400, - "y": 57.9, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "B5": { - "x": 19.3, - "total-liquid-volume": 3400, - "y": 77.2, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "B6": { - "x": 19.3, - "total-liquid-volume": 3400, - "y": 96.5, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "C1": { - "x": 38.6, - "total-liquid-volume": 3400, - "y": 0.0, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "C2": { - "x": 38.6, - "total-liquid-volume": 3400, - "y": 19.3, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "C3": { - "x": 38.6, - "total-liquid-volume": 3400, - "y": 38.6, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "C4": { - "x": 38.6, - "total-liquid-volume": 3400, - "y": 57.9, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "C5": { - "x": 38.6, - "total-liquid-volume": 3400, - "y": 77.2, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "C6": { - "x": 38.6, - "total-liquid-volume": 3400, - "y": 96.5, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "D1": { - "x": 57.9, - "total-liquid-volume": 3400, - "y": 0.0, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "D2": { - "x": 57.9, - "total-liquid-volume": 3400, - "y": 19.3, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "D3": { - "x": 57.9, - "total-liquid-volume": 3400, - "y": 38.6, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "D4": { - "x": 57.9, - "total-liquid-volume": 3400, - "y": 57.9, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "D5": { - "x": 57.9, - "total-liquid-volume": 3400, - "y": 77.2, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "D6": { - "x": 57.9, - "total-liquid-volume": 3400, - "y": 96.5, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - } - } - }, - - "96-deep-well": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - } - } - }, - - "96-PCR-tall": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - } - } - }, - - "96-PCR-flat": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - } - } - }, - - "biorad-hardshell-96-PCR": { - "origin-offset": { - "x": 18.24, - "y": 13.63 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B1": { - "x": 9, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C1": { - "x": 18, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D1": { - "x": 27, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E1": { - "x": 36, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F1": { - "x": 45, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G1": { - "x": 54, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H1": { - "x": 63, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A2": { - "x": 0, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B2": { - "x": 9, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C2": { - "x": 18, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D2": { - "x": 27, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E2": { - "x": 36, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F2": { - "x": 45, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G2": { - "x": 54, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H2": { - "x": 63, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A3": { - "x": 0, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B3": { - "x": 9, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C3": { - "x": 18, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D3": { - "x": 27, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E3": { - "x": 36, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F3": { - "x": 45, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G3": { - "x": 54, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H3": { - "x": 63, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A4": { - "x": 0, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B4": { - "x": 9, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C4": { - "x": 18, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D4": { - "x": 27, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E4": { - "x": 36, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F4": { - "x": 45, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G4": { - "x": 54, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H4": { - "x": 63, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A5": { - "x": 0, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B5": { - "x": 9, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C5": { - "x": 18, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D5": { - "x": 27, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E5": { - "x": 36, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F5": { - "x": 45, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G5": { - "x": 54, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H5": { - "x": 63, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A6": { - "x": 0, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B6": { - "x": 9, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C6": { - "x": 18, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D6": { - "x": 27, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E6": { - "x": 36, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F6": { - "x": 45, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G6": { - "x": 54, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H6": { - "x": 63, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A7": { - "x": 0, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B7": { - "x": 9, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C7": { - "x": 18, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D7": { - "x": 27, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E7": { - "x": 36, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F7": { - "x": 45, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G7": { - "x": 54, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H7": { - "x": 63, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A8": { - "x": 0, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B8": { - "x": 9, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C8": { - "x": 18, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D8": { - "x": 27, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E8": { - "x": 36, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F8": { - "x": 45, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G8": { - "x": 54, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H8": { - "x": 63, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A9": { - "x": 0, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B9": { - "x": 9, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C9": { - "x": 18, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D9": { - "x": 27, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E9": { - "x": 36, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F9": { - "x": 45, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G9": { - "x": 54, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H9": { - "x": 63, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A10": { - "x": 0, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B10": { - "x": 9, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C10": { - "x": 18, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D10": { - "x": 27, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E10": { - "x": 36, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F10": { - "x": 45, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G10": { - "x": 54, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H10": { - "x": 63, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A11": { - "x": 0, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B11": { - "x": 9, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C11": { - "x": 18, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D11": { - "x": 27, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E11": { - "x": 36, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F11": { - "x": 45, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G11": { - "x": 54, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H11": { - "x": 63, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A12": { - "x": 0, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B12": { - "x": 9, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C12": { - "x": 18, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D12": { - "x": 27, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E12": { - "x": 36, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F12": { - "x": 45, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G12": { - "x": 54, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H12": { - "x": 63, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - } - } - }, - - "96-flat": { - "origin-offset": { - "x": 17.64, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B1": { - "x": 9, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C1": { - "x": 18, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D1": { - "x": 27, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E1": { - "x": 36, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F1": { - "x": 45, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G1": { - "x": 54, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H1": { - "x": 63, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A2": { - "x": 0, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B2": { - "x": 9, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C2": { - "x": 18, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D2": { - "x": 27, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E2": { - "x": 36, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F2": { - "x": 45, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G2": { - "x": 54, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H2": { - "x": 63, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A3": { - "x": 0, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B3": { - "x": 9, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C3": { - "x": 18, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D3": { - "x": 27, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E3": { - "x": 36, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F3": { - "x": 45, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G3": { - "x": 54, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H3": { - "x": 63, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A4": { - "x": 0, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B4": { - "x": 9, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C4": { - "x": 18, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D4": { - "x": 27, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E4": { - "x": 36, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F4": { - "x": 45, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G4": { - "x": 54, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H4": { - "x": 63, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A5": { - "x": 0, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B5": { - "x": 9, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C5": { - "x": 18, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D5": { - "x": 27, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E5": { - "x": 36, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F5": { - "x": 45, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G5": { - "x": 54, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H5": { - "x": 63, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A6": { - "x": 0, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B6": { - "x": 9, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C6": { - "x": 18, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D6": { - "x": 27, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E6": { - "x": 36, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F6": { - "x": 45, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G6": { - "x": 54, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H6": { - "x": 63, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A7": { - "x": 0, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B7": { - "x": 9, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C7": { - "x": 18, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D7": { - "x": 27, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E7": { - "x": 36, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F7": { - "x": 45, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G7": { - "x": 54, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H7": { - "x": 63, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A8": { - "x": 0, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B8": { - "x": 9, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C8": { - "x": 18, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D8": { - "x": 27, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E8": { - "x": 36, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F8": { - "x": 45, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G8": { - "x": 54, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H8": { - "x": 63, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A9": { - "x": 0, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B9": { - "x": 9, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C9": { - "x": 18, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D9": { - "x": 27, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E9": { - "x": 36, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F9": { - "x": 45, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G9": { - "x": 54, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H9": { - "x": 63, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A10": { - "x": 0, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B10": { - "x": 9, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C10": { - "x": 18, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D10": { - "x": 27, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E10": { - "x": 36, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F10": { - "x": 45, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G10": { - "x": 54, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H10": { - "x": 63, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A11": { - "x": 0, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B11": { - "x": 9, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C11": { - "x": 18, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D11": { - "x": 27, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E11": { - "x": 36, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F11": { - "x": 45, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G11": { - "x": 54, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H11": { - "x": 63, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A12": { - "x": 0, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B12": { - "x": 9, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C12": { - "x": 18, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D12": { - "x": 27, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E12": { - "x": 36, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F12": { - "x": 45, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G12": { - "x": 54, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H12": { - "x": 63, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - } - } - }, - - "PCR-strip-tall": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - } - } - }, - - "384-plate": { - "origin-offset": { - "x": 9, - "y": 12.13 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B1": { - "x": 4.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D1": { - "x": 13.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F1": { - "x": 22.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H1": { - "x": 31.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J1": { - "x": 40.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L1": { - "x": 49.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N1": { - "x": 58.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P1": { - "x": 67.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A2": { - "x": 0, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B2": { - "x": 4.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C2": { - "x": 9, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D2": { - "x": 13.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E2": { - "x": 18, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F2": { - "x": 22.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G2": { - "x": 27, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H2": { - "x": 31.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I2": { - "x": 36, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J2": { - "x": 40.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K2": { - "x": 45, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L2": { - "x": 49.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M2": { - "x": 54, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N2": { - "x": 58.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O2": { - "x": 63, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P2": { - "x": 67.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A3": { - "x": 0, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B3": { - "x": 4.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C3": { - "x": 9, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D3": { - "x": 13.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E3": { - "x": 18, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F3": { - "x": 22.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G3": { - "x": 27, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H3": { - "x": 31.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I3": { - "x": 36, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J3": { - "x": 40.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K3": { - "x": 45, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L3": { - "x": 49.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M3": { - "x": 54, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N3": { - "x": 58.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O3": { - "x": 63, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P3": { - "x": 67.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A4": { - "x": 0, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B4": { - "x": 4.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C4": { - "x": 9, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D4": { - "x": 13.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E4": { - "x": 18, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F4": { - "x": 22.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G4": { - "x": 27, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H4": { - "x": 31.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I4": { - "x": 36, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J4": { - "x": 40.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K4": { - "x": 45, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L4": { - "x": 49.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M4": { - "x": 54, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N4": { - "x": 58.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O4": { - "x": 63, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P4": { - "x": 67.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A5": { - "x": 0, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B5": { - "x": 4.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C5": { - "x": 9, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D5": { - "x": 13.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E5": { - "x": 18, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F5": { - "x": 22.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G5": { - "x": 27, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H5": { - "x": 31.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I5": { - "x": 36, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J5": { - "x": 40.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K5": { - "x": 45, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L5": { - "x": 49.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M5": { - "x": 54, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N5": { - "x": 58.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O5": { - "x": 63, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P5": { - "x": 67.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A6": { - "x": 0, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B6": { - "x": 4.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C6": { - "x": 9, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D6": { - "x": 13.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E6": { - "x": 18, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F6": { - "x": 22.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G6": { - "x": 27, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H6": { - "x": 31.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I6": { - "x": 36, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J6": { - "x": 40.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K6": { - "x": 45, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L6": { - "x": 49.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M6": { - "x": 54, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N6": { - "x": 58.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O6": { - "x": 63, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P6": { - "x": 67.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A7": { - "x": 0, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B7": { - "x": 4.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C7": { - "x": 9, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D7": { - "x": 13.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E7": { - "x": 18, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F7": { - "x": 22.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G7": { - "x": 27, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H7": { - "x": 31.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I7": { - "x": 36, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J7": { - "x": 40.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K7": { - "x": 45, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L7": { - "x": 49.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M7": { - "x": 54, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N7": { - "x": 58.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O7": { - "x": 63, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P7": { - "x": 67.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A8": { - "x": 0, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B8": { - "x": 4.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C8": { - "x": 9, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D8": { - "x": 13.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E8": { - "x": 18, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F8": { - "x": 22.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G8": { - "x": 27, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H8": { - "x": 31.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I8": { - "x": 36, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J8": { - "x": 40.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K8": { - "x": 45, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L8": { - "x": 49.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M8": { - "x": 54, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N8": { - "x": 58.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O8": { - "x": 63, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P8": { - "x": 67.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A9": { - "x": 0, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B9": { - "x": 4.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C9": { - "x": 9, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D9": { - "x": 13.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E9": { - "x": 18, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F9": { - "x": 22.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G9": { - "x": 27, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H9": { - "x": 31.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I9": { - "x": 36, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J9": { - "x": 40.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K9": { - "x": 45, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L9": { - "x": 49.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M9": { - "x": 54, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N9": { - "x": 58.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O9": { - "x": 63, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P9": { - "x": 67.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A10": { - "x": 0, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B10": { - "x": 4.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C10": { - "x": 9, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D10": { - "x": 13.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E10": { - "x": 18, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F10": { - "x": 22.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G10": { - "x": 27, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H10": { - "x": 31.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I10": { - "x": 36, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J10": { - "x": 40.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K10": { - "x": 45, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L10": { - "x": 49.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M10": { - "x": 54, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N10": { - "x": 58.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O10": { - "x": 63, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P10": { - "x": 67.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A11": { - "x": 0, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B11": { - "x": 4.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C11": { - "x": 9, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D11": { - "x": 13.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E11": { - "x": 18, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F11": { - "x": 22.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G11": { - "x": 27, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H11": { - "x": 31.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I11": { - "x": 36, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J11": { - "x": 40.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K11": { - "x": 45, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L11": { - "x": 49.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M11": { - "x": 54, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N11": { - "x": 58.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O11": { - "x": 63, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P11": { - "x": 67.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A12": { - "x": 0, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B12": { - "x": 4.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C12": { - "x": 9, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D12": { - "x": 13.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E12": { - "x": 18, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F12": { - "x": 22.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G12": { - "x": 27, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H12": { - "x": 31.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I12": { - "x": 36, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J12": { - "x": 40.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K12": { - "x": 45, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L12": { - "x": 49.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M12": { - "x": 54, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N12": { - "x": 58.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O12": { - "x": 63, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P12": { - "x": 67.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A13": { - "x": 0, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B13": { - "x": 4.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C13": { - "x": 9, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D13": { - "x": 13.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E13": { - "x": 18, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F13": { - "x": 22.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G13": { - "x": 27, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H13": { - "x": 31.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I13": { - "x": 36, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J13": { - "x": 40.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K13": { - "x": 45, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L13": { - "x": 49.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M13": { - "x": 54, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N13": { - "x": 58.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O13": { - "x": 63, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P13": { - "x": 67.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A14": { - "x": 0, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B14": { - "x": 4.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C14": { - "x": 9, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D14": { - "x": 13.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E14": { - "x": 18, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F14": { - "x": 22.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G14": { - "x": 27, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H14": { - "x": 31.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I14": { - "x": 36, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J14": { - "x": 40.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K14": { - "x": 45, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L14": { - "x": 49.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M14": { - "x": 54, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N14": { - "x": 58.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O14": { - "x": 63, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P14": { - "x": 67.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A15": { - "x": 0, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B15": { - "x": 4.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C15": { - "x": 9, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D15": { - "x": 13.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E15": { - "x": 18, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F15": { - "x": 22.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G15": { - "x": 27, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H15": { - "x": 31.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I15": { - "x": 36, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J15": { - "x": 40.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K15": { - "x": 45, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L15": { - "x": 49.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M15": { - "x": 54, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N15": { - "x": 58.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O15": { - "x": 63, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P15": { - "x": 67.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A16": { - "x": 0, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B16": { - "x": 4.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C16": { - "x": 9, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D16": { - "x": 13.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E16": { - "x": 18, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F16": { - "x": 22.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G16": { - "x": 27, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H16": { - "x": 31.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I16": { - "x": 36, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J16": { - "x": 40.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K16": { - "x": 45, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L16": { - "x": 49.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M16": { - "x": 54, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N16": { - "x": 58.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O16": { - "x": 63, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P16": { - "x": 67.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A17": { - "x": 0, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B17": { - "x": 4.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C17": { - "x": 9, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D17": { - "x": 13.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E17": { - "x": 18, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F17": { - "x": 22.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G17": { - "x": 27, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H17": { - "x": 31.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I17": { - "x": 36, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J17": { - "x": 40.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K17": { - "x": 45, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L17": { - "x": 49.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M17": { - "x": 54, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N17": { - "x": 58.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O17": { - "x": 63, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P17": { - "x": 67.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A18": { - "x": 0, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B18": { - "x": 4.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C18": { - "x": 9, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D18": { - "x": 13.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E18": { - "x": 18, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F18": { - "x": 22.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G18": { - "x": 27, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H18": { - "x": 31.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I18": { - "x": 36, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J18": { - "x": 40.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K18": { - "x": 45, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L18": { - "x": 49.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M18": { - "x": 54, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N18": { - "x": 58.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O18": { - "x": 63, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P18": { - "x": 67.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A19": { - "x": 0, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B19": { - "x": 4.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C19": { - "x": 9, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D19": { - "x": 13.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E19": { - "x": 18, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F19": { - "x": 22.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G19": { - "x": 27, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H19": { - "x": 31.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I19": { - "x": 36, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J19": { - "x": 40.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K19": { - "x": 45, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L19": { - "x": 49.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M19": { - "x": 54, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N19": { - "x": 58.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O19": { - "x": 63, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P19": { - "x": 67.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A20": { - "x": 0, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B20": { - "x": 4.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C20": { - "x": 9, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D20": { - "x": 13.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E20": { - "x": 18, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F20": { - "x": 22.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G20": { - "x": 27, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H20": { - "x": 31.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I20": { - "x": 36, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J20": { - "x": 40.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K20": { - "x": 45, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L20": { - "x": 49.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M20": { - "x": 54, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N20": { - "x": 58.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O20": { - "x": 63, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P20": { - "x": 67.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A21": { - "x": 0, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B21": { - "x": 4.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C21": { - "x": 9, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D21": { - "x": 13.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E21": { - "x": 18, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F21": { - "x": 22.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G21": { - "x": 27, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H21": { - "x": 31.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I21": { - "x": 36, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J21": { - "x": 40.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K21": { - "x": 45, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L21": { - "x": 49.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M21": { - "x": 54, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N21": { - "x": 58.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O21": { - "x": 63, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P21": { - "x": 67.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A22": { - "x": 0, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B22": { - "x": 4.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C22": { - "x": 9, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D22": { - "x": 13.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E22": { - "x": 18, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F22": { - "x": 22.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G22": { - "x": 27, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H22": { - "x": 31.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I22": { - "x": 36, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J22": { - "x": 40.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K22": { - "x": 45, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L22": { - "x": 49.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M22": { - "x": 54, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N22": { - "x": 58.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O22": { - "x": 63, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P22": { - "x": 67.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A23": { - "x": 0, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B23": { - "x": 4.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C23": { - "x": 9, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D23": { - "x": 13.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E23": { - "x": 18, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F23": { - "x": 22.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G23": { - "x": 27, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H23": { - "x": 31.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I23": { - "x": 36, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J23": { - "x": 40.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K23": { - "x": 45, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L23": { - "x": 49.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M23": { - "x": 54, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N23": { - "x": 58.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O23": { - "x": 63, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P23": { - "x": 67.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A24": { - "x": 0, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B24": { - "x": 4.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C24": { - "x": 9, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D24": { - "x": 13.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E24": { - "x": 18, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F24": { - "x": 22.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G24": { - "x": 27, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H24": { - "x": 31.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I24": { - "x": 36, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J24": { - "x": 40.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K24": { - "x": 45, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L24": { - "x": 49.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M24": { - "x": 54, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N24": { - "x": 58.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O24": { - "x": 63, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P24": { - "x": 67.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - } - } - }, - - "MALDI-plate": { - "origin-offset": { - "x": 9, - "y": 12 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B1": { - "x": 4.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D1": { - "x": 13.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F1": { - "x": 22.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H1": { - "x": 31.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J1": { - "x": 40.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L1": { - "x": 49.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N1": { - "x": 58.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P1": { - "x": 67.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A2": { - "x": 0, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B2": { - "x": 4.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C2": { - "x": 9, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D2": { - "x": 13.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E2": { - "x": 18, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F2": { - "x": 22.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G2": { - "x": 27, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H2": { - "x": 31.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I2": { - "x": 36, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J2": { - "x": 40.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K2": { - "x": 45, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L2": { - "x": 49.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M2": { - "x": 54, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N2": { - "x": 58.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O2": { - "x": 63, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P2": { - "x": 67.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A3": { - "x": 0, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B3": { - "x": 4.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C3": { - "x": 9, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D3": { - "x": 13.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E3": { - "x": 18, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F3": { - "x": 22.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G3": { - "x": 27, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H3": { - "x": 31.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I3": { - "x": 36, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J3": { - "x": 40.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K3": { - "x": 45, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L3": { - "x": 49.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M3": { - "x": 54, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N3": { - "x": 58.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O3": { - "x": 63, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P3": { - "x": 67.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A4": { - "x": 0, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B4": { - "x": 4.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C4": { - "x": 9, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D4": { - "x": 13.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E4": { - "x": 18, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F4": { - "x": 22.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G4": { - "x": 27, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H4": { - "x": 31.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I4": { - "x": 36, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J4": { - "x": 40.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K4": { - "x": 45, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L4": { - "x": 49.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M4": { - "x": 54, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N4": { - "x": 58.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O4": { - "x": 63, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P4": { - "x": 67.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A5": { - "x": 0, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B5": { - "x": 4.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C5": { - "x": 9, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D5": { - "x": 13.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E5": { - "x": 18, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F5": { - "x": 22.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G5": { - "x": 27, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H5": { - "x": 31.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I5": { - "x": 36, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J5": { - "x": 40.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K5": { - "x": 45, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L5": { - "x": 49.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M5": { - "x": 54, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N5": { - "x": 58.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O5": { - "x": 63, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P5": { - "x": 67.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A6": { - "x": 0, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B6": { - "x": 4.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C6": { - "x": 9, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D6": { - "x": 13.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E6": { - "x": 18, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F6": { - "x": 22.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G6": { - "x": 27, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H6": { - "x": 31.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I6": { - "x": 36, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J6": { - "x": 40.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K6": { - "x": 45, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L6": { - "x": 49.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M6": { - "x": 54, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N6": { - "x": 58.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O6": { - "x": 63, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P6": { - "x": 67.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A7": { - "x": 0, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B7": { - "x": 4.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C7": { - "x": 9, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D7": { - "x": 13.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E7": { - "x": 18, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F7": { - "x": 22.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G7": { - "x": 27, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H7": { - "x": 31.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I7": { - "x": 36, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J7": { - "x": 40.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K7": { - "x": 45, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L7": { - "x": 49.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M7": { - "x": 54, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N7": { - "x": 58.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O7": { - "x": 63, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P7": { - "x": 67.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A8": { - "x": 0, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B8": { - "x": 4.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C8": { - "x": 9, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D8": { - "x": 13.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E8": { - "x": 18, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F8": { - "x": 22.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G8": { - "x": 27, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H8": { - "x": 31.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I8": { - "x": 36, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J8": { - "x": 40.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K8": { - "x": 45, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L8": { - "x": 49.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M8": { - "x": 54, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N8": { - "x": 58.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O8": { - "x": 63, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P8": { - "x": 67.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A9": { - "x": 0, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B9": { - "x": 4.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C9": { - "x": 9, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D9": { - "x": 13.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E9": { - "x": 18, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F9": { - "x": 22.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G9": { - "x": 27, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H9": { - "x": 31.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I9": { - "x": 36, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J9": { - "x": 40.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K9": { - "x": 45, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L9": { - "x": 49.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M9": { - "x": 54, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N9": { - "x": 58.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O9": { - "x": 63, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P9": { - "x": 67.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A10": { - "x": 0, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B10": { - "x": 4.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C10": { - "x": 9, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D10": { - "x": 13.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E10": { - "x": 18, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F10": { - "x": 22.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G10": { - "x": 27, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H10": { - "x": 31.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I10": { - "x": 36, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J10": { - "x": 40.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K10": { - "x": 45, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L10": { - "x": 49.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M10": { - "x": 54, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N10": { - "x": 58.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O10": { - "x": 63, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P10": { - "x": 67.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A11": { - "x": 0, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B11": { - "x": 4.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C11": { - "x": 9, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D11": { - "x": 13.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E11": { - "x": 18, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F11": { - "x": 22.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G11": { - "x": 27, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H11": { - "x": 31.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I11": { - "x": 36, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J11": { - "x": 40.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K11": { - "x": 45, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L11": { - "x": 49.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M11": { - "x": 54, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N11": { - "x": 58.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O11": { - "x": 63, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P11": { - "x": 67.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A12": { - "x": 0, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B12": { - "x": 4.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C12": { - "x": 9, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D12": { - "x": 13.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E12": { - "x": 18, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F12": { - "x": 22.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G12": { - "x": 27, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H12": { - "x": 31.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I12": { - "x": 36, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J12": { - "x": 40.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K12": { - "x": 45, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L12": { - "x": 49.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M12": { - "x": 54, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N12": { - "x": 58.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O12": { - "x": 63, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P12": { - "x": 67.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A13": { - "x": 0, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B13": { - "x": 4.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C13": { - "x": 9, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D13": { - "x": 13.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E13": { - "x": 18, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F13": { - "x": 22.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G13": { - "x": 27, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H13": { - "x": 31.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I13": { - "x": 36, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J13": { - "x": 40.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K13": { - "x": 45, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L13": { - "x": 49.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M13": { - "x": 54, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N13": { - "x": 58.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O13": { - "x": 63, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P13": { - "x": 67.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A14": { - "x": 0, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B14": { - "x": 4.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C14": { - "x": 9, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D14": { - "x": 13.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E14": { - "x": 18, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F14": { - "x": 22.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G14": { - "x": 27, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H14": { - "x": 31.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I14": { - "x": 36, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J14": { - "x": 40.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K14": { - "x": 45, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L14": { - "x": 49.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M14": { - "x": 54, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N14": { - "x": 58.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O14": { - "x": 63, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P14": { - "x": 67.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A15": { - "x": 0, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B15": { - "x": 4.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C15": { - "x": 9, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D15": { - "x": 13.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E15": { - "x": 18, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F15": { - "x": 22.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G15": { - "x": 27, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H15": { - "x": 31.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I15": { - "x": 36, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J15": { - "x": 40.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K15": { - "x": 45, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L15": { - "x": 49.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M15": { - "x": 54, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N15": { - "x": 58.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O15": { - "x": 63, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P15": { - "x": 67.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A16": { - "x": 0, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B16": { - "x": 4.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C16": { - "x": 9, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D16": { - "x": 13.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E16": { - "x": 18, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F16": { - "x": 22.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G16": { - "x": 27, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H16": { - "x": 31.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I16": { - "x": 36, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J16": { - "x": 40.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K16": { - "x": 45, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L16": { - "x": 49.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M16": { - "x": 54, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N16": { - "x": 58.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O16": { - "x": 63, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P16": { - "x": 67.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A17": { - "x": 0, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B17": { - "x": 4.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C17": { - "x": 9, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D17": { - "x": 13.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E17": { - "x": 18, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F17": { - "x": 22.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G17": { - "x": 27, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H17": { - "x": 31.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I17": { - "x": 36, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J17": { - "x": 40.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K17": { - "x": 45, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L17": { - "x": 49.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M17": { - "x": 54, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N17": { - "x": 58.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O17": { - "x": 63, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P17": { - "x": 67.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A18": { - "x": 0, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B18": { - "x": 4.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C18": { - "x": 9, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D18": { - "x": 13.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E18": { - "x": 18, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F18": { - "x": 22.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G18": { - "x": 27, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H18": { - "x": 31.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I18": { - "x": 36, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J18": { - "x": 40.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K18": { - "x": 45, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L18": { - "x": 49.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M18": { - "x": 54, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N18": { - "x": 58.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O18": { - "x": 63, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P18": { - "x": 67.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A19": { - "x": 0, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B19": { - "x": 4.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C19": { - "x": 9, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D19": { - "x": 13.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E19": { - "x": 18, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F19": { - "x": 22.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G19": { - "x": 27, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H19": { - "x": 31.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I19": { - "x": 36, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J19": { - "x": 40.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K19": { - "x": 45, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L19": { - "x": 49.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M19": { - "x": 54, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N19": { - "x": 58.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O19": { - "x": 63, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P19": { - "x": 67.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A20": { - "x": 0, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B20": { - "x": 4.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C20": { - "x": 9, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D20": { - "x": 13.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E20": { - "x": 18, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F20": { - "x": 22.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G20": { - "x": 27, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H20": { - "x": 31.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I20": { - "x": 36, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J20": { - "x": 40.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K20": { - "x": 45, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L20": { - "x": 49.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M20": { - "x": 54, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N20": { - "x": 58.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O20": { - "x": 63, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P20": { - "x": 67.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A21": { - "x": 0, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B21": { - "x": 4.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C21": { - "x": 9, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D21": { - "x": 13.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E21": { - "x": 18, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F21": { - "x": 22.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G21": { - "x": 27, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H21": { - "x": 31.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I21": { - "x": 36, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J21": { - "x": 40.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K21": { - "x": 45, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L21": { - "x": 49.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M21": { - "x": 54, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N21": { - "x": 58.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O21": { - "x": 63, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P21": { - "x": 67.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A22": { - "x": 0, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B22": { - "x": 4.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C22": { - "x": 9, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D22": { - "x": 13.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E22": { - "x": 18, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F22": { - "x": 22.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G22": { - "x": 27, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H22": { - "x": 31.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I22": { - "x": 36, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J22": { - "x": 40.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K22": { - "x": 45, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L22": { - "x": 49.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M22": { - "x": 54, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N22": { - "x": 58.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O22": { - "x": 63, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P22": { - "x": 67.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A23": { - "x": 0, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B23": { - "x": 4.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C23": { - "x": 9, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D23": { - "x": 13.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E23": { - "x": 18, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F23": { - "x": 22.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G23": { - "x": 27, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H23": { - "x": 31.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I23": { - "x": 36, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J23": { - "x": 40.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K23": { - "x": 45, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L23": { - "x": 49.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M23": { - "x": 54, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N23": { - "x": 58.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O23": { - "x": 63, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P23": { - "x": 67.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A24": { - "x": 0, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B24": { - "x": 4.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C24": { - "x": 9, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D24": { - "x": 13.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E24": { - "x": 18, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F24": { - "x": 22.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G24": { - "x": 27, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H24": { - "x": 31.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I24": { - "x": 36, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J24": { - "x": 40.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K24": { - "x": 45, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L24": { - "x": 49.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M24": { - "x": 54, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N24": { - "x": 58.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O24": { - "x": 63, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P24": { - "x": 67.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - } - } - }, - - "48-vial-plate": { - "origin-offset": { - "x": 10.5, - "y": 18 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 13, - "y": 0, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 26, - "y": 0, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 39, - "y": 0, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E1": { - "x": 52, - "y": 0, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F1": { - "x": 65, - "y": 0, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A2": { - "x": 0, - "y": 13, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 13, - "y": 13, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 26, - "y": 13, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 39, - "y": 13, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E2": { - "x": 52, - "y": 13, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F2": { - "x": 65, - "y": 13, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A3": { - "x": 0, - "y": 26, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 13, - "y": 26, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 26, - "y": 26, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 39, - "y": 26, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E3": { - "x": 52, - "y": 26, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F3": { - "x": 65, - "y": 26, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A4": { - "x": 0, - "y": 39, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 13, - "y": 39, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 26, - "y": 39, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 39, - "y": 39, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E4": { - "x": 52, - "y": 39, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F4": { - "x": 65, - "y": 39, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A5": { - "x": 0, - "y": 52, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 13, - "y": 52, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 26, - "y": 52, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 39, - "y": 52, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E5": { - "x": 52, - "y": 52, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F5": { - "x": 65, - "y": 52, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A6": { - "x": 0, - "y": 65, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 13, - "y": 65, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 26, - "y": 65, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 39, - "y": 65, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E6": { - "x": 52, - "y": 65, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F6": { - "x": 65, - "y": 65, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A7": { - "x": 0, - "y": 78, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B7": { - "x": 13, - "y": 78, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C7": { - "x": 26, - "y": 78, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D7": { - "x": 39, - "y": 78, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E7": { - "x": 52, - "y": 78, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F7": { - "x": 65, - "y": 78, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A8": { - "x": 0, - "y": 91, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B8": { - "x": 13, - "y": 91, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C8": { - "x": 26, - "y": 91, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D8": { - "x": 39, - "y": 91, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E8": { - "x": 52, - "y": 91, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F8": { - "x": 65, - "y": 91, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - } - } - }, - - "e-gelgol": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - } - } - }, - - "5ml-3x4": { - "origin-offset": { - "x": 18, - "y": 19 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "B1": { - "x": 25, - "y": 0, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "C1": { - "x": 50, - "y": 0, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "A2": { - "x": 0, - "y": 30, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "B2": { - "x": 25, - "y": 30, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "C2": { - "x": 50, - "y": 30, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "A3": { - "x": 0, - "y": 60, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "B3": { - "x": 25, - "y": 60, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "C3": { - "x": 50, - "y": 60, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "A4": { - "x": 0, - "y": 90, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "B4": { - "x": 25, - "y": 90, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "C4": { - "x": 50, - "y": 90, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - } - } - }, - - "small_vial_rack_16x45": { - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "B1": { - "x": 40, - "y": 0, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "C1": { - "x": 80, - "y": 0, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "D1": { - "x": 120, - "y": 0, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "A2": { - "x": 0, - "y": 20, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "B2": { - "x": 40, - "y": 20, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "C2": { - "x": 80, - "y": 20, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "D2": { - "x": 120, - "y": 20, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "A3": { - "x": 0, - "y": 40, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "B3": { - "x": 40, - "y": 40, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "C3": { - "x": 80, - "y": 40, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "D3": { - "x": 120, - "y": 40, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "A4": { - "x": 0, - "y": 60, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "B4": { - "x": 40, - "y": 60, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "C4": { - "x": 80, - "y": 60, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "D4": { - "x": 120, - "y": 60, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "A5": { - "x": 0, - "y": 80, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "B5": { - "x": 40, - "y": 80, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "C5": { - "x": 80, - "y": 80, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "D5": { - "x": 120, - "y": 80, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "A6": { - "x": 0, - "y": 100, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "B6": { - "x": 40, - "y": 100, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "C6": { - "x": 80, - "y": 100, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "D6": { - "x": 120, - "y": 100, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - } - } - }, - - "opentrons-tuberack-15ml": { - "origin-offset": { - "x": 34.375, - "y": 13.5 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B1": { - "x": 25, - "y": 0, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C1": { - "x": 50, - "y": 0, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "A2": { - "x": 0, - "y": 25, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B2": { - "x": 25, - "y": 25, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C2": { - "x": 50, - "y": 25, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "A3": { - "x": 0, - "y": 50, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B3": { - "x": 25, - "y": 50, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C3": { - "x": 50, - "y": 50, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "A4": { - "x": 0, - "y": 75, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B4": { - "x": 25, - "y": 75, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C4": { - "x": 50, - "y": 75, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "A5": { - "x": 0, - "y": 100, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B5": { - "x": 25, - "y": 100, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C5": { - "x": 50, - "y": 100, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - } - } - }, - - "opentrons-tuberack-50ml": { - "origin-offset": { - "x": 39.875, - "y": 37 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 6.95, - "depth": 112.85, - "diameter": 17, - "total-liquid-volume": 50000 - }, - "B1": { - "x": 35, - "y": 0, - "z": 6.95, - "depth": 112.85, - "diameter": 17, - "total-liquid-volume": 50000 - }, - "A2": { - "x": 0, - "y": 35, - "z": 6.95, - "depth": 112.85, - "diameter": 17, - "total-liquid-volume": 50000 - }, - "B2": { - "x": 35, - "y": 35, - "z": 6.95, - "depth": 112.85, - "diameter": 17, - "total-liquid-volume": 50000 - }, - "A3": { - "x": 0, - "y": 70, - "z": 6.95, - "depth": 112.85, - "diameter": 17, - "total-liquid-volume": 50000 - }, - "B3": { - "x": 35, - "y": 70, - "z": 6.95, - "depth": 112.85, - "diameter": 17, - "total-liquid-volume": 50000 - } - } - }, - - "opentrons-tuberack-15_50ml": { - "origin-offset": { - "x": 32.75, - "y": 14.875 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 6.78, - "depth": 117.98, - "diameter": 14.5, - "total-liquid-volume": 15000 - }, - "B1": { - "x": 25, - "y": 0, - "z": 6.78, - "depth": 117.98, - "diameter": 14.5, - "total-liquid-volume": 15000 - }, - "C1": { - "x": 50, - "y": 0, - "z": 6.78, - "depth": 117.98, - "diameter": 14.5, - "total-liquid-volume": 15000 - }, - "A2": { - "x": 0, - "y": 25, - "z": 6.78, - "depth": 117.98, - "diameter": 14.5, - "total-liquid-volume": 15000 - }, - "B2": { - "x": 25, - "y": 25, - "z": 6.78, - "depth": 117.98, - "diameter": 14.5, - "total-liquid-volume": 15000 - }, - "C2": { - "x": 50, - "y": 25, - "z": 6.78, - "depth": 117.98, - "diameter": 14.5, - "total-liquid-volume": 15000 - }, - "A3": { - "x": 18.25, - "y": 57.5, - "z": 6.95, - "depth": 112.85, - "diameter": 26.45, - "total-liquid-volume": 50000 - }, - "B3": { - "x": 53.25, - "y": 57.5, - "z": 6.95, - "depth": 112.85, - "diameter": 26.45, - "total-liquid-volume": 50000 - }, - "A4": { - "x": 18.25, - "y": 92.5, - "z": 6.95, - "depth": 112.85, - "diameter": 26.45, - "total-liquid-volume": 50000 - }, - "B4": { - "x": 53.25, - "y": 92.5, - "z": 6.95, - "depth": 112.85, - "diameter": 26.45, - "total-liquid-volume": 50000 - } - } - }, - - "opentrons-tuberack-2ml-eppendorf": { - "origin-offset": { - "x": 21.07, - "y": 18.21 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 19.28, - "y": 0, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 38.56, - "y": 0, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 57.84, - "y": 0, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 19.89, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 19.28, - "y": 19.89, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 38.56, - "y": 19.89, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 57.84, - "y": 19.89, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 39.78, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 19.28, - "y": 39.78, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 38.56, - "y": 39.78, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 57.84, - "y": 39.78, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 59.67, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 19.28, - "y": 59.67, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 38.56, - "y": 59.67, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 57.84, - "y": 59.67, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 79.56, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 19.28, - "y": 79.56, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 38.56, - "y": 79.56, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 57.84, - "y": 79.56, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 99.45, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 19.28, - "y": 99.45, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 38.56, - "y": 99.45, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 57.84, - "y": 99.45, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - } - } - }, - - "opentrons-tuberack-2ml-screwcap": { - "origin-offset": { - "x": 21.07, - "y": 18.21 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 19.28, - "y": 0, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 38.56, - "y": 0, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 57.84, - "y": 0, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 19.89, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 19.28, - "y": 19.89, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 38.56, - "y": 19.89, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 57.84, - "y": 19.89, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 39.78, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 19.28, - "y": 39.78, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 38.56, - "y": 39.78, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 57.84, - "y": 39.78, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 59.67, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 19.28, - "y": 59.67, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 38.56, - "y": 59.67, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 57.84, - "y": 59.67, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 79.56, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 19.28, - "y": 79.56, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 38.56, - "y": 79.56, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 57.84, - "y": 79.56, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 99.45, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 19.28, - "y": 99.45, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 38.56, - "y": 99.45, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 57.84, - "y": 99.45, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - } - } - }, - - "opentrons-tuberack-1.5ml-eppendorf": { - "origin-offset": { - "x": 21.07, - "y": 18.21 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "B1": { - "x": 19.28, - "y": 0, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "C1": { - "x": 38.56, - "y": 0, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "D1": { - "x": 57.84, - "y": 0, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "A2": { - "x": 0, - "y": 19.89, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "B2": { - "x": 19.28, - "y": 19.89, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "C2": { - "x": 38.56, - "y": 19.89, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "D2": { - "x": 57.84, - "y": 19.89, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "A3": { - "x": 0, - "y": 39.78, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "B3": { - "x": 19.28, - "y": 39.78, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "C3": { - "x": 38.56, - "y": 39.78, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "D3": { - "x": 57.84, - "y": 39.78, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "A4": { - "x": 0, - "y": 59.67, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "B4": { - "x": 19.28, - "y": 59.67, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "C4": { - "x": 38.56, - "y": 59.67, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "D4": { - "x": 57.84, - "y": 59.67, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "A5": { - "x": 0, - "y": 79.56, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "B5": { - "x": 19.28, - "y": 79.56, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "C5": { - "x": 38.56, - "y": 79.56, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "D5": { - "x": 57.84, - "y": 79.56, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "A6": { - "x": 0, - "y": 99.45, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "B6": { - "x": 19.28, - "y": 99.45, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "C6": { - "x": 38.56, - "y": 99.45, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "D6": { - "x": 57.84, - "y": 99.45, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - } - } - }, - - "opentrons-aluminum-block-2ml-eppendorf": { - "origin-offset": { - "x": 25.88, - "y": 20.75 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 17.25, - "y": 0, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 34.5, - "y": 0, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 51.75, - "y": 0, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 17.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 17.25, - "y": 17.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 34.5, - "y": 17.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 51.75, - "y": 17.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 34.5, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 17.25, - "y": 34.5, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 34.5, - "y": 34.5, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 51.75, - "y": 34.5, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 51.75, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 17.25, - "y": 51.75, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 34.5, - "y": 51.75, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 51.75, - "y": 51.75, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 69, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 17.25, - "y": 69, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 34.5, - "y": 69, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 51.75, - "y": 69, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 86.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 17.25, - "y": 86.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 34.5, - "y": 86.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 51.75, - "y": 86.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - } - } - }, - "opentrons-aluminum-block-2ml-screwcap": { - "origin-offset": { - "x": 25.88, - "y": 20.75 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 17.25, - "y": 0, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 34.5, - "y": 0, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 51.75, - "y": 0, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 17.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 17.25, - "y": 17.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 34.5, - "y": 17.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 51.75, - "y": 17.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 34.5, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 17.25, - "y": 34.5, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 34.5, - "y": 34.5, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 51.75, - "y": 34.5, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 51.75, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 17.25, - "y": 51.75, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 34.5, - "y": 51.75, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 51.75, - "y": 51.75, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 69, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 17.25, - "y": 69, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 34.5, - "y": 69, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 51.75, - "y": 69, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 86.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 17.25, - "y": 86.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 34.5, - "y": 86.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 51.75, - "y": 86.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - } - } - }, - "opentrons-aluminum-block-96-PCR-plate": { - "origin-offset": { - "x": 17.25, - "y": 13.38 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B1": { - "x": 9, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C1": { - "x": 18, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D1": { - "x": 27, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E1": { - "x": 36, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F1": { - "x": 45, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G1": { - "x": 54, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H1": { - "x": 63, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A2": { - "x": 0, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B2": { - "x": 9, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C2": { - "x": 18, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D2": { - "x": 27, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E2": { - "x": 36, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F2": { - "x": 45, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G2": { - "x": 54, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H2": { - "x": 63, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A3": { - "x": 0, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B3": { - "x": 9, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C3": { - "x": 18, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D3": { - "x": 27, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E3": { - "x": 36, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F3": { - "x": 45, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G3": { - "x": 54, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H3": { - "x": 63, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A4": { - "x": 0, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B4": { - "x": 9, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C4": { - "x": 18, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D4": { - "x": 27, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E4": { - "x": 36, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F4": { - "x": 45, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G4": { - "x": 54, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H4": { - "x": 63, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A5": { - "x": 0, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B5": { - "x": 9, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C5": { - "x": 18, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D5": { - "x": 27, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E5": { - "x": 36, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F5": { - "x": 45, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G5": { - "x": 54, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H5": { - "x": 63, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A6": { - "x": 0, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B6": { - "x": 9, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C6": { - "x": 18, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D6": { - "x": 27, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E6": { - "x": 36, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F6": { - "x": 45, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G6": { - "x": 54, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H6": { - "x": 63, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A7": { - "x": 0, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B7": { - "x": 9, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C7": { - "x": 18, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D7": { - "x": 27, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E7": { - "x": 36, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F7": { - "x": 45, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G7": { - "x": 54, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H7": { - "x": 63, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A8": { - "x": 0, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B8": { - "x": 9, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C8": { - "x": 18, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D8": { - "x": 27, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E8": { - "x": 36, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F8": { - "x": 45, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G8": { - "x": 54, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H8": { - "x": 63, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A9": { - "x": 0, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B9": { - "x": 9, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C9": { - "x": 18, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D9": { - "x": 27, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E9": { - "x": 36, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F9": { - "x": 45, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G9": { - "x": 54, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H9": { - "x": 63, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A10": { - "x": 0, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B10": { - "x": 9, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C10": { - "x": 18, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D10": { - "x": 27, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E10": { - "x": 36, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F10": { - "x": 45, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G10": { - "x": 54, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H10": { - "x": 63, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A11": { - "x": 0, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B11": { - "x": 9, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C11": { - "x": 18, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D11": { - "x": 27, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E11": { - "x": 36, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F11": { - "x": 45, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G11": { - "x": 54, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H11": { - "x": 63, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A12": { - "x": 0, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B12": { - "x": 9, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C12": { - "x": 18, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D12": { - "x": 27, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E12": { - "x": 36, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F12": { - "x": 45, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G12": { - "x": 54, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H12": { - "x": 63, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - } - } - }, - "opentrons-aluminum-block-PCR-strips-200ul": { - "origin-offset": { - "x": 17.25, - "y": 13.38 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B1": { - "x": 9, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C1": { - "x": 18, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D1": { - "x": 27, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E1": { - "x": 36, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F1": { - "x": 45, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G1": { - "x": 54, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H1": { - "x": 63, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A2": { - "x": 0, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B2": { - "x": 9, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C2": { - "x": 18, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D2": { - "x": 27, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E2": { - "x": 36, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F2": { - "x": 45, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G2": { - "x": 54, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H2": { - "x": 63, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A3": { - "x": 0, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B3": { - "x": 9, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C3": { - "x": 18, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D3": { - "x": 27, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E3": { - "x": 36, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F3": { - "x": 45, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G3": { - "x": 54, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H3": { - "x": 63, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A4": { - "x": 0, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B4": { - "x": 9, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C4": { - "x": 18, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D4": { - "x": 27, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E4": { - "x": 36, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F4": { - "x": 45, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G4": { - "x": 54, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H4": { - "x": 63, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A5": { - "x": 0, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B5": { - "x": 9, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C5": { - "x": 18, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D5": { - "x": 27, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E5": { - "x": 36, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F5": { - "x": 45, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G5": { - "x": 54, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H5": { - "x": 63, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A6": { - "x": 0, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B6": { - "x": 9, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C6": { - "x": 18, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D6": { - "x": 27, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E6": { - "x": 36, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F6": { - "x": 45, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G6": { - "x": 54, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H6": { - "x": 63, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A7": { - "x": 0, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B7": { - "x": 9, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C7": { - "x": 18, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D7": { - "x": 27, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E7": { - "x": 36, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F7": { - "x": 45, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G7": { - "x": 54, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H7": { - "x": 63, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A8": { - "x": 0, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B8": { - "x": 9, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C8": { - "x": 18, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D8": { - "x": 27, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E8": { - "x": 36, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F8": { - "x": 45, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G8": { - "x": 54, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H8": { - "x": 63, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A9": { - "x": 0, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B9": { - "x": 9, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C9": { - "x": 18, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D9": { - "x": 27, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E9": { - "x": 36, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F9": { - "x": 45, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G9": { - "x": 54, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H9": { - "x": 63, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A10": { - "x": 0, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B10": { - "x": 9, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C10": { - "x": 18, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D10": { - "x": 27, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E10": { - "x": 36, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F10": { - "x": 45, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G10": { - "x": 54, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H10": { - "x": 63, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A11": { - "x": 0, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B11": { - "x": 9, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C11": { - "x": 18, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D11": { - "x": 27, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E11": { - "x": 36, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F11": { - "x": 45, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G11": { - "x": 54, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H11": { - "x": 63, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A12": { - "x": 0, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B12": { - "x": 9, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C12": { - "x": 18, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D12": { - "x": 27, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E12": { - "x": 36, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F12": { - "x": 45, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G12": { - "x": 54, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H12": { - "x": 63, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - } - } - } - } -} From 733da90a1762da166fa46e61dc5dfb1f605313cb Mon Sep 17 00:00:00 2001 From: Shlok Amin Date: Mon, 13 Nov 2023 17:40:44 -0500 Subject: [PATCH 05/46] refactor(shared-data): update movable trash offset from cutout fixture (#13976) --- shared-data/deck/definitions/4/ot3_standard.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shared-data/deck/definitions/4/ot3_standard.json b/shared-data/deck/definitions/4/ot3_standard.json index 87ba7e78e14..728f9743299 100644 --- a/shared-data/deck/definitions/4/ot3_standard.json +++ b/shared-data/deck/definitions/4/ot3_standard.json @@ -252,7 +252,7 @@ { "id": "movableTrashD1", "areaType": "movableTrash", - "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "offsetFromCutoutFixture": [-101.5, -2.75, 0.0], "boundingBox": { "xDimension": 246.5, "yDimension": 91.5, @@ -265,7 +265,7 @@ { "id": "movableTrashC1", "areaType": "movableTrash", - "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "offsetFromCutoutFixture": [-101.5, -2.75, 0.0], "boundingBox": { "xDimension": 246.5, "yDimension": 91.5, @@ -278,7 +278,7 @@ { "id": "movableTrashB1", "areaType": "movableTrash", - "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "offsetFromCutoutFixture": [-101.5, -2.75, 0.0], "boundingBox": { "xDimension": 246.5, "yDimension": 91.5, @@ -291,7 +291,7 @@ { "id": "movableTrashA1", "areaType": "movableTrash", - "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "offsetFromCutoutFixture": [-101.5, -2.75, 0.0], "boundingBox": { "xDimension": 246.5, "yDimension": 91.5, From e401da0f192e1e842633d173620eeaeb9ea101b9 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 14 Nov 2023 08:23:04 -0500 Subject: [PATCH 06/46] feat(step-generation): wire up moveLabware into waste chute (#13950) closes RAUT-778 --- .../src/load-file/migration/8_0_0.ts | 2 +- .../src/localization/en/alert.json | 4 ++ .../src/steplist/fieldLevel/index.ts | 18 ++++++ .../ui/labware/__tests__/selectors.test.ts | 9 ++- protocol-designer/src/ui/labware/selectors.ts | 28 ++++++-- shared-data/js/types.ts | 2 +- .../src/__tests__/moveLabware.test.ts | 64 ++++++++++++++++++- .../src/commandCreators/atomic/moveLabware.ts | 30 +++++---- step-generation/src/errorCreators.ts | 7 ++ .../forMoveLabware.ts | 2 + step-generation/src/types.ts | 1 + 11 files changed, 143 insertions(+), 24 deletions(-) diff --git a/protocol-designer/src/load-file/migration/8_0_0.ts b/protocol-designer/src/load-file/migration/8_0_0.ts index c7afb0bbffe..734f2f9ea46 100644 --- a/protocol-designer/src/load-file/migration/8_0_0.ts +++ b/protocol-designer/src/load-file/migration/8_0_0.ts @@ -18,12 +18,12 @@ import type { CommandV8Mixin, LabwareV2Mixin, LiquidV1Mixin, + LoadLabwareCreateCommand, OT2RobotMixin, OT3RobotMixin, ProtocolBase, ProtocolFile, } from '@opentrons/shared-data/protocol/types/schemaV8' -import type { LoadLabwareCreateCommand } from '@opentrons/shared-data/protocol/types/schemaV7' import type { DesignerApplicationData } from './utils/getLoadLiquidCommands' // NOTE: this migration is to schema v8 and updates fixed trash by diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index bf391dfa358..d4412ea07a8 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -163,6 +163,10 @@ "ADDITIONAL_EQUIPMENT_DOES_NOT_EXIST": { "title": "{{additionalEquipment}} does not exist", "body": "Attempting to interact with an unknown entity." + }, + "GRIPPER_REQUIRED": { + "title": "A gripper is required to complete this action", + "body": "Attempting to move a labware without a gripper into the waste chute. Please add a gripper to this step." } }, "warning": { diff --git a/protocol-designer/src/steplist/fieldLevel/index.ts b/protocol-designer/src/steplist/fieldLevel/index.ts index 0eae673ff73..d9a86262b12 100644 --- a/protocol-designer/src/steplist/fieldLevel/index.ts +++ b/protocol-designer/src/steplist/fieldLevel/index.ts @@ -42,6 +42,7 @@ import { PipetteEntity, InvariantContext, LabwareEntities, + AdditionalEquipmentEntities, } from '@opentrons/step-generation' import { StepFieldName } from '../../form-types' import type { LabwareLocation } from '@opentrons/shared-data' @@ -64,6 +65,14 @@ const getIsAdapterLocation = ( labwareEntities[newLocation].def.allowedRoles?.includes('adapter') ?? false ) } +const getIsWasteChuteLocation = ( + newLocation: string, + additionalEquipmentEntities: AdditionalEquipmentEntities +): boolean => + Object.values(additionalEquipmentEntities).find( + aE => aE.location === newLocation && aE.name === 'wasteChute' + ) != null + const getLabwareLocation = ( state: InvariantContext, newLocationString: string @@ -77,6 +86,15 @@ const getLabwareLocation = ( getIsAdapterLocation(newLocationString, state.labwareEntities) ) { return { labwareId: newLocationString } + } else if ( + getIsWasteChuteLocation( + newLocationString, + state.additionalEquipmentEntities + ) + ) { + return { + addressableAreaName: 'gripperWasteChute', + } } else { return { slotName: newLocationString } } diff --git a/protocol-designer/src/ui/labware/__tests__/selectors.test.ts b/protocol-designer/src/ui/labware/__tests__/selectors.test.ts index c3388d91825..2e3ba647add 100644 --- a/protocol-designer/src/ui/labware/__tests__/selectors.test.ts +++ b/protocol-designer/src/ui/labware/__tests__/selectors.test.ts @@ -149,6 +149,7 @@ describe('labware selectors', () => { names, initialDeckSetup, {}, + {}, {} ) ).toEqual([ @@ -179,6 +180,7 @@ describe('labware selectors', () => { names, initialDeckSetup, presavedStepForm, + {}, {} ) ).toEqual([ @@ -261,6 +263,7 @@ describe('labware selectors', () => { nicknames, initialDeckSetup, {}, + {}, {} ) ).toEqual([ @@ -317,11 +320,13 @@ describe('labware selectors', () => { labwareEntities, nicknames, initialDeckSetup, - savedStep + {}, + savedStep, + {} ) ).toEqual([ { name: 'Trash', value: mockTrash }, - { name: 'Well Plate in Magnetic Module', value: 'wellPlateId' }, + { name: 'Well Plate', value: 'wellPlateId' }, ]) }) }) diff --git a/protocol-designer/src/ui/labware/selectors.ts b/protocol-designer/src/ui/labware/selectors.ts index 7ee927c1ff7..f14923369a0 100644 --- a/protocol-designer/src/ui/labware/selectors.ts +++ b/protocol-designer/src/ui/labware/selectors.ts @@ -48,14 +48,20 @@ export const getLabwareOptions: Selector = createSelector( stepFormSelectors.getInitialDeckSetup, stepFormSelectors.getPresavedStepForm, stepFormSelectors.getSavedStepForms, + stepFormSelectors.getAdditionalEquipmentEntities, ( labwareEntities, nicknamesById, initialDeckSetup, presavedStepForm, - savedStepForms + savedStepForms, + additionalEquipmentEntities ) => { const moveLabwarePresavedStep = presavedStepForm?.stepType === 'moveLabware' + const wasteChuteLocation = Object.values(additionalEquipmentEntities).find( + aE => aE.name === 'wasteChute' + )?.location + const options = reduce( labwareEntities, ( @@ -63,6 +69,13 @@ export const getLabwareOptions: Selector = createSelector( labwareEntity: LabwareEntity, labwareId: string ): Options => { + const isLabwareInWasteChute = Object.values(savedStepForms).find( + form => + form.stepType === 'moveLabware' && + form.labware === labwareId && + form.newLocation === wasteChuteLocation + ) + const isAdapter = labwareEntity.def.allowedRoles?.includes('adapter') const isOffDeck = getLabwareOffDeck( initialDeckSetup, @@ -94,7 +107,11 @@ export const getLabwareOptions: Selector = createSelector( } if (!moveLabwarePresavedStep) { - return getIsTiprack(labwareEntity.def) || isAdapter + // filter out tip racks, adapters, and labware in waste chute + // for aspirating/dispensing/mixing into + return getIsTiprack(labwareEntity.def) || + isAdapter || + isLabwareInWasteChute ? acc : [ ...acc, @@ -104,8 +121,11 @@ export const getLabwareOptions: Selector = createSelector( }, ] } else { - // filter out moving trash for now in MoveLabware step type - return nickName === TRASH || isAdapterOrAluminumBlock + // filter out moving trash, aluminum blocks, adapters and labware in + // waste chute for moveLabware + return nickName === TRASH || + isAdapterOrAluminumBlock || + isLabwareInWasteChute ? acc : [ ...acc, diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 3d0ad89ff3e..79e84d2af33 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -31,8 +31,8 @@ import { } from './constants' import type { INode } from 'svgson' import type { RunTimeCommand, LabwareLocation } from '../command/types' -import type { PipetteName } from './pipettes' import type { AddressableAreaName, CutoutFixtureId, CutoutId } from '../deck' +import type { PipetteName } from './pipettes' export type RobotType = 'OT-2 Standard' | 'OT-3 Standard' diff --git a/step-generation/src/__tests__/moveLabware.test.ts b/step-generation/src/__tests__/moveLabware.test.ts index f6d8ac69c1f..12ecf2e46a8 100644 --- a/step-generation/src/__tests__/moveLabware.test.ts +++ b/step-generation/src/__tests__/moveLabware.test.ts @@ -17,6 +17,7 @@ import { moveLabware, MoveLabwareArgs } from '..' import type { InvariantContext, RobotState } from '../types' const mockWasteChuteId = 'mockWasteChuteId' +const mockGripperId = 'mockGripperId' describe('moveLabware', () => { let robotState: RobotState @@ -24,6 +25,16 @@ describe('moveLabware', () => { beforeEach(() => { invariantContext = makeContext() robotState = getInitialRobotStateStandard(invariantContext) + + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + mockGripperId: { + name: 'gripper', + id: mockGripperId, + }, + }, + } }) afterEach(() => { jest.resetAllMocks() @@ -129,7 +140,7 @@ describe('moveLabware', () => { const params = { commandCreatorFnName: 'moveLabware', labware: SOURCE_LABWARE, - useGripper: true, + useGripper: false, newLocation: { moduleId: thermocyclerId }, } as MoveLabwareArgs @@ -246,6 +257,7 @@ describe('moveLabware', () => { const wasteChuteInvariantContext = { ...invariantContext, additionalEquipmentEntities: { + ...invariantContext.additionalEquipmentEntities, mockWasteChuteId: { name: 'wasteChute', id: mockWasteChuteId, @@ -266,7 +278,7 @@ describe('moveLabware', () => { commandCreatorFnName: 'moveLabware', labware: TIPRACK_1, useGripper: true, - newLocation: { slotName: WASTE_CHUTE_CUTOUT }, + newLocation: { addressableAreaName: 'gripperWasteChute' }, } as MoveLabwareArgs const result = moveLabware( @@ -285,6 +297,7 @@ describe('moveLabware', () => { const wasteChuteInvariantContext = { ...invariantContext, additionalEquipmentEntities: { + ...invariantContext.additionalEquipmentEntities, mockWasteChuteId: { name: 'wasteChute', id: mockWasteChuteId, @@ -304,7 +317,7 @@ describe('moveLabware', () => { commandCreatorFnName: 'moveLabware', labware: SOURCE_LABWARE, useGripper: true, - newLocation: { slotName: WASTE_CHUTE_CUTOUT }, + newLocation: { addressableAreaName: 'gripperWasteChute' }, } as MoveLabwareArgs const result = moveLabware( @@ -319,4 +332,49 @@ describe('moveLabware', () => { }, ]) }) + it('should return an error when trying to move with gripper when there is no gripper', () => { + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: {}, + } as InvariantContext + + const params = { + commandCreatorFnName: 'moveLabware', + labware: SOURCE_LABWARE, + useGripper: true, + newLocation: { slotName: 'A1' }, + } as MoveLabwareArgs + + const result = moveLabware(params, invariantContext, robotState) + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ + type: 'GRIPPER_REQUIRED', + }) + }) + it('should return an error when trying to move into the waste chute when useGripper is not selected', () => { + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + ...invariantContext.additionalEquipmentEntities, + mockWasteChuteId: { + name: 'wasteChute', + id: mockWasteChuteId, + location: WASTE_CHUTE_CUTOUT, + }, + }, + } as InvariantContext + + const params = { + commandCreatorFnName: 'moveLabware', + labware: SOURCE_LABWARE, + useGripper: false, + newLocation: { addressableAreaName: 'gripperWasteChute' }, + } as MoveLabwareArgs + + const result = moveLabware(params, invariantContext, robotState) + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ + type: 'GRIPPER_REQUIRED', + }) + }) }) diff --git a/step-generation/src/commandCreators/atomic/moveLabware.ts b/step-generation/src/commandCreators/atomic/moveLabware.ts index 1d80433e375..46962e1bb76 100644 --- a/step-generation/src/commandCreators/atomic/moveLabware.ts +++ b/step-generation/src/commandCreators/atomic/moveLabware.ts @@ -3,7 +3,6 @@ import { HEATERSHAKER_MODULE_TYPE, LabwareMovementStrategy, THERMOCYCLER_MODULE_TYPE, - WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import * as errorCreators from '../../errorCreators' import * as warningCreators from '../../warningCreators' @@ -28,6 +27,7 @@ export const moveLabware: CommandCreator = ( ) => { const { labware, useGripper, newLocation } = args const { additionalEquipmentEntities } = invariantContext + const hasWasteChute = getHasWasteChute(additionalEquipmentEntities) const tiprackHasTip = prevRobotState.tipState != null ? getTiprackHasTips(prevRobotState.tipState, labware) @@ -43,8 +43,12 @@ export const moveLabware: CommandCreator = ( const newLocationInWasteChute = newLocation !== 'offDeck' && - 'slotName' in newLocation && - newLocation.slotName === WASTE_CHUTE_CUTOUT + 'addressableAreaName' in newLocation && + newLocation.addressableAreaName === 'gripperWasteChute' + + const hasGripper = Object.values(additionalEquipmentEntities).find( + aE => aE.name === 'gripper' + ) if (!labware || !prevRobotState.labware[labware]) { errors.push( @@ -57,6 +61,13 @@ export const moveLabware: CommandCreator = ( errors.push(errorCreators.labwareOffDeck()) } + if ( + (newLocationInWasteChute && hasGripper && !useGripper) || + (!hasGripper && useGripper) + ) { + errors.push(errorCreators.gripperRequired()) + } + const initialLabwareSlot = prevRobotState.labware[labware]?.slot const initialAdapterSlot = prevRobotState.labware[initialLabwareSlot]?.slot const initialSlot = @@ -99,17 +110,9 @@ export const moveLabware: CommandCreator = ( errors.push(errorCreators.labwareOffDeck()) } - if ( - tiprackHasTip && - newLocationInWasteChute && - getHasWasteChute(additionalEquipmentEntities) - ) { + if (tiprackHasTip && newLocationInWasteChute && hasWasteChute) { warnings.push(warningCreators.tiprackInWasteChuteHasTips()) - } else if ( - labwareHasLiquid && - newLocationInWasteChute && - getHasWasteChute(additionalEquipmentEntities) - ) { + } else if (labwareHasLiquid && newLocationInWasteChute && hasWasteChute) { warnings.push(warningCreators.labwareInWasteChuteHasLiquid()) } @@ -154,6 +157,7 @@ export const moveLabware: CommandCreator = ( params, }, ] + return { commands, warnings: warnings.length > 0 ? warnings : undefined, diff --git a/step-generation/src/errorCreators.ts b/step-generation/src/errorCreators.ts index be1407c2b97..f3364dc8b8e 100644 --- a/step-generation/src/errorCreators.ts +++ b/step-generation/src/errorCreators.ts @@ -211,3 +211,10 @@ export const additionalEquipmentDoesNotExist = (args: { message: `The ${args.additionalEquipment} does not exist`, } } + +export const gripperRequired = (): CommandCreatorError => { + return { + type: 'GRIPPER_REQUIRED', + message: 'The gripper is required to fulfill this action', + } +} diff --git a/step-generation/src/getNextRobotStateAndWarnings/forMoveLabware.ts b/step-generation/src/getNextRobotStateAndWarnings/forMoveLabware.ts index 56ddf85115e..389860d302b 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/forMoveLabware.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/forMoveLabware.ts @@ -18,6 +18,8 @@ export function forMoveLabware( newLocationString = newLocation.slotName } else if ('labwareId' in newLocation) { newLocationString = newLocation.labwareId + } else if ('addressableAreaName' in newLocation) { + newLocationString = newLocation.addressableAreaName } robotState.labware[labwareId].slot = newLocationString diff --git a/step-generation/src/types.ts b/step-generation/src/types.ts index 4d5fdfe5340..dcc18666ad3 100644 --- a/step-generation/src/types.ts +++ b/step-generation/src/types.ts @@ -492,6 +492,7 @@ export interface RobotState { export type ErrorType = | 'ADDITIONAL_EQUIPMENT_DOES_NOT_EXIST' | 'DROP_TIP_LOCATION_DOES_NOT_EXIST' + | 'GRIPPER_REQUIRED' | 'HEATER_SHAKER_EAST_WEST_LATCH_OPEN' | 'HEATER_SHAKER_EAST_WEST_MULTI_CHANNEL' | 'HEATER_SHAKER_IS_SHAKING' From c43d2d712067a6270676242e12d7eb2e5a4e58e1 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 14 Nov 2023 11:15:11 -0500 Subject: [PATCH 07/46] feat(protocol-designer): deck v4 touchups and staging area support (#13965) closes RAUT-787, RAUT-805, RAUT-806, RAUT-850 --- .../BaseDeck/StagingAreaFixture.tsx | 14 +- .../components/DeckSetup/FlexModuleTag.tsx | 2 +- .../LabwareOverlays/SlotControls.tsx | 4 +- .../__tests__/FlexModuleTag.test.tsx | 4 +- .../src/components/DeckSetup/index.tsx | 96 +++++++----- .../__tests__/FileSidebar.test.tsx | 6 +- .../__tests__/getUnusedStagingAreas.test.ts | 24 ++- .../utils/getUnusedStagingAreas.ts | 22 +-- .../CreateFileWizard/__tests__/utils.test.tsx | 10 +- .../modals/CreateFileWizard/types.ts | 8 +- .../modals/CreateFileWizard/utils.ts | 18 +-- .../src/components/modules/ModuleRow.tsx | 8 +- .../components/modules/StagingAreasModal.tsx | 9 +- .../src/components/modules/TrashModal.tsx | 2 +- .../modules/__tests__/ModuleRow.test.tsx | 2 +- protocol-designer/src/constants.ts | 2 + .../src/file-data/selectors/fileCreator.ts | 28 ---- .../src/step-forms/reducers/index.ts | 139 ++++++++++++------ .../src/step-forms/utils/index.ts | 17 ++- .../top-selectors/labware-locations/index.ts | 44 +++++- protocol-designer/src/ui/labware/selectors.ts | 22 +++ protocol-designer/src/utils/index.ts | 27 +++- shared-data/deck/types/schemaV4.ts | 10 +- shared-data/js/constants.ts | 5 +- shared-data/js/helpers/index.ts | 7 +- .../src/__tests__/aspirate.test.ts | 23 +++ .../src/__tests__/dispense.test.ts | 13 ++ .../src/__tests__/moveToWell.test.ts | 21 +++ .../src/commandCreators/atomic/aspirate.ts | 5 + .../src/commandCreators/atomic/blowout.ts | 15 +- .../src/commandCreators/atomic/dispense.ts | 5 + .../src/commandCreators/atomic/moveToWell.ts | 7 + .../src/commandCreators/atomic/replaceTip.ts | 6 + step-generation/src/constants.ts | 2 + step-generation/src/errorCreators.ts | 9 ++ step-generation/src/types.ts | 1 + 36 files changed, 451 insertions(+), 186 deletions(-) diff --git a/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx b/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx index 3e87517110a..107da94b8c2 100644 --- a/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx +++ b/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx @@ -5,7 +5,11 @@ import { SlotClip } from './SlotClip' import type { DeckDefinition, ModuleType } from '@opentrons/shared-data' -export type StagingAreaLocation = 'A3' | 'B3' | 'C3' | 'D3' +export type StagingAreaLocation = + | 'cutoutA3' + | 'cutoutB3' + | 'cutoutC3' + | 'cutoutD3' interface StagingAreaFixtureProps extends React.SVGProps { cutoutId: StagingAreaLocation @@ -40,7 +44,7 @@ export function StagingAreaFixture( const contentsByCutoutLocation: { [cutoutId in StagingAreaLocation]: JSX.Element } = { - A3: ( + cutoutA3: ( <> ), - B3: ( + cutoutB3: ( <> ), - C3: ( + cutoutC3: ( <> ), - D3: ( + cutoutD3: ( <> unknown diff --git a/protocol-designer/src/components/DeckSetup/__tests__/FlexModuleTag.test.tsx b/protocol-designer/src/components/DeckSetup/__tests__/FlexModuleTag.test.tsx index f1945aac1c9..6c8cb890003 100644 --- a/protocol-designer/src/components/DeckSetup/__tests__/FlexModuleTag.test.tsx +++ b/protocol-designer/src/components/DeckSetup/__tests__/FlexModuleTag.test.tsx @@ -28,7 +28,7 @@ describe('FlexModuleTag', () => { .calledWith( partialComponentPropsMatcher({ width: 5, - height: 16, + height: 20, }) ) .mockImplementation(({ children }) => ( @@ -48,7 +48,7 @@ describe('FlexModuleTag', () => { .calledWith( partialComponentPropsMatcher({ width: 5, - height: 16, + height: 20, }) ) .mockImplementation(({ children }) => ( diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx index 3b351618d50..ed443435e00 100644 --- a/protocol-designer/src/components/DeckSetup/index.tsx +++ b/protocol-designer/src/components/DeckSetup/index.tsx @@ -3,18 +3,18 @@ import { useDispatch, useSelector } from 'react-redux' import compact from 'lodash/compact' import values from 'lodash/values' import { - useOnClickOutside, - RobotWorkSpaceRenderProps, - Module, COLORS, - TrashLocation, + DeckFromLayers, FlexTrash, + Module, RobotCoordinateSpaceWithDOMCoords, - WasteChuteFixture, + RobotWorkSpaceRenderProps, + SingleSlotFixture, StagingAreaFixture, StagingAreaLocation, - SingleSlotFixture, - DeckFromLayers, + TrashLocation, + useOnClickOutside, + WasteChuteFixture, } from '@opentrons/components' import { AdditionalEquipmentEntity, @@ -22,29 +22,28 @@ import { ModuleTemporalProperties, } from '@opentrons/step-generation' import { - getLabwareHasQuirk, - inferModuleOrientationFromSlot, + FLEX_ROBOT_TYPE, + getAddressableAreaFromSlotId, getDeckDefFromRobotType, - OT2_ROBOT_TYPE, + getLabwareHasQuirk, getModuleDef2, + getModuleDisplayName, + getPositionFromSlotId, + inferModuleOrientationFromSlot, inferModuleOrientationFromXCoordinate, + isAddressableAreaStandardSlot, + OT2_ROBOT_TYPE, + STAGING_AREA_LOAD_NAME, THERMOCYCLER_MODULE_TYPE, - getModuleDisplayName, - DeckDefinition, - RobotType, - FLEX_ROBOT_TYPE, TRASH_BIN_LOAD_NAME, - STAGING_AREA_LOAD_NAME, WASTE_CHUTE_CUTOUT, WASTE_CHUTE_LOAD_NAME, - AddressableAreaName, - CutoutFixture, - CutoutId, } from '@opentrons/shared-data' import { FLEX_TRASH_DEF_URI, OT_2_TRASH_DEF_URI } from '../../constants' import { selectors as labwareDefSelectors } from '../../labware-defs' import { selectors as featureFlagSelectors } from '../../feature-flags' +import { getStagingAreaAddressableAreas } from '../../utils' import { getSlotIdsBlockedBySpanning, getSlotIsEmpty, @@ -72,12 +71,15 @@ import { Ot2ModuleTag } from './Ot2ModuleTag' import { SlotLabels } from './SlotLabels' import { getHasGen1MultiChannelPipette, getSwapBlocked } from './utils' +import type { + AddressableAreaName, + CutoutFixture, + CutoutId, + DeckDefinition, + RobotType, +} from '@opentrons/shared-data' + import styles from './DeckSetup.css' -import { - getAddressableAreaFromSlotId, - getPositionFromSlotId, - isAddressableAreaStandardSlot, -} from '@opentrons/shared-data/js' export const DECK_LAYER_BLOCKLIST = [ 'calibrationMarkings', @@ -89,6 +91,16 @@ export const DECK_LAYER_BLOCKLIST = [ 'screwHoles', ] +const OT2_STANDARD_DECK_VIEW_LAYER_BLOCK_LIST: string[] = [ + 'calibrationMarkings', + 'fixedBase', + 'doorStops', + 'metalFrame', + 'removalHandle', + 'removableDeckOutline', + 'screwHoles', +] + interface ContentsProps { getRobotCoordsFromDOMCoords: RobotWorkSpaceRenderProps['getRobotCoordsFromDOMCoords'] activeDeckSetup: InitialDeckSetup @@ -96,6 +108,7 @@ interface ContentsProps { showGen1MultichannelCollisionWarnings: boolean deckDef: DeckDefinition robotType: RobotType + stagingAreaCutoutIds: CutoutId[] trashSlot: string | null } @@ -110,6 +123,7 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { deckDef, robotType, trashSlot, + stagingAreaCutoutIds, } = props // NOTE: handling module<>labware compat when moving labware to empty module // is handled by SlotControls. @@ -173,12 +187,6 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { ]) : [] - console.log( - 'AA', - deckDef.locations.addressableAreas.filter(addressableArea => - isAddressableAreaStandardSlot(addressableArea.id, deckDef) - ) - ) return ( <> {/* all modules */} @@ -300,6 +308,7 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { selectedTerminalItemId={props.selectedTerminalItemId} moduleType={moduleOnDeck.type} handleDragHover={handleHoverEmptySlot} + slotId={moduleOnDeck.id} /> ) : null} {robotType === FLEX_ROBOT_TYPE ? ( @@ -336,16 +345,22 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { ) : null })} - {/* SlotControls for all empty deck + module slots */} + {/* SlotControls for all empty deck */} {deckDef.locations.addressableAreas - // only render standard slot fixture components - .filter( - addressableArea => - isAddressableAreaStandardSlot(addressableArea.id, deckDef) && + .filter(addressableArea => { + const stagingAreaAddressableAreas = getStagingAreaAddressableAreas( + stagingAreaCutoutIds + ) + const addressableAreas = + isAddressableAreaStandardSlot(addressableArea.id, deckDef) || + stagingAreaAddressableAreas.includes(addressableArea.id) + return ( + addressableAreas && !slotIdsBlockedBySpanning.includes(addressableArea.id) && getSlotIsEmpty(activeDeckSetup, addressableArea.id) && addressableArea.id !== trashSlot - ) + ) + }) .map(addressableArea => { return ( // @ts-expect-error @@ -534,7 +549,6 @@ export const DeckSetup = (): JSX.Element => { const filteredAddressableAreas = deckDef.locations.addressableAreas.filter( aa => isAddressableAreaStandardSlot(aa.id, deckDef) ) - return (
{drilledDown && } @@ -547,7 +561,10 @@ export const DeckSetup = (): JSX.Element => { {({ getRobotCoordsFromDOMCoords }) => ( <> {robotType === OT2_ROBOT_TYPE ? ( - + ) : ( <> {filteredAddressableAreas.map(addressableArea => { @@ -613,6 +630,11 @@ export const DeckSetup = (): JSX.Element => { robotType={robotType} activeDeckSetup={activeDeckSetup} selectedTerminalItemId={selectedTerminalItemId} + stagingAreaCutoutIds={stagingAreaFixtures.map( + // TODO(jr, 11/13/23): fix this type since AdditionalEquipment['location'] is type string + // instead of CutoutId + areas => areas.location as CutoutId + )} {...{ deckDef, getRobotCoordsFromDOMCoords, diff --git a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx index 88b9ae98d9a..2a19aab1d42 100644 --- a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx +++ b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx @@ -165,7 +165,11 @@ describe('FileSidebar', () => { // @ts-expect-error(sa, 2021-6-22): props.fileData might be null props.fileData.commands = commands props.additionalEquipment = { - [stagingArea]: { name: 'stagingArea', id: stagingArea, location: 'A3' }, + [stagingArea]: { + name: 'stagingArea', + id: stagingArea, + location: 'cutoutA3', + }, } const wrapper = shallow() diff --git a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.test.ts b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.test.ts index 333a6aee456..e95f2c06d3f 100644 --- a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.test.ts +++ b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.test.ts @@ -6,7 +6,11 @@ describe('getUnusedStagingAreas', () => { it('returns true for unused staging area', () => { const stagingArea = 'stagingAreaId' const mockAdditionalEquipment = { - [stagingArea]: { name: 'stagingArea', id: stagingArea, location: 'A3' }, + [stagingArea]: { + name: 'stagingArea', + id: stagingArea, + location: 'cutoutA3', + }, } as AdditionalEquipment expect(getUnusedStagingAreas(mockAdditionalEquipment, [])).toEqual(['A4']) @@ -15,8 +19,16 @@ describe('getUnusedStagingAreas', () => { const stagingArea = 'stagingAreaId' const stagingArea2 = 'stagingAreaId2' const mockAdditionalEquipment = { - [stagingArea]: { name: 'stagingArea', id: stagingArea, location: 'A3' }, - [stagingArea2]: { name: 'stagingArea', id: stagingArea2, location: 'B3' }, + [stagingArea]: { + name: 'stagingArea', + id: stagingArea, + location: 'cutoutA3', + }, + [stagingArea2]: { + name: 'stagingArea', + id: stagingArea2, + location: 'cutoutB3', + }, } as AdditionalEquipment expect(getUnusedStagingAreas(mockAdditionalEquipment, [])).toEqual([ @@ -27,7 +39,11 @@ describe('getUnusedStagingAreas', () => { it('returns false for unused staging area', () => { const stagingArea = 'stagingAreaId' const mockAdditionalEquipment = { - [stagingArea]: { name: 'stagingArea', id: stagingArea, location: 'A3' }, + [stagingArea]: { + name: 'stagingArea', + id: stagingArea, + location: 'cutoutA3', + }, } as AdditionalEquipment const mockCommand = ([ { diff --git a/protocol-designer/src/components/FileSidebar/utils/getUnusedStagingAreas.ts b/protocol-designer/src/components/FileSidebar/utils/getUnusedStagingAreas.ts index a701858a9eb..ea68068390c 100644 --- a/protocol-designer/src/components/FileSidebar/utils/getUnusedStagingAreas.ts +++ b/protocol-designer/src/components/FileSidebar/utils/getUnusedStagingAreas.ts @@ -1,11 +1,12 @@ -import type { CreateCommand } from '@opentrons/shared-data' +import { getStagingAreaAddressableAreas } from '../../../utils' +import type { CreateCommand, CutoutId } from '@opentrons/shared-data' import type { AdditionalEquipment } from '../FileSidebar' export const getUnusedStagingAreas = ( additionalEquipment: AdditionalEquipment, commands?: CreateCommand[] ): string[] => { - const stagingAreaSlots = Object.values(additionalEquipment) + const stagingAreaCutoutIds = Object.values(additionalEquipment) .filter(equipment => equipment?.name === 'stagingArea') .map(equipment => { if (equipment.location == null) { @@ -16,19 +17,12 @@ export const getUnusedStagingAreas = ( return equipment.location ?? '' }) - const corresponding4thColumnSlots = stagingAreaSlots.map(slot => { - const letter = slot.charAt(0) - const correspondingLocation = stagingAreaSlots.find(slot => - slot.startsWith(letter) - ) - if (correspondingLocation) { - return letter + '4' - } - - return slot - }) + const stagingAreaAddressableAreaNames = getStagingAreaAddressableAreas( + // TODO(jr, 11/13/23): fix AdditionalEquipment['location'] from type string to CutoutId + stagingAreaCutoutIds as CutoutId[] + ) - const stagingAreaCommandSlots: string[] = corresponding4thColumnSlots.filter( + const stagingAreaCommandSlots: string[] = stagingAreaAddressableAreaNames.filter( location => commands?.filter( command => diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx index fff9f9c446b..756142495e1 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx @@ -55,10 +55,10 @@ describe('getLastCheckedEquipment', () => { ...MOCK_FORM_STATE, additionalEquipment: [ 'trashBin', - 'stagingArea_A3', - 'stagingArea_B3', - 'stagingArea_C3', - 'stagingArea_D3', + 'stagingArea_cutoutA3', + 'stagingArea_cutoutB3', + 'stagingArea_cutoutC3', + 'stagingArea_cutoutD3', ], modulesByType: { ...MOCK_FORM_STATE.modulesByType, @@ -83,7 +83,7 @@ describe('getTrashSlot', () => { it('should return B3 when there is a staging area in slot A3', () => { MOCK_FORM_STATE = { ...MOCK_FORM_STATE, - additionalEquipment: ['stagingArea_A3'], + additionalEquipment: ['stagingArea_cutoutA3'], } const result = getTrashSlot(MOCK_FORM_STATE) expect(result).toBe('B3') diff --git a/protocol-designer/src/components/modals/CreateFileWizard/types.ts b/protocol-designer/src/components/modals/CreateFileWizard/types.ts index 8eb99fa5dba..1c15b081f9a 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/types.ts +++ b/protocol-designer/src/components/modals/CreateFileWizard/types.ts @@ -10,10 +10,10 @@ export type AdditionalEquipment = | 'gripper' | 'wasteChute' | 'trashBin' - | 'stagingArea_A3' - | 'stagingArea_B3' - | 'stagingArea_C3' - | 'stagingArea_D3' + | 'stagingArea_cutoutA3' + | 'stagingArea_cutoutB3' + | 'stagingArea_cutoutC3' + | 'stagingArea_cutoutD3' export interface FormState { fields: NewProtocolFields pipettesByMount: FormPipettesByMount diff --git a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts index 84e5b585c99..ef367349783 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts +++ b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts @@ -4,6 +4,7 @@ import { TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' +import { COLUMN_3_SLOTS } from '../../../constants' import { OUTER_SLOTS_FLEX } from '../../../modules' import { isModuleWithCollisionIssue } from '../../modules' import { @@ -81,12 +82,14 @@ export const getTrashBinOptionDisabled = (values: FormState): boolean => { } export const getTrashSlot = (values: FormState): string => { - const stagingAreaLocations = values.additionalEquipment - .filter(equipment => equipment.includes('stagingArea')) - .map(stagingArea => stagingArea.split('_')[1]) + const stagingAddressableAreas = values.additionalEquipment.filter(equipment => + equipment.includes('stagingArea') + ) + const cutouts = stagingAddressableAreas.flatMap(aa => + COLUMN_3_SLOTS.filter(cutout => aa.includes(cutout)) + ) - // return default trash slot A3 if staging area is not on slot - if (!stagingAreaLocations.includes(FLEX_TRASH_DEFAULT_SLOT)) { + if (!cutouts.includes(FLEX_TRASH_DEFAULT_SLOT)) { return FLEX_TRASH_DEFAULT_SLOT } @@ -103,11 +106,8 @@ export const getTrashSlot = (values: FormState): string => { }, [] ) - const unoccupiedSlot = OUTER_SLOTS_FLEX.find( - slot => - !stagingAreaLocations.includes(slot.value) && - !moduleSlots.includes(slot.value) + slot => !cutouts.includes(slot.value) && !moduleSlots.includes(slot.value) ) if (unoccupiedSlot == null) { console.error( diff --git a/protocol-designer/src/components/modules/ModuleRow.tsx b/protocol-designer/src/components/modules/ModuleRow.tsx index 08c1c2ca9f3..073082878b3 100644 --- a/protocol-designer/src/components/modules/ModuleRow.tsx +++ b/protocol-designer/src/components/modules/ModuleRow.tsx @@ -23,11 +23,11 @@ import { DEFAULT_MODEL_FOR_MODULE_TYPE, } from '../../constants' import { ModuleDiagram } from './ModuleDiagram' +import { FlexSlotMap } from './FlexSlotMap' import { isModuleWithCollisionIssue } from './utils' import styles from './styles.css' import type { ModuleType, RobotType } from '@opentrons/shared-data' -import { FlexSlotMap } from './FlexSlotMap' interface Props { robotType?: RobotType @@ -68,17 +68,17 @@ export function ModuleRow(props: Props): JSX.Element { // If this module is in a deck slot + is not TC spanning Slot // add to occupiedSlots if (slot && slot !== SPAN7_8_10_11_SLOT) { - slotDisplayName = `Slot ${slot}` + slotDisplayName = slot occupiedSlotsForMap = [slot] } // If this Module is a TC deck slot and spanning // populate all 4 slots individually if (slot === SPAN7_8_10_11_SLOT) { - slotDisplayName = 'Slot 7,8,10,11' + slotDisplayName = '7,8,10,11' occupiedSlotsForMap = ['7', '8', '10', '11'] // TC on Flex } else if (isFlex && type === THERMOCYCLER_MODULE_TYPE && slot === 'B1') { - slotDisplayName = 'Slot A1+B1' + slotDisplayName = 'A1+B1' occupiedSlotsForMap = ['A1', 'B1'] } // If collisionSlots are populated, check which slot is occupied diff --git a/protocol-designer/src/components/modules/StagingAreasModal.tsx b/protocol-designer/src/components/modules/StagingAreasModal.tsx index 7bfc762bb29..79725aa5147 100644 --- a/protocol-designer/src/components/modules/StagingAreasModal.tsx +++ b/protocol-designer/src/components/modules/StagingAreasModal.tsx @@ -47,7 +47,14 @@ const StagingAreasModalComponent = ( const areSlotsEmpty = values.selectedSlots.map(slot => getSlotIsEmpty(initialDeckSetup, slot) ) - const hasConflictedSlot = areSlotsEmpty.includes(false) + const hasWasteChute = + Object.values(initialDeckSetup.additionalEquipmentOnDeck).find( + aE => aE.name === 'wasteChute' + ) != null + const hasConflictedSlot = + hasWasteChute && values.selectedSlots.find(slot => slot === 'cutoutD3') + ? false + : areSlotsEmpty.includes(false) const mappedStagingAreas = stagingAreas.flatMap(area => { return [ diff --git a/protocol-designer/src/components/modules/TrashModal.tsx b/protocol-designer/src/components/modules/TrashModal.tsx index fb3a1a4762b..f9e33cd75be 100644 --- a/protocol-designer/src/components/modules/TrashModal.tsx +++ b/protocol-designer/src/components/modules/TrashModal.tsx @@ -154,7 +154,7 @@ export const TrashModal = (props: TrashModalProps): JSX.Element => { diff --git a/protocol-designer/src/components/modules/__tests__/ModuleRow.test.tsx b/protocol-designer/src/components/modules/__tests__/ModuleRow.test.tsx index ebb5ab60f17..2b240aae6ef 100644 --- a/protocol-designer/src/components/modules/__tests__/ModuleRow.test.tsx +++ b/protocol-designer/src/components/modules/__tests__/ModuleRow.test.tsx @@ -151,7 +151,7 @@ describe('ModuleRow', () => { ).toBe('GEN1') expect( wrapper.find(LabeledValue).filter({ label: 'Position' }).prop('value') - ).toBe('Slot 1') + ).toBe('1') }) it('does not display module model and slot when module has not been added to protocol', () => { diff --git a/protocol-designer/src/constants.ts b/protocol-designer/src/constants.ts index 92b2ba2d5df..e5b5bb6fe4e 100644 --- a/protocol-designer/src/constants.ts +++ b/protocol-designer/src/constants.ts @@ -159,3 +159,5 @@ export const THERMOCYCLER_PROFILE: 'thermocyclerProfile' = 'thermocyclerProfile' export const OT_2_TRASH_DEF_URI = 'opentrons/opentrons_1_trash_1100ml_fixed/1' export const FLEX_TRASH_DEF_URI = 'opentrons/opentrons_1_trash_3200ml_fixed/1' + +export const COLUMN_3_SLOTS = ['A3', 'B3', 'C3', 'D3'] diff --git a/protocol-designer/src/file-data/selectors/fileCreator.ts b/protocol-designer/src/file-data/selectors/fileCreator.ts index ed5aaa56638..89eba6b230e 100644 --- a/protocol-designer/src/file-data/selectors/fileCreator.ts +++ b/protocol-designer/src/file-data/selectors/fileCreator.ts @@ -41,16 +41,13 @@ import type { LabwareEntities, PipetteEntities, RobotState, - AdditionalEquipmentEntity, } from '@opentrons/step-generation' import type { CommandAnnotationV1Mixin, CommandV8Mixin, CreateCommand, - Cutout, LabwareV2Mixin, LiquidV1Mixin, - LoadFixtureCreateCommand, LoadLabwareCreateCommand, LoadModuleCreateCommand, LoadPipetteCreateCommand, @@ -300,30 +297,6 @@ export const createFile: Selector = createSelector( [] ) - // TODO(jr, 10/31/23): update to loadAddressableArea - const loadFixtureCommands = reduce< - AdditionalEquipmentEntity, - LoadFixtureCreateCommand[] - >( - Object.values(additionalEquipmentEntities), - (acc, additionalEquipment): LoadFixtureCreateCommand[] => { - if (additionalEquipment.name === 'gripper') return acc - - const loadFixtureCommands = { - key: uuid(), - commandType: 'loadFixture' as const, - params: { - fixtureId: additionalEquipment.id, - location: { cutout: additionalEquipment.location as Cutout }, - loadName: additionalEquipment.name, - }, - } - - return [...acc, loadFixtureCommands] - }, - [] - ) - const loadLiquidCommands = getLoadLiquidCommands( ingredients, ingredLocations @@ -356,7 +329,6 @@ export const createFile: Selector = createSelector( labwareDefsByURI ) const loadCommands: CreateCommand[] = [ - ...loadFixtureCommands, ...loadPipetteCommands, ...loadModuleCommands, ...loadAdapterCommands, diff --git a/protocol-designer/src/step-forms/reducers/index.ts b/protocol-designer/src/step-forms/reducers/index.ts index 440288c983a..11d1da71917 100644 --- a/protocol-designer/src/step-forms/reducers/index.ts +++ b/protocol-designer/src/step-forms/reducers/index.ts @@ -20,9 +20,10 @@ import { MAGNETIC_MODULE_V1, PipetteName, THERMOCYCLER_MODULE_TYPE, - LoadFixtureCreateCommand, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, + WASTE_CHUTE_ADDRESSABLE_AREAS, + getDeckDefFromRobotTypeV4, + AddressableAreaName, + CutoutId, } from '@opentrons/shared-data' import type { RootState as LabwareDefsRootState } from '../../labware-defs' import { rootReducer as labwareDefsRootReducer } from '../../labware-defs' @@ -43,6 +44,7 @@ import { getLabwareOnModule } from '../../ui/modules/utils' import { nestedCombineReducers } from './nestedCombineReducers' import { PROFILE_CYCLE, PROFILE_STEP } from '../../form-types' import { + COLUMN_4_SLOTS, NormalizedAdditionalEquipmentById, NormalizedPipetteById, } from '@opentrons/step-generation' @@ -1328,58 +1330,111 @@ export const additionalEquipmentInvariantProperties = handleActions { const { file } = action.payload - const gripperCommands = Object.values(file.commands).filter( + const isFlex = file.robot.model === FLEX_ROBOT_TYPE + const deckDef = getDeckDefFromRobotTypeV4(FLEX_ROBOT_TYPE) + const cutoutFixtures = deckDef.cutoutFixtures + const providesAddressableAreasForAddressableArea = cutoutFixtures.find( + cutoutFixture => cutoutFixture.id.includes('stagingAreaRightSlot') + )?.providesAddressableAreas + + const hasGripperCommands = Object.values(file.commands).some( (command): command is MoveLabwareCreateCommand => command.commandType === 'moveLabware' && command.params.strategy === 'usingGripper' ) - const fixtureCommands = Object.values(file.commands).filter( - (command): command is LoadFixtureCreateCommand => - command.commandType === 'loadFixture' + const hasWasteChuteCommands = Object.values(file.commands).some( + command => + (command.commandType === 'moveToAddressableArea' && + WASTE_CHUTE_ADDRESSABLE_AREAS.includes( + command.params.addressableAreaName + )) || + (command.commandType === 'moveLabware' && + command.params.newLocation !== 'offDeck' && + 'addressableAreaName' in command.params.newLocation && + WASTE_CHUTE_ADDRESSABLE_AREAS.includes( + command.params.addressableAreaName + )) ) - const fixtures = fixtureCommands.reduce( - ( - acc: NormalizedAdditionalEquipmentById, - command: LoadFixtureCreateCommand - ) => { - const { fixtureId, loadName, location } = command.params - const id = fixtureId ?? '' - if ( - loadName === STANDARD_SLOT_LOAD_NAME || - loadName === TRASH_BIN_LOAD_NAME - ) { - return acc - } - return { - ...acc, - [id]: { - id: id, - name: loadName, - location: location.cutout, + const wasteChuteId = `${uuid()}:wasteChute` + const wasteChute = hasWasteChuteCommands + ? { + [wasteChuteId]: { + name: 'wasteChute' as const, + id: wasteChuteId, + location: 'cutoutD3', }, } - }, - {} - ) - const hasGripper = gripperCommands.length > 0 - const isFlex = file.robot.model === FLEX_ROBOT_TYPE - const gripperId = `${uuid()}:gripper` - const gripper = { - [gripperId]: { - name: 'gripper' as const, - id: gripperId, - }, + : {} + + const getStagingAreaSlotNames = ( + commandType: 'moveLabware' | 'loadLabware', + locationKey: 'newLocation' | 'location' + ): AddressableAreaName[] => { + return Object.values(file.commands) + .filter( + command => + command.commandType === commandType && + command.params[locationKey] !== 'offDeck' && + 'slotName' in command.params[locationKey] && + COLUMN_4_SLOTS.includes(command.params[locationKey].slotName) + ) + .map(command => command.params[locationKey].slotName) } - if (isFlex) { - if (hasGripper) { - return { ...state, ...gripper, ...fixtures } - } else { - return { ...state, ...fixtures } + + const stagingAreaSlotNames = [ + ...new Set([ + ...getStagingAreaSlotNames('moveLabware', 'newLocation'), + ...getStagingAreaSlotNames('loadLabware', 'location'), + ]), + ] + + const findCutoutIdByAddressableArea = ( + addressableAreaName: AddressableAreaName + ): CutoutId | null => { + if (providesAddressableAreasForAddressableArea != null) { + for (const cutoutId in providesAddressableAreasForAddressableArea) { + if ( + providesAddressableAreasForAddressableArea[ + cutoutId as keyof typeof providesAddressableAreasForAddressableArea + ].includes(addressableAreaName) + ) { + return cutoutId as CutoutId + } + } + } + return null + } + + const stagingAreas = stagingAreaSlotNames.reduce((acc, slot) => { + const stagingAreaId = `${uuid()}:stagingArea` + const cutoutId = findCutoutIdByAddressableArea(slot) + return { + ...acc, + [stagingAreaId]: { + name: 'stagingArea' as const, + id: stagingAreaId, + location: cutoutId, + }, } + }, {}) + + const gripperId = `${uuid()}:gripper` + const gripper = hasGripperCommands + ? { + [gripperId]: { + name: 'gripper' as const, + id: gripperId, + }, + } + : {} + + if (isFlex) { + return { ...state, ...gripper, ...wasteChute, ...stagingAreas } } else { return { ...state } } }, + TOGGLE_IS_GRIPPER_REQUIRED: ( state: NormalizedAdditionalEquipmentById ): NormalizedAdditionalEquipmentById => { diff --git a/protocol-designer/src/step-forms/utils/index.ts b/protocol-designer/src/step-forms/utils/index.ts index b6b09bdeea7..139ff2f6e64 100644 --- a/protocol-designer/src/step-forms/utils/index.ts +++ b/protocol-designer/src/step-forms/utils/index.ts @@ -123,23 +123,24 @@ export const getSlotIsEmpty = ( } const filteredAdditionalEquipmentOnDeck = includeStagingAreas - ? values(initialDeckSetup.additionalEquipmentOnDeck).filter( - (additionalEquipment: AdditionalEquipmentOnDeck) => - additionalEquipment.location === slot + ? values( + initialDeckSetup.additionalEquipmentOnDeck + ).filter((additionalEquipment: AdditionalEquipmentOnDeck) => + additionalEquipment.location?.includes(slot) ) : values(initialDeckSetup.additionalEquipmentOnDeck).filter( (additionalEquipment: AdditionalEquipmentOnDeck) => - additionalEquipment.location === slot && + additionalEquipment.location?.includes(slot) && additionalEquipment.name !== 'stagingArea' ) return ( [ - ...values(initialDeckSetup.modules).filter( - (moduleOnDeck: ModuleOnDeck) => moduleOnDeck.slot === slot + ...values(initialDeckSetup.modules).filter((moduleOnDeck: ModuleOnDeck) => + slot.includes(moduleOnDeck.slot) ), - ...values(initialDeckSetup.labware).filter( - (labware: LabwareOnDeckType) => labware.slot === slot + ...values(initialDeckSetup.labware).filter((labware: LabwareOnDeckType) => + slot.includes(labware.slot) ), ...filteredAdditionalEquipmentOnDeck, ].length === 0 diff --git a/protocol-designer/src/top-selectors/labware-locations/index.ts b/protocol-designer/src/top-selectors/labware-locations/index.ts index 9fad438c2d4..94384f44c7f 100644 --- a/protocol-designer/src/top-selectors/labware-locations/index.ts +++ b/protocol-designer/src/top-selectors/labware-locations/index.ts @@ -7,7 +7,12 @@ import { FLEX_ROBOT_TYPE, WASTE_CHUTE_ADDRESSABLE_AREAS, WASTE_CHUTE_CUTOUT, + CutoutId, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + isAddressableAreaStandardSlot, + MOVABLE_TRASH_ADDRESSABLE_AREAS, } from '@opentrons/shared-data' +import { COLUMN_4_SLOTS } from '@opentrons/step-generation' import { START_TERMINAL_ITEM_ID, END_TERMINAL_ITEM_ID, @@ -108,9 +113,13 @@ export const getUnocuppiedLabwareLocationOptions: Selector< additionalEquipmentEntities ) => { const deckDef = getDeckDefFromRobotType(robotType) - const trashSlot = robotType === FLEX_ROBOT_TYPE ? 'A3' : '12' + const cutoutFixtures = deckDef.cutoutFixtures const allSlotIds = deckDef.locations.addressableAreas.map(slot => slot.id) const hasWasteChute = getHasWasteChute(additionalEquipmentEntities) + const stagingAreaCutoutIds = Object.values(additionalEquipmentEntities) + .filter(aE => aE.name === 'stagingArea') + // TODO(jr, 11/13/23): fix AdditionalEquipment['location'] from type string to CutoutId + .map(aE => aE.location as CutoutId) if (robotState == null) return null @@ -189,16 +198,39 @@ export const getUnocuppiedLabwareLocationOptions: Selector< [] ) + const stagingAreaAddressableAreaNames = stagingAreaCutoutIds + .flatMap(cutoutId => { + const addressableAreasOnCutout = cutoutFixtures.find( + cutoutFixture => cutoutFixture.id === STAGING_AREA_RIGHT_SLOT_FIXTURE + )?.providesAddressableAreas[cutoutId] + return addressableAreasOnCutout ?? [] + }) + .filter(aa => !isAddressableAreaStandardSlot(aa, deckDef)) + + // TODO(jr, 11/13/23): update COLUMN_4_SLOTS usage to FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS + const notSelectedStagingAreaAddressableAreas = COLUMN_4_SLOTS.filter(slot => + stagingAreaAddressableAreaNames.every( + addressableArea => addressableArea !== slot + ) + ) + const unoccupiedSlotOptions = allSlotIds - .filter( - slotId => + .filter(slotId => { + const isTrashSlot = + robotType === FLEX_ROBOT_TYPE + ? MOVABLE_TRASH_ADDRESSABLE_AREAS.includes(slotId) + : slotId === 'fixedTrash' + + return ( !slotIdsOccupiedByModules.includes(slotId) && !Object.values(labware) .map(lw => lw.slot) .includes(slotId) && - slotId !== trashSlot && - (hasWasteChute ? !(slotId in WASTE_CHUTE_ADDRESSABLE_AREAS) : true) - ) + !isTrashSlot && + !WASTE_CHUTE_ADDRESSABLE_AREAS.includes(slotId) && + !notSelectedStagingAreaAddressableAreas.includes(slotId) + ) + }) .map(slotId => ({ name: slotId, value: slotId })) const offDeck = { name: 'Off-deck', value: 'offDeck' } const wasteChuteSlot = { diff --git a/protocol-designer/src/ui/labware/selectors.ts b/protocol-designer/src/ui/labware/selectors.ts index f14923369a0..128d5831580 100644 --- a/protocol-designer/src/ui/labware/selectors.ts +++ b/protocol-designer/src/ui/labware/selectors.ts @@ -6,6 +6,7 @@ import { getLabwareDisplayName, getLabwareHasQuirk, } from '@opentrons/shared-data' +import { COLUMN_4_SLOTS } from '@opentrons/step-generation' import { i18n } from '../../localization' import * as stepFormSelectors from '../../step-forms/selectors' import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' @@ -82,9 +83,28 @@ export const getLabwareOptions: Selector = createSelector( savedStepForms ?? {}, labwareId ) + const isStartingInColumn4 = COLUMN_4_SLOTS.includes( + initialDeckSetup.labware[labwareId]?.slot + ) + + const isInColumn4 = + savedStepForms != null + ? Object.values(savedStepForms) + ?.reverse() + .some( + form => + form.stepType === 'moveLabware' && + form.labware === labwareId && + (COLUMN_4_SLOTS.includes(form.newLocation) || + (isStartingInColumn4 && + !COLUMN_4_SLOTS.includes(form.newLocation))) + ) + : false + const isAdapterOrAluminumBlock = isAdapter || labwareEntity.def.metadata.displayCategory === 'aluminumBlock' + const moduleOnDeck = getModuleUnderLabware( initialDeckSetup, savedStepForms ?? {}, @@ -104,6 +124,8 @@ export const getLabwareOptions: Selector = createSelector( nickName = `Off-deck - ${nicknamesById[labwareId]}` } else if (nickName === 'Opentrons Fixed Trash') { nickName = TRASH + } else if (isInColumn4) { + nickName = `${nicknamesById[labwareId]} in staging area slot` } if (!moveLabwarePresavedStep) { diff --git a/protocol-designer/src/utils/index.ts b/protocol-designer/src/utils/index.ts index 210016ffa45..eb6fd018728 100644 --- a/protocol-designer/src/utils/index.ts +++ b/protocol-designer/src/utils/index.ts @@ -1,5 +1,14 @@ import uuidv1 from 'uuid/v4' -import { WellSetHelpers, makeWellSetHelpers } from '@opentrons/shared-data' +import { + WellSetHelpers, + makeWellSetHelpers, + AddressableAreaName, + getDeckDefFromRobotTypeV4, + FLEX_ROBOT_TYPE, + CutoutId, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + isAddressableAreaStandardSlot, +} from '@opentrons/shared-data' import { i18n } from '../localization' import { WellGroup } from '@opentrons/components' import { BoundingRect, GenericRect } from '../collision-types' @@ -132,3 +141,19 @@ export const getStagingAreaSlots = ( export const getHas96Channel = (pipettes: PipetteEntities): boolean => { return Object.values(pipettes).some(pip => pip.spec.channels === 96) } + +export const getStagingAreaAddressableAreas = ( + cutoutIds: CutoutId[] +): AddressableAreaName[] => { + const deckDef = getDeckDefFromRobotTypeV4(FLEX_ROBOT_TYPE) + const cutoutFixtures = deckDef.cutoutFixtures + + return cutoutIds + .flatMap(cutoutId => { + const addressableAreasOnCutout = cutoutFixtures.find( + cutoutFixture => cutoutFixture.id === STAGING_AREA_RIGHT_SLOT_FIXTURE + )?.providesAddressableAreas[cutoutId] + return addressableAreasOnCutout ?? [] + }) + .filter(aa => !isAddressableAreaStandardSlot(aa, deckDef)) +} diff --git a/shared-data/deck/types/schemaV4.ts b/shared-data/deck/types/schemaV4.ts index 04aeb85cb59..ecf6bb51d26 100644 --- a/shared-data/deck/types/schemaV4.ts +++ b/shared-data/deck/types/schemaV4.ts @@ -15,7 +15,14 @@ export type FlexAddressableAreaName = | 'B4' | 'C4' | 'D4' - | 'movableTrash' + | 'movableTrashA1' + | 'movableTrashA3' + | 'movableTrashB1' + | 'movableTrashB3' + | 'movableTrashC1' + | 'movableTrashC3' + | 'movableTrashD1' + | 'movableTrashD3' | '1and8ChannelWasteChute' | '96ChannelWasteChute' | 'gripperWasteChute' @@ -69,3 +76,4 @@ export type CutoutFixtureId = | SingleSlotCutoutFixtureId | TrashBinAdapterCutoutFixtureId | WasteChuteCutoutFixtureId + | 'stagingAreaRightSlot' diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index 9ac6327bbcb..b31a67b958d 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -1,3 +1,4 @@ +import { AddressableAreaName } from '.' import type { Cutout, ModuleType } from './types' // constants for dealing with robot coordinate system (eg in labwareTools) @@ -259,7 +260,7 @@ export const FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS = [ D4_ADDRESSABLE_AREA, ] -export const MOVABLE_TRASH_ADDRESSABLE_AREAS = [ +export const MOVABLE_TRASH_ADDRESSABLE_AREAS: AddressableAreaName[] = [ MOVABLE_TRASH_A1_ADDRESSABLE_AREA, MOVABLE_TRASH_A3_ADDRESSABLE_AREA, MOVABLE_TRASH_B1_ADDRESSABLE_AREA, @@ -270,7 +271,7 @@ export const MOVABLE_TRASH_ADDRESSABLE_AREAS = [ MOVABLE_TRASH_D3_ADDRESSABLE_AREA, ] -export const WASTE_CHUTE_ADDRESSABLE_AREAS = [ +export const WASTE_CHUTE_ADDRESSABLE_AREAS: AddressableAreaName[] = [ ONE_AND_EIGHT_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA, NINETY_SIX_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA, GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA, diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index a0cb30f9eb9..cdc79967cf7 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -220,15 +220,12 @@ export const getAreSlotsHorizontallyAdjacent = ( } const slotANumber = parseInt(slotNameA) const slotBNumber = parseInt(slotNameB) - if (isNaN(slotBNumber) || isNaN(slotANumber)) { return false } - // TODO(bh, 2023-11-03): is this OT-2 only? const orderedSlots = standardOt2DeckDef.locations.cutouts // intentionally not substracting by 1 because trash (slot 12) should not count const numSlots = orderedSlots.length - if (slotBNumber > numSlots || slotANumber > numSlots) { return false } @@ -261,7 +258,6 @@ export const getAreSlotsVerticallyAdjacent = ( if (isNaN(slotBNumber) || isNaN(slotANumber)) { return false } - // TODO(bh, 2023-11-03): is this OT-2 only? const orderedSlots = standardOt2DeckDef.locations.cutouts // intentionally not substracting by 1 because trash (slot 12) should not count const numSlots = orderedSlots.length @@ -286,6 +282,9 @@ export const getAreSlotsVerticallyAdjacent = ( return areSlotsVerticallyAdjacent } +// TODO(jr, 11/12/23): rename this utility to mention that it +// is only used in the OT-2, same with getAreSlotsHorizontallyAdjacent +// and getAreSlotsVerticallyAdjacent export const getAreSlotsAdjacent = ( slotNameA?: string | null, slotNameB?: string | null diff --git a/step-generation/src/__tests__/aspirate.test.ts b/step-generation/src/__tests__/aspirate.test.ts index 4c6b3ab8911..ab9b7869327 100644 --- a/step-generation/src/__tests__/aspirate.test.ts +++ b/step-generation/src/__tests__/aspirate.test.ts @@ -198,6 +198,29 @@ describe('aspirate', () => { type: 'LABWARE_DOES_NOT_EXIST', }) }) + it('should return an error when aspirating from the 4th column', () => { + robotStateWithTip = { + ...robotStateWithTip, + labware: { + [SOURCE_LABWARE]: { slot: 'A4' }, + }, + } + const result = aspirate( + { + ...flowRateAndOffsets, + pipette: DEFAULT_PIPETTE, + volume: 50, + labware: SOURCE_LABWARE, + well: 'A1', + } as AspDispAirgapParams, + invariantContext, + robotStateWithTip + ) + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ + type: 'PIPETTING_INTO_COLUMN_4', + }) + }) it('should return an error when aspirating from labware off deck', () => { initialRobotState = getInitialRobotStateWithOffDeckLabwareStandard( invariantContext diff --git a/step-generation/src/__tests__/dispense.test.ts b/step-generation/src/__tests__/dispense.test.ts index b1399c54c4a..d66fae15b5e 100644 --- a/step-generation/src/__tests__/dispense.test.ts +++ b/step-generation/src/__tests__/dispense.test.ts @@ -141,6 +141,19 @@ describe('dispense', () => { type: 'LABWARE_DOES_NOT_EXIST', }) }) + it('should return an error when dispensing from the 4th column', () => { + robotStateWithTip = { + ...robotStateWithTip, + labware: { + [SOURCE_LABWARE]: { slot: 'A4' }, + }, + } + const result = dispense(params, invariantContext, robotStateWithTip) + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ + type: 'PIPETTING_INTO_COLUMN_4', + }) + }) it('should return an error when dispensing into thermocycler with pipette collision', () => { mockThermocyclerPipetteCollision.mockImplementationOnce( ( diff --git a/step-generation/src/__tests__/moveToWell.test.ts b/step-generation/src/__tests__/moveToWell.test.ts index 0906d8f8629..4020cc52e08 100644 --- a/step-generation/src/__tests__/moveToWell.test.ts +++ b/step-generation/src/__tests__/moveToWell.test.ts @@ -160,6 +160,27 @@ describe('moveToWell', () => { type: 'LABWARE_OFF_DECK', }) }) + it('should return an error when dispensing from the 4th column', () => { + robotStateWithTip = { + ...robotStateWithTip, + labware: { + [SOURCE_LABWARE]: { slot: 'A4' }, + }, + } + const result = moveToWell( + { + pipette: DEFAULT_PIPETTE, + labware: SOURCE_LABWARE, + well: 'A1', + }, + invariantContext, + robotStateWithTip + ) + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ + type: 'PIPETTING_INTO_COLUMN_4', + }) + }) it('should return an error when moving to well in a thermocycler with pipette collision', () => { mockThermocyclerPipetteCollision.mockImplementationOnce( ( diff --git a/step-generation/src/commandCreators/atomic/aspirate.ts b/step-generation/src/commandCreators/atomic/aspirate.ts index e32c017ef2a..efc734275f9 100644 --- a/step-generation/src/commandCreators/atomic/aspirate.ts +++ b/step-generation/src/commandCreators/atomic/aspirate.ts @@ -12,6 +12,7 @@ import { getIsHeaterShakerNorthSouthOfNonTiprackWithMultiChannelPipette, uuid, } from '../../utils' +import { COLUMN_4_SLOTS } from '../../constants' import type { CreateCommand } from '@opentrons/shared-data' import type { AspirateParams } from '@opentrons/shared-data/protocol/types/schemaV3' import type { CommandCreator, CommandCreatorError } from '../../types' @@ -64,6 +65,10 @@ export const aspirate: CommandCreator = ( errors.push(errorCreators.labwareOffDeck()) } + if (COLUMN_4_SLOTS.includes(slotName)) { + errors.push(errorCreators.pipettingIntoColumn4({ typeOfStep: actionName })) + } + if ( modulePipetteCollision({ pipette, diff --git a/step-generation/src/commandCreators/atomic/blowout.ts b/step-generation/src/commandCreators/atomic/blowout.ts index 7fbd13a56df..5c18b8d654c 100644 --- a/step-generation/src/commandCreators/atomic/blowout.ts +++ b/step-generation/src/commandCreators/atomic/blowout.ts @@ -1,8 +1,9 @@ -import { uuid } from '../../utils' +import { uuid, getLabwareSlot } from '../../utils' +import { COLUMN_4_SLOTS } from '../../constants' import * as errorCreators from '../../errorCreators' +import type { CreateCommand } from '@opentrons/shared-data' import type { BlowoutParams } from '@opentrons/shared-data/protocol/types/schemaV3' import type { CommandCreatorError, CommandCreator } from '../../types' -import { CreateCommand } from '@opentrons/shared-data' export const blowout: CommandCreator = ( args, @@ -14,7 +15,11 @@ export const blowout: CommandCreator = ( const actionName = 'blowout' const errors: CommandCreatorError[] = [] const pipetteData = prevRobotState.pipettes[pipette] - + const slotName = getLabwareSlot( + labware, + prevRobotState.labware, + prevRobotState.modules + ) // TODO Ian 2018-04-30 this logic using command creator args + robotstate to push errors // is duplicated across several command creators (eg aspirate & blowout overlap). // You can probably make higher-level error creator util fns to be more DRY @@ -49,6 +54,10 @@ export const blowout: CommandCreator = ( errors.push(errorCreators.labwareOffDeck()) } + if (COLUMN_4_SLOTS.includes(slotName)) { + errors.push(errorCreators.pipettingIntoColumn4({ typeOfStep: actionName })) + } + if (errors.length > 0) { return { errors, diff --git a/step-generation/src/commandCreators/atomic/dispense.ts b/step-generation/src/commandCreators/atomic/dispense.ts index c8ddfe1dc96..7d52d5c5eb3 100644 --- a/step-generation/src/commandCreators/atomic/dispense.ts +++ b/step-generation/src/commandCreators/atomic/dispense.ts @@ -11,6 +11,7 @@ import { getIsHeaterShakerNorthSouthOfNonTiprackWithMultiChannelPipette, uuid, } from '../../utils' +import { COLUMN_4_SLOTS } from '../../constants' import type { CreateCommand } from '@opentrons/shared-data' import type { DispenseParams } from '@opentrons/shared-data/protocol/types/schemaV3' import type { CommandCreator, CommandCreatorError } from '../../types' @@ -84,6 +85,10 @@ export const dispense: CommandCreator = ( errors.push(errorCreators.labwareOffDeck()) } + if (COLUMN_4_SLOTS.includes(slotName)) { + errors.push(errorCreators.pipettingIntoColumn4({ typeOfStep: actionName })) + } + if ( thermocyclerPipetteCollision( prevRobotState.modules, diff --git a/step-generation/src/commandCreators/atomic/moveToWell.ts b/step-generation/src/commandCreators/atomic/moveToWell.ts index bf2369509cc..e16f1cff417 100644 --- a/step-generation/src/commandCreators/atomic/moveToWell.ts +++ b/step-generation/src/commandCreators/atomic/moveToWell.ts @@ -11,6 +11,7 @@ import { getIsHeaterShakerNorthSouthOfNonTiprackWithMultiChannelPipette, uuid, } from '../../utils' +import { COLUMN_4_SLOTS } from '../../constants' import type { CreateCommand } from '@opentrons/shared-data' import type { MoveToWellParams as v5MoveToWellParams } from '@opentrons/shared-data/protocol/types/schemaV5' import type { MoveToWellParams as v6MoveToWellParams } from '@opentrons/shared-data/protocol/types/schemaV6/command/gantry' @@ -58,6 +59,12 @@ export const moveToWell: CommandCreator = ( errors.push(errorCreators.labwareOffDeck()) } + if (COLUMN_4_SLOTS.includes(slotName)) { + errors.push( + errorCreators.pipettingIntoColumn4({ typeOfStep: 'move to well' }) + ) + } + if ( modulePipetteCollision({ pipette, diff --git a/step-generation/src/commandCreators/atomic/replaceTip.ts b/step-generation/src/commandCreators/atomic/replaceTip.ts index cf59d1341d2..3485fe1d654 100644 --- a/step-generation/src/commandCreators/atomic/replaceTip.ts +++ b/step-generation/src/commandCreators/atomic/replaceTip.ts @@ -1,5 +1,6 @@ import { getNextTiprack } from '../../robotStateSelectors' import * as errorCreators from '../../errorCreators' +import { COLUMN_4_SLOTS } from '../../constants' import { dropTip } from './dropTip' import { curryCommandCreator, @@ -37,6 +38,11 @@ const _pickUpTip: CommandCreator = ( if (adapterId == null && pipetteName === 'p1000_96') { errors.push(errorCreators.missingAdapter()) } + if (COLUMN_4_SLOTS.includes(tiprackSlot)) { + errors.push( + errorCreators.pipettingIntoColumn4({ typeOfStep: 'pick up tip' }) + ) + } if (errors.length > 0) { return { errors } diff --git a/step-generation/src/constants.ts b/step-generation/src/constants.ts index d8dfa2142c4..63e0f0d4018 100644 --- a/step-generation/src/constants.ts +++ b/step-generation/src/constants.ts @@ -20,3 +20,5 @@ export const FIXED_TRASH_ID: 'fixedTrash' = 'fixedTrash' export const OT_2_TRASH_DEF_URI = 'opentrons/opentrons_1_trash_1100ml_fixed/1' export const FLEX_TRASH_DEF_URI = 'opentrons/opentrons_1_trash_3200ml_fixed/1' + +export const COLUMN_4_SLOTS = ['A4', 'B4', 'C4', 'D4'] diff --git a/step-generation/src/errorCreators.ts b/step-generation/src/errorCreators.ts index f3364dc8b8e..3d6cadc5c57 100644 --- a/step-generation/src/errorCreators.ts +++ b/step-generation/src/errorCreators.ts @@ -218,3 +218,12 @@ export const gripperRequired = (): CommandCreatorError => { message: 'The gripper is required to fulfill this action', } } + +export const pipettingIntoColumn4 = (args: { + typeOfStep: string +}): CommandCreatorError => { + return { + type: 'PIPETTING_INTO_COLUMN_4', + message: `Cannot ${args.typeOfStep} into a column 4 slot.`, + } +} diff --git a/step-generation/src/types.ts b/step-generation/src/types.ts index dcc18666ad3..26dbf13368a 100644 --- a/step-generation/src/types.ts +++ b/step-generation/src/types.ts @@ -512,6 +512,7 @@ export type ErrorType = | 'NO_TIP_ON_PIPETTE' | 'PIPETTE_DOES_NOT_EXIST' | 'PIPETTE_VOLUME_EXCEEDED' + | 'PIPETTING_INTO_COLUMN_4' | 'TALL_LABWARE_EAST_WEST_OF_HEATER_SHAKER' | 'THERMOCYCLER_LID_CLOSED' | 'TIP_VOLUME_EXCEEDED' From 8c899e4f044690e0ea1f87a30b52b4742c677d3c Mon Sep 17 00:00:00 2001 From: Jamey H Date: Tue, 14 Nov 2023 11:21:20 -0500 Subject: [PATCH 08/46] fix(app): fix protocol slideout whitescreen (#13977) Closes RQA-1887, RQA-1888 * fix(app): fix all ProtocolSlideout deck thumbnails displaying the same deckmap * fix(app): fix white-screen when selecting failed analysis protocol in protocol slideout --- .../__tests__/DeckThumbnail.test.tsx | 15 +++++++++++++++ app/src/molecules/DeckThumbnail/index.tsx | 2 +- .../organisms/ChooseProtocolSlideout/index.tsx | 10 ++++------ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx b/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx index d1d5fe35282..2c47a31532e 100644 --- a/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx +++ b/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx @@ -188,6 +188,21 @@ describe('DeckThumbnail', () => { getByText('mock BaseDeck') }) + it('returns null when there is no protocolAnalysis or the protocolAnalysis contains an error', () => { + const { queryByText } = render({ + protocolAnalysis: null, + }) + expect(queryByText('mock BaseDeck')).not.toBeInTheDocument() + + render({ + protocolAnalysis: { + ...protocolAnalysis, + errors: 'test error', + }, + }) + expect(queryByText('mock BaseDeck')).not.toBeInTheDocument() + }) + it('renders an OT-3 deck view when the protocol is an OT-3 protocol', () => { // ToDo (kk:11/06/2023) update this test later // const mockLabwareLocations = [ diff --git a/app/src/molecules/DeckThumbnail/index.tsx b/app/src/molecules/DeckThumbnail/index.tsx index 79df3017cad..f4295fa8dd9 100644 --- a/app/src/molecules/DeckThumbnail/index.tsx +++ b/app/src/molecules/DeckThumbnail/index.tsx @@ -35,7 +35,7 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element | null { const { protocolAnalysis, showSlotLabels = false, ...styleProps } = props const attachedModules = useAttachedModules() - if (protocolAnalysis == null) return null + if (protocolAnalysis == null || protocolAnalysis.errors.length) return null const robotType = getRobotTypeFromLoadedLabware(protocolAnalysis.labware) const deckDef = getDeckDefFromRobotType(robotType) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 67a4148d54e..ab3b201318b 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -36,7 +36,6 @@ import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/us import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' -import { ProtocolAnalysisOutput } from '@opentrons/shared-data' import type { Robot } from '../../redux/discovery/types' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State } from '../../redux/types' @@ -163,7 +162,6 @@ export function ChooseProtocolSlideoutComponent( }} robotName={robot.name} {...{ selectedProtocol, runCreationError, runCreationErrorCode }} - protocolAnalysis={selectedProtocol?.mostRecentAnalysis} /> ) : null} @@ -182,7 +180,6 @@ interface StoredProtocolListProps { runCreationError: string | null runCreationErrorCode: number | null robotName: string - protocolAnalysis?: ProtocolAnalysisOutput | null } function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { @@ -192,7 +189,6 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { runCreationError, runCreationErrorCode, robotName, - protocolAnalysis, } = props const { t } = useTranslation(['device_details', 'shared']) const storedProtocols = useSelector((state: State) => @@ -227,8 +223,10 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { height="4.25rem" width="4.75rem" > - {protocolAnalysis != null ? ( - + {storedProtocol.mostRecentAnalysis != null ? ( + ) : null} Date: Tue, 14 Nov 2023 11:32:58 -0500 Subject: [PATCH 09/46] ci(labware-library): add build timeout of 30 minutes (#13978) --- .github/workflows/ll-test-build-deploy.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ll-test-build-deploy.yaml b/.github/workflows/ll-test-build-deploy.yaml index 75e907af97f..e2e33d54146 100644 --- a/.github/workflows/ll-test-build-deploy.yaml +++ b/.github/workflows/ll-test-build-deploy.yaml @@ -116,6 +116,7 @@ jobs: build-ll: name: 'build labware library artifact' needs: ['js-unit-test'] + timeout-minutes: 30 runs-on: 'ubuntu-20.04' if: github.event_name != 'pull_request' steps: From a462d0464fe014c212dc43f6f94a8c10ee279df2 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 14 Nov 2023 17:44:25 -0500 Subject: [PATCH 10/46] feat(protocol-designer, shared-data, step-generation): wireup dropTip wasteChute commands (#13981) closes RAUT-767 Updates the pipetting compound commands to check the dropTipLocation. If the location is included in additionalEquipment, wasteChuteCommandsUtil is called with the correct arguments needed. This is plugged into returnTip and generateRobotStateTimeline. --- .../__snapshots__/LabwareList.test.tsx.snap | 1187 +++++ .../FilterCategory.test.tsx.snap | 11 + .../generateRobotStateTimeline.ts | 33 +- .../2/fixture_flex_96_tiprack_1000ul.json | 1026 ++++ .../2/fixture_flex_96_tiprack_adapter.json | 41 + shared-data/pipette/fixtures/name/index.ts | 1 + .../fixtureGeneration.test.ts.snap | 4449 ++++++++++++++++- .../src/__tests__/fixtureGeneration.test.ts | 12 + .../src/__tests__/replaceTip.test.ts | 89 + .../src/commandCreators/atomic/replaceTip.ts | 44 +- .../src/fixtures/commandFixtures.ts | 23 + .../src/fixtures/robotStateFixtures.ts | 51 + .../inPlaceCommandUpdates.ts | 4 +- .../src/utils/wasteChuteCommandsUtil.ts | 21 +- 14 files changed, 6804 insertions(+), 188 deletions(-) create mode 100644 shared-data/labware/fixtures/2/fixture_flex_96_tiprack_1000ul.json create mode 100644 shared-data/labware/fixtures/2/fixture_flex_96_tiprack_adapter.json diff --git a/labware-library/src/components/LabwareList/__tests__/__snapshots__/LabwareList.test.tsx.snap b/labware-library/src/components/LabwareList/__tests__/__snapshots__/LabwareList.test.tsx.snap index 4fc515c877c..aeb4b5c916d 100644 --- a/labware-library/src/components/LabwareList/__tests__/__snapshots__/LabwareList.test.tsx.snap +++ b/labware-library/src/components/LabwareList/__tests__/__snapshots__/LabwareList.test.tsx.snap @@ -6258,6 +6258,1193 @@ exports[`LabwareList component renders 1`] = ` } key="fixture/fixture_96_plate/1" /> + + +
  • + + Adapter + +
  • `; diff --git a/protocol-designer/src/timelineMiddleware/generateRobotStateTimeline.ts b/protocol-designer/src/timelineMiddleware/generateRobotStateTimeline.ts index 72ff5301b8e..544a14dca10 100644 --- a/protocol-designer/src/timelineMiddleware/generateRobotStateTimeline.ts +++ b/protocol-designer/src/timelineMiddleware/generateRobotStateTimeline.ts @@ -2,6 +2,7 @@ import takeWhile from 'lodash/takeWhile' import * as StepGeneration from '@opentrons/step-generation' import { commandCreatorFromStepArgs } from '../file-data/selectors/commands' import type { StepArgsAndErrorsById } from '../steplist/types' + export interface GenerateRobotStateTimelineArgs { allStepArgsAndErrors: StepArgsAndErrorsById orderedStepIds: string[] @@ -65,18 +66,36 @@ export const generateRobotStateTimeline = ( // @ts-expect-error(sa, 2021-6-20): not a valid type narrow, use in operator nextStepArgsForPipette.changeTip === 'never' + const isWasteChute = + invariantContext.additionalEquipmentEntities[dropTipLocation] != null + + const pipetteSpec = invariantContext.pipetteEntities[pipetteId]?.spec + + const addressableAreaName = + pipetteSpec.channels === 96 + ? '96ChannelWasteChute' + : '1and8ChannelWasteChute' + + const dropTipCommand = isWasteChute + ? StepGeneration.curryCommandCreator( + StepGeneration.wasteChuteCommandsUtil, + { + type: 'dropTip', + pipetteId: pipetteId, + addressableAreaName, + } + ) + : StepGeneration.curryCommandCreator(StepGeneration.dropTip, { + pipette: pipetteId, + dropTipLocation, + }) + if (!willReuseTip) { return [ ...acc, (_invariantContext, _prevRobotState) => StepGeneration.reduceCommandCreators( - [ - curriedCommandCreator, - StepGeneration.curryCommandCreator(StepGeneration.dropTip, { - pipette: pipetteId, - dropTipLocation, - }), - ], + [curriedCommandCreator, dropTipCommand], _invariantContext, _prevRobotState ), diff --git a/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_1000ul.json b/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_1000ul.json new file mode 100644 index 00000000000..5eaa01f9440 --- /dev/null +++ b/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_1000ul.json @@ -0,0 +1,1026 @@ +{ + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "brand": { + "brand": "Fixture", + "brandId": [] + }, + "metadata": { + "displayName": "Fixture Flex Tiprack 1000 uL", + "displayCategory": "tipRack", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99 + }, + "gripForce": 16, + "gripHeightFromLabwareBottom": 23.9, + "wells": { + "A1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 74.38, + "z": 1.5 + }, + "B1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 65.38, + "z": 1.5 + }, + "C1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 56.38, + "z": 1.5 + }, + "D1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 47.38, + "z": 1.5 + }, + "E1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 38.38, + "z": 1.5 + }, + "F1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 29.38, + "z": 1.5 + }, + "G1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 20.38, + "z": 1.5 + }, + "H1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 11.38, + "z": 1.5 + }, + "A2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 74.38, + "z": 1.5 + }, + "B2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 65.38, + "z": 1.5 + }, + "C2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 56.38, + "z": 1.5 + }, + "D2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 47.38, + "z": 1.5 + }, + "E2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 38.38, + "z": 1.5 + }, + "F2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 29.38, + "z": 1.5 + }, + "G2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 20.38, + "z": 1.5 + }, + "H2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 11.38, + "z": 1.5 + }, + "A3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 74.38, + "z": 1.5 + }, + "B3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 65.38, + "z": 1.5 + }, + "C3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 56.38, + "z": 1.5 + }, + "D3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 47.38, + "z": 1.5 + }, + "E3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 38.38, + "z": 1.5 + }, + "F3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 29.38, + "z": 1.5 + }, + "G3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 20.38, + "z": 1.5 + }, + "H3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 11.38, + "z": 1.5 + }, + "A4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 74.38, + "z": 1.5 + }, + "B4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 65.38, + "z": 1.5 + }, + "C4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 56.38, + "z": 1.5 + }, + "D4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 47.38, + "z": 1.5 + }, + "E4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 38.38, + "z": 1.5 + }, + "F4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 29.38, + "z": 1.5 + }, + "G4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 20.38, + "z": 1.5 + }, + "H4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 11.38, + "z": 1.5 + }, + "A5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 74.38, + "z": 1.5 + }, + "B5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 65.38, + "z": 1.5 + }, + "C5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 56.38, + "z": 1.5 + }, + "D5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 47.38, + "z": 1.5 + }, + "E5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 38.38, + "z": 1.5 + }, + "F5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 29.38, + "z": 1.5 + }, + "G5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 20.38, + "z": 1.5 + }, + "H5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 11.38, + "z": 1.5 + }, + "A6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 74.38, + "z": 1.5 + }, + "B6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 65.38, + "z": 1.5 + }, + "C6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 56.38, + "z": 1.5 + }, + "D6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 47.38, + "z": 1.5 + }, + "E6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 38.38, + "z": 1.5 + }, + "F6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 29.38, + "z": 1.5 + }, + "G6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 20.38, + "z": 1.5 + }, + "H6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 11.38, + "z": 1.5 + }, + "A7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 74.38, + "z": 1.5 + }, + "B7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 65.38, + "z": 1.5 + }, + "C7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 56.38, + "z": 1.5 + }, + "D7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 47.38, + "z": 1.5 + }, + "E7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 38.38, + "z": 1.5 + }, + "F7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 29.38, + "z": 1.5 + }, + "G7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 20.38, + "z": 1.5 + }, + "H7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 11.38, + "z": 1.5 + }, + "A8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 74.38, + "z": 1.5 + }, + "B8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 65.38, + "z": 1.5 + }, + "C8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 56.38, + "z": 1.5 + }, + "D8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 47.38, + "z": 1.5 + }, + "E8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 38.38, + "z": 1.5 + }, + "F8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 29.38, + "z": 1.5 + }, + "G8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 20.38, + "z": 1.5 + }, + "H8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 11.38, + "z": 1.5 + }, + "A9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 74.38, + "z": 1.5 + }, + "B9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 65.38, + "z": 1.5 + }, + "C9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 56.38, + "z": 1.5 + }, + "D9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 47.38, + "z": 1.5 + }, + "E9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 38.38, + "z": 1.5 + }, + "F9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 29.38, + "z": 1.5 + }, + "G9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 20.38, + "z": 1.5 + }, + "H9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 11.38, + "z": 1.5 + }, + "A10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 74.38, + "z": 1.5 + }, + "B10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 65.38, + "z": 1.5 + }, + "C10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 56.38, + "z": 1.5 + }, + "D10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 47.38, + "z": 1.5 + }, + "E10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 38.38, + "z": 1.5 + }, + "F10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 29.38, + "z": 1.5 + }, + "G10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 20.38, + "z": 1.5 + }, + "H10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 11.38, + "z": 1.5 + }, + "A11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 74.38, + "z": 1.5 + }, + "B11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 65.38, + "z": 1.5 + }, + "C11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 56.38, + "z": 1.5 + }, + "D11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 47.38, + "z": 1.5 + }, + "E11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 38.38, + "z": 1.5 + }, + "F11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 29.38, + "z": 1.5 + }, + "G11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 20.38, + "z": 1.5 + }, + "H11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 11.38, + "z": 1.5 + }, + "A12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 74.38, + "z": 1.5 + }, + "B12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 65.38, + "z": 1.5 + }, + "C12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 56.38, + "z": 1.5 + }, + "D12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 47.38, + "z": 1.5 + }, + "E12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 38.38, + "z": 1.5 + }, + "F12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 29.38, + "z": 1.5 + }, + "G12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 20.38, + "z": 1.5 + }, + "H12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 11.38, + "z": 1.5 + } + }, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ] + } + ], + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": true, + "tipLength": 95.6, + "tipOverlap": 10.5, + "isMagneticModuleCompatible": false, + "loadName": "fixture_flex_96_tiprack_1000ul" + }, + "namespace": "fixture", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "stackingOffsetWithLabware": { + "opentrons_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 121 + } + } +} diff --git a/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_adapter.json b/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_adapter.json new file mode 100644 index 00000000000..a97b50810f5 --- /dev/null +++ b/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_adapter.json @@ -0,0 +1,41 @@ +{ + "ordering": [], + "brand": { + "brand": "Fixture", + "brandId": [] + }, + "metadata": { + "displayName": "Fixture Flex 96 Tip Rack Adapter", + "displayCategory": "adapter", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 156.5, + "yDimension": 93, + "zDimension": 132 + }, + "wells": {}, + "groups": [ + { + "metadata": {}, + "wells": [] + } + ], + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "fixture_flex_96_tiprack_adapter" + }, + "namespace": "fixture", + "version": 1, + "schemaVersion": 2, + "allowedRoles": ["adapter"], + "cornerOffsetFromSlot": { + "x": -14.25, + "y": -3.5, + "z": 0 + } +} diff --git a/shared-data/pipette/fixtures/name/index.ts b/shared-data/pipette/fixtures/name/index.ts index 1625636981e..0831ffc7fdd 100644 --- a/shared-data/pipette/fixtures/name/index.ts +++ b/shared-data/pipette/fixtures/name/index.ts @@ -16,3 +16,4 @@ export const fixtureP300Multi: PipetteNameSpecs = pipetteNameSpecFixtures.p300_multi export const fixtureP1000Single: PipetteNameSpecs = pipetteNameSpecFixtures.p1000_single +export const fixtureP100096: PipetteNameSpecs = pipetteNameSpecFixtures.p1000_96 diff --git a/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap b/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap index 496ad3aed3c..e8e545df2b3 100644 --- a/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap +++ b/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap @@ -496,6 +496,204 @@ Object { "H8": Object {}, "H9": Object {}, }, + "tiprack4AdapterId": Object {}, + "tiprack4Id": Object { + "A1": Object {}, + "A10": Object {}, + "A11": Object {}, + "A12": Object {}, + "A2": Object {}, + "A3": Object {}, + "A4": Object {}, + "A5": Object {}, + "A6": Object {}, + "A7": Object {}, + "A8": Object {}, + "A9": Object {}, + "B1": Object {}, + "B10": Object {}, + "B11": Object {}, + "B12": Object {}, + "B2": Object {}, + "B3": Object {}, + "B4": Object {}, + "B5": Object {}, + "B6": Object {}, + "B7": Object {}, + "B8": Object {}, + "B9": Object {}, + "C1": Object {}, + "C10": Object {}, + "C11": Object {}, + "C12": Object {}, + "C2": Object {}, + "C3": Object {}, + "C4": Object {}, + "C5": Object {}, + "C6": Object {}, + "C7": Object {}, + "C8": Object {}, + "C9": Object {}, + "D1": Object {}, + "D10": Object {}, + "D11": Object {}, + "D12": Object {}, + "D2": Object {}, + "D3": Object {}, + "D4": Object {}, + "D5": Object {}, + "D6": Object {}, + "D7": Object {}, + "D8": Object {}, + "D9": Object {}, + "E1": Object {}, + "E10": Object {}, + "E11": Object {}, + "E12": Object {}, + "E2": Object {}, + "E3": Object {}, + "E4": Object {}, + "E5": Object {}, + "E6": Object {}, + "E7": Object {}, + "E8": Object {}, + "E9": Object {}, + "F1": Object {}, + "F10": Object {}, + "F11": Object {}, + "F12": Object {}, + "F2": Object {}, + "F3": Object {}, + "F4": Object {}, + "F5": Object {}, + "F6": Object {}, + "F7": Object {}, + "F8": Object {}, + "F9": Object {}, + "G1": Object {}, + "G10": Object {}, + "G11": Object {}, + "G12": Object {}, + "G2": Object {}, + "G3": Object {}, + "G4": Object {}, + "G5": Object {}, + "G6": Object {}, + "G7": Object {}, + "G8": Object {}, + "G9": Object {}, + "H1": Object {}, + "H10": Object {}, + "H11": Object {}, + "H12": Object {}, + "H2": Object {}, + "H3": Object {}, + "H4": Object {}, + "H5": Object {}, + "H6": Object {}, + "H7": Object {}, + "H8": Object {}, + "H9": Object {}, + }, + "tiprack5AdapterId": Object {}, + "tiprack5Id": Object { + "A1": Object {}, + "A10": Object {}, + "A11": Object {}, + "A12": Object {}, + "A2": Object {}, + "A3": Object {}, + "A4": Object {}, + "A5": Object {}, + "A6": Object {}, + "A7": Object {}, + "A8": Object {}, + "A9": Object {}, + "B1": Object {}, + "B10": Object {}, + "B11": Object {}, + "B12": Object {}, + "B2": Object {}, + "B3": Object {}, + "B4": Object {}, + "B5": Object {}, + "B6": Object {}, + "B7": Object {}, + "B8": Object {}, + "B9": Object {}, + "C1": Object {}, + "C10": Object {}, + "C11": Object {}, + "C12": Object {}, + "C2": Object {}, + "C3": Object {}, + "C4": Object {}, + "C5": Object {}, + "C6": Object {}, + "C7": Object {}, + "C8": Object {}, + "C9": Object {}, + "D1": Object {}, + "D10": Object {}, + "D11": Object {}, + "D12": Object {}, + "D2": Object {}, + "D3": Object {}, + "D4": Object {}, + "D5": Object {}, + "D6": Object {}, + "D7": Object {}, + "D8": Object {}, + "D9": Object {}, + "E1": Object {}, + "E10": Object {}, + "E11": Object {}, + "E12": Object {}, + "E2": Object {}, + "E3": Object {}, + "E4": Object {}, + "E5": Object {}, + "E6": Object {}, + "E7": Object {}, + "E8": Object {}, + "E9": Object {}, + "F1": Object {}, + "F10": Object {}, + "F11": Object {}, + "F12": Object {}, + "F2": Object {}, + "F3": Object {}, + "F4": Object {}, + "F5": Object {}, + "F6": Object {}, + "F7": Object {}, + "F8": Object {}, + "F9": Object {}, + "G1": Object {}, + "G10": Object {}, + "G11": Object {}, + "G12": Object {}, + "G2": Object {}, + "G3": Object {}, + "G4": Object {}, + "G5": Object {}, + "G6": Object {}, + "G7": Object {}, + "G8": Object {}, + "G9": Object {}, + "H1": Object {}, + "H10": Object {}, + "H11": Object {}, + "H12": Object {}, + "H2": Object {}, + "H3": Object {}, + "H4": Object {}, + "H5": Object {}, + "H6": Object {}, + "H7": Object {}, + "H8": Object {}, + "H9": Object {}, + }, "troughId": Object { "A1": Object {}, "A10": Object {}, @@ -512,6 +710,104 @@ Object { }, }, "pipettes": Object { + "p100096Id": Object { + "0": Object {}, + "1": Object {}, + "10": Object {}, + "11": Object {}, + "12": Object {}, + "13": Object {}, + "14": Object {}, + "15": Object {}, + "16": Object {}, + "17": Object {}, + "18": Object {}, + "19": Object {}, + "2": Object {}, + "20": Object {}, + "21": Object {}, + "22": Object {}, + "23": Object {}, + "24": Object {}, + "25": Object {}, + "26": Object {}, + "27": Object {}, + "28": Object {}, + "29": Object {}, + "3": Object {}, + "30": Object {}, + "31": Object {}, + "32": Object {}, + "33": Object {}, + "34": Object {}, + "35": Object {}, + "36": Object {}, + "37": Object {}, + "38": Object {}, + "39": Object {}, + "4": Object {}, + "40": Object {}, + "41": Object {}, + "42": Object {}, + "43": Object {}, + "44": Object {}, + "45": Object {}, + "46": Object {}, + "47": Object {}, + "48": Object {}, + "49": Object {}, + "5": Object {}, + "50": Object {}, + "51": Object {}, + "52": Object {}, + "53": Object {}, + "54": Object {}, + "55": Object {}, + "56": Object {}, + "57": Object {}, + "58": Object {}, + "59": Object {}, + "6": Object {}, + "60": Object {}, + "61": Object {}, + "62": Object {}, + "63": Object {}, + "64": Object {}, + "65": Object {}, + "66": Object {}, + "67": Object {}, + "68": Object {}, + "69": Object {}, + "7": Object {}, + "70": Object {}, + "71": Object {}, + "72": Object {}, + "73": Object {}, + "74": Object {}, + "75": Object {}, + "76": Object {}, + "77": Object {}, + "78": Object {}, + "79": Object {}, + "8": Object {}, + "80": Object {}, + "81": Object {}, + "82": Object {}, + "83": Object {}, + "84": Object {}, + "85": Object {}, + "86": Object {}, + "87": Object {}, + "88": Object {}, + "89": Object {}, + "9": Object {}, + "90": Object {}, + "91": Object {}, + "92": Object {}, + "93": Object {}, + "94": Object {}, + "95": Object {}, + }, "p10MultiId": Object { "0": Object {}, "1": Object {}, @@ -6261,230 +6557,3755 @@ Object { "id": "tiprack3Id", "labwareDefURI": "fixture/fixture_tiprack_300_ul/1", }, - "troughId": Object { + "tiprack4AdapterId": Object { "def": Object { + "allowedRoles": Array [ + "adapter", + ], "brand": Object { - "brand": "USA Scientific", - "brandId": Array [ - "1061-8150", - ], + "brand": "Fixture", + "brandId": Array [], }, "cornerOffsetFromSlot": Object { - "x": 0, - "y": 0, + "x": -14.25, + "y": -3.5, "z": 0, }, "dimensions": Object { - "xDimension": 127.76, - "yDimension": 85.8, - "zDimension": 44.45, + "xDimension": 156.5, + "yDimension": 93, + "zDimension": 132, }, "groups": Array [ Object { - "metadata": Object { - "wellBottomShape": "v", - }, - "wells": Array [ - "A1", - "A2", - "A3", - "A4", - "A5", - "A6", - "A7", - "A8", - "A9", - "A10", - "A11", - "A12", - ], + "metadata": Object {}, + "wells": Array [], }, ], "metadata": Object { - "displayCategory": "reservoir", - "displayName": "12 Channel Trough", - "displayVolumeUnits": "mL", + "displayCategory": "adapter", + "displayName": "Fixture Flex 96 Tip Rack Adapter", + "displayVolumeUnits": "µL", + "tags": Array [], }, "namespace": "fixture", - "ordering": Array [ - Array [ - "A1", - ], - Array [ - "A2", - ], - Array [ - "A3", - ], - Array [ - "A4", - ], - Array [ - "A5", - ], - Array [ - "A6", - ], + "ordering": Array [], + "parameters": Object { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "fixture_flex_96_tiprack_adapter", + "quirks": Array [], + }, + "schemaVersion": 2, + "version": 1, + "wells": Object {}, + }, + "id": "tiprack4AdapterId", + "labwareDefURI": "fixture/fixture_flex_96_tiprack_adapter/1", + }, + "tiprack4Id": Object { + "def": Object { + "brand": Object { + "brand": "Fixture", + "brandId": Array [], + }, + "cornerOffsetFromSlot": Object { + "x": 0, + "y": 0, + "z": 0, + }, + "dimensions": Object { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99, + }, + "gripForce": 16, + "gripHeightFromLabwareBottom": 23.9, + "groups": Array [ + Object { + "metadata": Object {}, + "wells": Array [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12", + ], + }, + ], + "metadata": Object { + "displayCategory": "tipRack", + "displayName": "Fixture Flex Tiprack 1000 uL", + "displayVolumeUnits": "µL", + "tags": Array [], + }, + "namespace": "fixture", + "ordering": Array [ + Array [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + ], + Array [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + ], + Array [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + ], + Array [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + ], + Array [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + ], + Array [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + ], Array [ "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", ], Array [ "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", ], Array [ "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", ], Array [ "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", ], Array [ "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", ], Array [ "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12", ], ], "parameters": Object { - "format": "trough", + "format": "96Standard", "isMagneticModuleCompatible": false, - "isTiprack": false, - "loadName": "fixture_12_trough", - "quirks": Array [ - "centerMultichannelOnWells", - "touchTipDisabled", - ], + "isTiprack": true, + "loadName": "fixture_flex_96_tiprack_1000ul", + "quirks": Array [], + "tipLength": 95.6, + "tipOverlap": 10.5, }, "schemaVersion": 2, + "stackingOffsetWithLabware": Object { + "opentrons_flex_96_tiprack_adapter": Object { + "x": 0, + "y": 0, + "z": 121, + }, + }, "version": 1, "wells": Object { "A1": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 13.94, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 74.38, + "z": 1.5, }, "A10": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 95.75, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 74.38, + "z": 1.5, }, "A11": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 104.84, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 74.38, + "z": 1.5, }, "A12": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 113.93, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 74.38, + "z": 1.5, }, "A2": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 23.03, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 74.38, + "z": 1.5, }, "A3": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 32.12, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 74.38, + "z": 1.5, }, "A4": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 41.21, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 74.38, + "z": 1.5, }, "A5": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 50.3, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 74.38, + "z": 1.5, }, "A6": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 59.39, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 74.38, + "z": 1.5, + }, + "A7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 74.38, + "z": 1.5, + }, + "A8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 74.38, + "z": 1.5, + }, + "A9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 74.38, + "z": 1.5, + }, + "B1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 65.38, + "z": 1.5, + }, + "B10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 65.38, + "z": 1.5, + }, + "B11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 65.38, + "z": 1.5, + }, + "B12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 65.38, + "z": 1.5, + }, + "B2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 65.38, + "z": 1.5, + }, + "B3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 65.38, + "z": 1.5, + }, + "B4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 65.38, + "z": 1.5, + }, + "B5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 65.38, + "z": 1.5, + }, + "B6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 65.38, + "z": 1.5, + }, + "B7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 65.38, + "z": 1.5, + }, + "B8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 65.38, + "z": 1.5, + }, + "B9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 65.38, + "z": 1.5, + }, + "C1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 56.38, + "z": 1.5, + }, + "C10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 56.38, + "z": 1.5, + }, + "C11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 56.38, + "z": 1.5, + }, + "C12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 56.38, + "z": 1.5, + }, + "C2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 56.38, + "z": 1.5, + }, + "C3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 56.38, + "z": 1.5, + }, + "C4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 56.38, + "z": 1.5, + }, + "C5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 56.38, + "z": 1.5, + }, + "C6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 56.38, + "z": 1.5, + }, + "C7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 56.38, + "z": 1.5, + }, + "C8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 56.38, + "z": 1.5, + }, + "C9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 56.38, + "z": 1.5, + }, + "D1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 47.38, + "z": 1.5, + }, + "D10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 47.38, + "z": 1.5, + }, + "D11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 47.38, + "z": 1.5, + }, + "D12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 47.38, + "z": 1.5, + }, + "D2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 47.38, + "z": 1.5, + }, + "D3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 47.38, + "z": 1.5, + }, + "D4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 47.38, + "z": 1.5, + }, + "D5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 47.38, + "z": 1.5, + }, + "D6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 47.38, + "z": 1.5, + }, + "D7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 47.38, + "z": 1.5, + }, + "D8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 47.38, + "z": 1.5, + }, + "D9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 47.38, + "z": 1.5, + }, + "E1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 38.38, + "z": 1.5, + }, + "E10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 38.38, + "z": 1.5, + }, + "E11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 38.38, + "z": 1.5, + }, + "E12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 38.38, + "z": 1.5, + }, + "E2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 38.38, + "z": 1.5, + }, + "E3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 38.38, + "z": 1.5, + }, + "E4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 38.38, + "z": 1.5, + }, + "E5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 38.38, + "z": 1.5, + }, + "E6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 38.38, + "z": 1.5, + }, + "E7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 38.38, + "z": 1.5, + }, + "E8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 38.38, + "z": 1.5, + }, + "E9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 38.38, + "z": 1.5, + }, + "F1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 29.38, + "z": 1.5, + }, + "F10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 29.38, + "z": 1.5, + }, + "F11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 29.38, + "z": 1.5, + }, + "F12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 29.38, + "z": 1.5, + }, + "F2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 29.38, + "z": 1.5, + }, + "F3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 29.38, + "z": 1.5, + }, + "F4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 29.38, + "z": 1.5, + }, + "F5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 29.38, + "z": 1.5, + }, + "F6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 29.38, + "z": 1.5, + }, + "F7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 29.38, + "z": 1.5, + }, + "F8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 29.38, + "z": 1.5, + }, + "F9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 29.38, + "z": 1.5, + }, + "G1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 20.38, + "z": 1.5, + }, + "G10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 20.38, + "z": 1.5, + }, + "G11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 20.38, + "z": 1.5, + }, + "G12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 20.38, + "z": 1.5, + }, + "G2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 20.38, + "z": 1.5, + }, + "G3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 20.38, + "z": 1.5, + }, + "G4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 20.38, + "z": 1.5, + }, + "G5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 20.38, + "z": 1.5, + }, + "G6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 20.38, + "z": 1.5, + }, + "G7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 20.38, + "z": 1.5, + }, + "G8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 20.38, + "z": 1.5, + }, + "G9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 20.38, + "z": 1.5, + }, + "H1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 11.38, + "z": 1.5, + }, + "H10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 11.38, + "z": 1.5, + }, + "H11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 11.38, + "z": 1.5, + }, + "H12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 11.38, + "z": 1.5, + }, + "H2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 11.38, + "z": 1.5, + }, + "H3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 11.38, + "z": 1.5, + }, + "H4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 11.38, + "z": 1.5, + }, + "H5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 11.38, + "z": 1.5, + }, + "H6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 11.38, + "z": 1.5, + }, + "H7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 11.38, + "z": 1.5, + }, + "H8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 11.38, + "z": 1.5, + }, + "H9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 11.38, + "z": 1.5, + }, + }, + }, + "id": "tiprack4Id", + "labwareDefURI": "fixture/fixture_flex_96_tiprack_1000ul/1", + }, + "tiprack5AdapterId": Object { + "def": Object { + "allowedRoles": Array [ + "adapter", + ], + "brand": Object { + "brand": "Fixture", + "brandId": Array [], + }, + "cornerOffsetFromSlot": Object { + "x": -14.25, + "y": -3.5, + "z": 0, + }, + "dimensions": Object { + "xDimension": 156.5, + "yDimension": 93, + "zDimension": 132, + }, + "groups": Array [ + Object { + "metadata": Object {}, + "wells": Array [], + }, + ], + "metadata": Object { + "displayCategory": "adapter", + "displayName": "Fixture Flex 96 Tip Rack Adapter", + "displayVolumeUnits": "µL", + "tags": Array [], + }, + "namespace": "fixture", + "ordering": Array [], + "parameters": Object { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "fixture_flex_96_tiprack_adapter", + "quirks": Array [], + }, + "schemaVersion": 2, + "version": 1, + "wells": Object {}, + }, + "id": "tiprack5AdapterId", + "labwareDefURI": "fixture/fixture_flex_96_tiprack_adapter/1", + }, + "tiprack5Id": Object { + "def": Object { + "brand": Object { + "brand": "Fixture", + "brandId": Array [], + }, + "cornerOffsetFromSlot": Object { + "x": 0, + "y": 0, + "z": 0, + }, + "dimensions": Object { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99, + }, + "gripForce": 16, + "gripHeightFromLabwareBottom": 23.9, + "groups": Array [ + Object { + "metadata": Object {}, + "wells": Array [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12", + ], + }, + ], + "metadata": Object { + "displayCategory": "tipRack", + "displayName": "Fixture Flex Tiprack 1000 uL", + "displayVolumeUnits": "µL", + "tags": Array [], + }, + "namespace": "fixture", + "ordering": Array [ + Array [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + ], + Array [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + ], + Array [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + ], + Array [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + ], + Array [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + ], + Array [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + ], + Array [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + ], + Array [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + ], + Array [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + ], + Array [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + ], + Array [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + ], + Array [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12", + ], + ], + "parameters": Object { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "fixture_flex_96_tiprack_1000ul", + "quirks": Array [], + "tipLength": 95.6, + "tipOverlap": 10.5, + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": Object { + "opentrons_flex_96_tiprack_adapter": Object { + "x": 0, + "y": 0, + "z": 121, + }, + }, + "version": 1, + "wells": Object { + "A1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 74.38, + "z": 1.5, + }, + "A10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 74.38, + "z": 1.5, + }, + "A11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 74.38, + "z": 1.5, + }, + "A12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 74.38, + "z": 1.5, + }, + "A2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 74.38, + "z": 1.5, + }, + "A3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 74.38, + "z": 1.5, + }, + "A4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 74.38, + "z": 1.5, + }, + "A5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 74.38, + "z": 1.5, + }, + "A6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 74.38, + "z": 1.5, + }, + "A7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 74.38, + "z": 1.5, + }, + "A8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 74.38, + "z": 1.5, + }, + "A9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 74.38, + "z": 1.5, + }, + "B1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 65.38, + "z": 1.5, + }, + "B10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 65.38, + "z": 1.5, + }, + "B11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 65.38, + "z": 1.5, + }, + "B12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 65.38, + "z": 1.5, + }, + "B2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 65.38, + "z": 1.5, + }, + "B3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 65.38, + "z": 1.5, + }, + "B4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 65.38, + "z": 1.5, + }, + "B5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 65.38, + "z": 1.5, + }, + "B6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 65.38, + "z": 1.5, + }, + "B7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 65.38, + "z": 1.5, + }, + "B8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 65.38, + "z": 1.5, + }, + "B9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 65.38, + "z": 1.5, + }, + "C1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 56.38, + "z": 1.5, + }, + "C10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 56.38, + "z": 1.5, + }, + "C11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 56.38, + "z": 1.5, + }, + "C12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 56.38, + "z": 1.5, + }, + "C2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 56.38, + "z": 1.5, + }, + "C3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 56.38, + "z": 1.5, + }, + "C4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 56.38, + "z": 1.5, + }, + "C5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 56.38, + "z": 1.5, + }, + "C6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 56.38, + "z": 1.5, + }, + "C7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 56.38, + "z": 1.5, + }, + "C8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 56.38, + "z": 1.5, + }, + "C9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 56.38, + "z": 1.5, + }, + "D1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 47.38, + "z": 1.5, + }, + "D10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 47.38, + "z": 1.5, + }, + "D11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 47.38, + "z": 1.5, + }, + "D12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 47.38, + "z": 1.5, + }, + "D2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 47.38, + "z": 1.5, + }, + "D3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 47.38, + "z": 1.5, + }, + "D4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 47.38, + "z": 1.5, + }, + "D5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 47.38, + "z": 1.5, + }, + "D6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 47.38, + "z": 1.5, + }, + "D7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 47.38, + "z": 1.5, + }, + "D8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 47.38, + "z": 1.5, + }, + "D9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 47.38, + "z": 1.5, + }, + "E1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 38.38, + "z": 1.5, + }, + "E10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 38.38, + "z": 1.5, + }, + "E11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 38.38, + "z": 1.5, + }, + "E12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 38.38, + "z": 1.5, + }, + "E2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 38.38, + "z": 1.5, + }, + "E3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 38.38, + "z": 1.5, + }, + "E4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 38.38, + "z": 1.5, + }, + "E5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 38.38, + "z": 1.5, + }, + "E6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 38.38, + "z": 1.5, + }, + "E7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 38.38, + "z": 1.5, + }, + "E8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 38.38, + "z": 1.5, + }, + "E9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 38.38, + "z": 1.5, + }, + "F1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 29.38, + "z": 1.5, + }, + "F10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 29.38, + "z": 1.5, + }, + "F11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 29.38, + "z": 1.5, + }, + "F12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 29.38, + "z": 1.5, + }, + "F2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 29.38, + "z": 1.5, + }, + "F3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 29.38, + "z": 1.5, + }, + "F4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 29.38, + "z": 1.5, + }, + "F5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 29.38, + "z": 1.5, + }, + "F6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 29.38, + "z": 1.5, + }, + "F7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 29.38, + "z": 1.5, + }, + "F8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 29.38, + "z": 1.5, + }, + "F9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 29.38, + "z": 1.5, + }, + "G1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 20.38, + "z": 1.5, + }, + "G10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 20.38, + "z": 1.5, + }, + "G11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 20.38, + "z": 1.5, + }, + "G12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 20.38, + "z": 1.5, + }, + "G2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 20.38, + "z": 1.5, + }, + "G3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 20.38, + "z": 1.5, + }, + "G4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 20.38, + "z": 1.5, + }, + "G5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 20.38, + "z": 1.5, + }, + "G6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 20.38, + "z": 1.5, + }, + "G7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 20.38, + "z": 1.5, + }, + "G8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 20.38, + "z": 1.5, + }, + "G9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 20.38, + "z": 1.5, + }, + "H1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 11.38, + "z": 1.5, + }, + "H10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 11.38, + "z": 1.5, + }, + "H11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 11.38, + "z": 1.5, + }, + "H12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 11.38, + "z": 1.5, + }, + "H2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 11.38, + "z": 1.5, + }, + "H3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 11.38, + "z": 1.5, + }, + "H4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 11.38, + "z": 1.5, + }, + "H5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 11.38, + "z": 1.5, + }, + "H6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 11.38, + "z": 1.5, + }, + "H7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 11.38, + "z": 1.5, + }, + "H8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 11.38, + "z": 1.5, + }, + "H9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 11.38, + "z": 1.5, + }, + }, + }, + "id": "tiprack5Id", + "labwareDefURI": "fixture/fixture_flex_96_tiprack_1000ul/1", + }, + "troughId": Object { + "def": Object { + "brand": Object { + "brand": "USA Scientific", + "brandId": Array [ + "1061-8150", + ], + }, + "cornerOffsetFromSlot": Object { + "x": 0, + "y": 0, + "z": 0, + }, + "dimensions": Object { + "xDimension": 127.76, + "yDimension": 85.8, + "zDimension": 44.45, + }, + "groups": Array [ + Object { + "metadata": Object { + "wellBottomShape": "v", + }, + "wells": Array [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12", + ], + }, + ], + "metadata": Object { + "displayCategory": "reservoir", + "displayName": "12 Channel Trough", + "displayVolumeUnits": "mL", + }, + "namespace": "fixture", + "ordering": Array [ + Array [ + "A1", + ], + Array [ + "A2", + ], + Array [ + "A3", + ], + Array [ + "A4", + ], + Array [ + "A5", + ], + Array [ + "A6", + ], + Array [ + "A7", + ], + Array [ + "A8", + ], + Array [ + "A9", + ], + Array [ + "A10", + ], + Array [ + "A11", + ], + Array [ + "A12", + ], + ], + "parameters": Object { + "format": "trough", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "fixture_12_trough", + "quirks": Array [ + "centerMultichannelOnWells", + "touchTipDisabled", + ], + }, + "schemaVersion": 2, + "version": 1, + "wells": Object { + "A1": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 13.94, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A10": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 95.75, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A11": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 104.84, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A12": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 113.93, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A2": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 23.03, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A3": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 32.12, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A4": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 41.21, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A5": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 50.3, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A6": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 59.39, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A7": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 68.48, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A8": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 77.57, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A9": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 86.66, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + }, + }, + "id": "troughId", + "labwareDefURI": "fixture/fixture_12_trough/1", + }, + }, + "moduleEntities": Object {}, + "pipetteEntities": Object { + "p100096Id": Object { + "id": "p100096Id", + "name": "p1000_96", + "spec": Object { + "channels": 96, + "defaultAspirateFlowRate": Object { + "max": 812, + "min": 3, + "value": 7.85, + }, + "defaultDispenseFlowRate": Object { + "max": 812, + "min": 3, + "value": 7.85, + }, + "displayName": "Flex 96-Channel 1000 μL", + "maxVolume": 1000, + "minVolume": 5, + }, + "tiprackDefURI": "fixture/fixture_flex_96_tiprack_1000ul/1", + "tiprackLabwareDef": Object { + "brand": Object { + "brand": "Fixture", + "brandId": Array [], + }, + "cornerOffsetFromSlot": Object { + "x": 0, + "y": 0, + "z": 0, + }, + "dimensions": Object { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99, + }, + "gripForce": 16, + "gripHeightFromLabwareBottom": 23.9, + "groups": Array [ + Object { + "metadata": Object {}, + "wells": Array [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12", + ], + }, + ], + "metadata": Object { + "displayCategory": "tipRack", + "displayName": "Fixture Flex Tiprack 1000 uL", + "displayVolumeUnits": "µL", + "tags": Array [], + }, + "namespace": "fixture", + "ordering": Array [ + Array [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + ], + Array [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + ], + Array [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + ], + Array [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + ], + Array [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + ], + Array [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + ], + Array [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + ], + Array [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + ], + Array [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + ], + Array [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + ], + Array [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + ], + Array [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12", + ], + ], + "parameters": Object { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "fixture_flex_96_tiprack_1000ul", + "quirks": Array [], + "tipLength": 95.6, + "tipOverlap": 10.5, + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": Object { + "opentrons_flex_96_tiprack_adapter": Object { + "x": 0, + "y": 0, + "z": 121, + }, + }, + "version": 1, + "wells": Object { + "A1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 74.38, + "z": 1.5, + }, + "A10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 74.38, + "z": 1.5, + }, + "A11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 74.38, + "z": 1.5, + }, + "A12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 74.38, + "z": 1.5, + }, + "A2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 74.38, + "z": 1.5, + }, + "A3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 74.38, + "z": 1.5, + }, + "A4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 74.38, + "z": 1.5, + }, + "A5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 74.38, + "z": 1.5, + }, + "A6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 74.38, + "z": 1.5, + }, + "A7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 74.38, + "z": 1.5, + }, + "A8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 74.38, + "z": 1.5, + }, + "A9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 74.38, + "z": 1.5, + }, + "B1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 65.38, + "z": 1.5, + }, + "B10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 65.38, + "z": 1.5, + }, + "B11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 65.38, + "z": 1.5, + }, + "B12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 65.38, + "z": 1.5, + }, + "B2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 65.38, + "z": 1.5, + }, + "B3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 65.38, + "z": 1.5, + }, + "B4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 65.38, + "z": 1.5, + }, + "B5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 65.38, + "z": 1.5, + }, + "B6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 65.38, + "z": 1.5, + }, + "B7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 65.38, + "z": 1.5, + }, + "B8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 65.38, + "z": 1.5, + }, + "B9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 65.38, + "z": 1.5, + }, + "C1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 56.38, + "z": 1.5, + }, + "C10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 56.38, + "z": 1.5, + }, + "C11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 56.38, + "z": 1.5, + }, + "C12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 56.38, + "z": 1.5, + }, + "C2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 56.38, + "z": 1.5, + }, + "C3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 56.38, + "z": 1.5, + }, + "C4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 56.38, + "z": 1.5, + }, + "C5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 56.38, + "z": 1.5, + }, + "C6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 56.38, + "z": 1.5, + }, + "C7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 56.38, + "z": 1.5, + }, + "C8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 56.38, + "z": 1.5, + }, + "C9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 56.38, + "z": 1.5, + }, + "D1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 47.38, + "z": 1.5, + }, + "D10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 47.38, + "z": 1.5, + }, + "D11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 47.38, + "z": 1.5, + }, + "D12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 47.38, + "z": 1.5, + }, + "D2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 47.38, + "z": 1.5, + }, + "D3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 47.38, + "z": 1.5, + }, + "D4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 47.38, + "z": 1.5, + }, + "D5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 47.38, + "z": 1.5, + }, + "D6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 47.38, + "z": 1.5, + }, + "D7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 47.38, + "z": 1.5, + }, + "D8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 47.38, + "z": 1.5, + }, + "D9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 47.38, + "z": 1.5, + }, + "E1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 38.38, + "z": 1.5, + }, + "E10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 38.38, + "z": 1.5, + }, + "E11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 38.38, + "z": 1.5, + }, + "E12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 38.38, + "z": 1.5, + }, + "E2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 38.38, + "z": 1.5, + }, + "E3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 38.38, + "z": 1.5, + }, + "E4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 38.38, + "z": 1.5, + }, + "E5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 38.38, + "z": 1.5, + }, + "E6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 38.38, + "z": 1.5, + }, + "E7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 38.38, + "z": 1.5, + }, + "E8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 38.38, + "z": 1.5, + }, + "E9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 38.38, + "z": 1.5, + }, + "F1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 29.38, + "z": 1.5, + }, + "F10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 29.38, + "z": 1.5, + }, + "F11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 29.38, + "z": 1.5, + }, + "F12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 29.38, + "z": 1.5, + }, + "F2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 29.38, + "z": 1.5, + }, + "F3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 29.38, + "z": 1.5, + }, + "F4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 29.38, + "z": 1.5, + }, + "F5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 29.38, + "z": 1.5, + }, + "F6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 29.38, + "z": 1.5, + }, + "F7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 29.38, + "z": 1.5, + }, + "F8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 29.38, + "z": 1.5, + }, + "F9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 29.38, + "z": 1.5, + }, + "G1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 20.38, + "z": 1.5, + }, + "G10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 20.38, + "z": 1.5, + }, + "G11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 20.38, + "z": 1.5, + }, + "G12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 20.38, + "z": 1.5, + }, + "G2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 20.38, + "z": 1.5, + }, + "G3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 20.38, + "z": 1.5, + }, + "G4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 20.38, + "z": 1.5, + }, + "G5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 20.38, + "z": 1.5, + }, + "G6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 20.38, + "z": 1.5, + }, + "G7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 20.38, + "z": 1.5, + }, + "G8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 20.38, + "z": 1.5, + }, + "G9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 20.38, + "z": 1.5, + }, + "H1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 11.38, + "z": 1.5, + }, + "H10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 11.38, + "z": 1.5, + }, + "H11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 11.38, + "z": 1.5, + }, + "H12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 11.38, + "z": 1.5, + }, + "H2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 11.38, + "z": 1.5, + }, + "H3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 11.38, + "z": 1.5, + }, + "H4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 11.38, + "z": 1.5, }, - "A7": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 68.48, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "H5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 11.38, + "z": 1.5, }, - "A8": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 77.57, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "H6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 11.38, + "z": 1.5, }, - "A9": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 86.66, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "H7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 11.38, + "z": 1.5, + }, + "H8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 11.38, + "z": 1.5, + }, + "H9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 11.38, + "z": 1.5, }, }, }, - "id": "troughId", - "labwareDefURI": "fixture/fixture_12_trough/1", }, - }, - "moduleEntities": Object {}, - "pipetteEntities": Object { "p10MultiId": Object { "id": "p10MultiId", "name": "p10_multi", @@ -11088,6 +14909,18 @@ Object { "tiprack2Id": Object { "slot": "2", }, + "tiprack4AdapterId": Object { + "slot": "7", + }, + "tiprack4Id": Object { + "slot": "tiprack4AdapterId", + }, + "tiprack5AdapterId": Object { + "slot": "8", + }, + "tiprack5Id": Object { + "slot": "tiprack5AdapterId", + }, }, "liquidState": Object { "labware": Object { @@ -11584,6 +15417,204 @@ Object { "H8": Object {}, "H9": Object {}, }, + "tiprack4AdapterId": Object {}, + "tiprack4Id": Object { + "A1": Object {}, + "A10": Object {}, + "A11": Object {}, + "A12": Object {}, + "A2": Object {}, + "A3": Object {}, + "A4": Object {}, + "A5": Object {}, + "A6": Object {}, + "A7": Object {}, + "A8": Object {}, + "A9": Object {}, + "B1": Object {}, + "B10": Object {}, + "B11": Object {}, + "B12": Object {}, + "B2": Object {}, + "B3": Object {}, + "B4": Object {}, + "B5": Object {}, + "B6": Object {}, + "B7": Object {}, + "B8": Object {}, + "B9": Object {}, + "C1": Object {}, + "C10": Object {}, + "C11": Object {}, + "C12": Object {}, + "C2": Object {}, + "C3": Object {}, + "C4": Object {}, + "C5": Object {}, + "C6": Object {}, + "C7": Object {}, + "C8": Object {}, + "C9": Object {}, + "D1": Object {}, + "D10": Object {}, + "D11": Object {}, + "D12": Object {}, + "D2": Object {}, + "D3": Object {}, + "D4": Object {}, + "D5": Object {}, + "D6": Object {}, + "D7": Object {}, + "D8": Object {}, + "D9": Object {}, + "E1": Object {}, + "E10": Object {}, + "E11": Object {}, + "E12": Object {}, + "E2": Object {}, + "E3": Object {}, + "E4": Object {}, + "E5": Object {}, + "E6": Object {}, + "E7": Object {}, + "E8": Object {}, + "E9": Object {}, + "F1": Object {}, + "F10": Object {}, + "F11": Object {}, + "F12": Object {}, + "F2": Object {}, + "F3": Object {}, + "F4": Object {}, + "F5": Object {}, + "F6": Object {}, + "F7": Object {}, + "F8": Object {}, + "F9": Object {}, + "G1": Object {}, + "G10": Object {}, + "G11": Object {}, + "G12": Object {}, + "G2": Object {}, + "G3": Object {}, + "G4": Object {}, + "G5": Object {}, + "G6": Object {}, + "G7": Object {}, + "G8": Object {}, + "G9": Object {}, + "H1": Object {}, + "H10": Object {}, + "H11": Object {}, + "H12": Object {}, + "H2": Object {}, + "H3": Object {}, + "H4": Object {}, + "H5": Object {}, + "H6": Object {}, + "H7": Object {}, + "H8": Object {}, + "H9": Object {}, + }, + "tiprack5AdapterId": Object {}, + "tiprack5Id": Object { + "A1": Object {}, + "A10": Object {}, + "A11": Object {}, + "A12": Object {}, + "A2": Object {}, + "A3": Object {}, + "A4": Object {}, + "A5": Object {}, + "A6": Object {}, + "A7": Object {}, + "A8": Object {}, + "A9": Object {}, + "B1": Object {}, + "B10": Object {}, + "B11": Object {}, + "B12": Object {}, + "B2": Object {}, + "B3": Object {}, + "B4": Object {}, + "B5": Object {}, + "B6": Object {}, + "B7": Object {}, + "B8": Object {}, + "B9": Object {}, + "C1": Object {}, + "C10": Object {}, + "C11": Object {}, + "C12": Object {}, + "C2": Object {}, + "C3": Object {}, + "C4": Object {}, + "C5": Object {}, + "C6": Object {}, + "C7": Object {}, + "C8": Object {}, + "C9": Object {}, + "D1": Object {}, + "D10": Object {}, + "D11": Object {}, + "D12": Object {}, + "D2": Object {}, + "D3": Object {}, + "D4": Object {}, + "D5": Object {}, + "D6": Object {}, + "D7": Object {}, + "D8": Object {}, + "D9": Object {}, + "E1": Object {}, + "E10": Object {}, + "E11": Object {}, + "E12": Object {}, + "E2": Object {}, + "E3": Object {}, + "E4": Object {}, + "E5": Object {}, + "E6": Object {}, + "E7": Object {}, + "E8": Object {}, + "E9": Object {}, + "F1": Object {}, + "F10": Object {}, + "F11": Object {}, + "F12": Object {}, + "F2": Object {}, + "F3": Object {}, + "F4": Object {}, + "F5": Object {}, + "F6": Object {}, + "F7": Object {}, + "F8": Object {}, + "F9": Object {}, + "G1": Object {}, + "G10": Object {}, + "G11": Object {}, + "G12": Object {}, + "G2": Object {}, + "G3": Object {}, + "G4": Object {}, + "G5": Object {}, + "G6": Object {}, + "G7": Object {}, + "G8": Object {}, + "G9": Object {}, + "H1": Object {}, + "H10": Object {}, + "H11": Object {}, + "H12": Object {}, + "H2": Object {}, + "H3": Object {}, + "H4": Object {}, + "H5": Object {}, + "H6": Object {}, + "H7": Object {}, + "H8": Object {}, + "H9": Object {}, + }, "troughId": Object { "A1": Object {}, "A10": Object {}, @@ -11600,6 +15631,104 @@ Object { }, }, "pipettes": Object { + "p100096Id": Object { + "0": Object {}, + "1": Object {}, + "10": Object {}, + "11": Object {}, + "12": Object {}, + "13": Object {}, + "14": Object {}, + "15": Object {}, + "16": Object {}, + "17": Object {}, + "18": Object {}, + "19": Object {}, + "2": Object {}, + "20": Object {}, + "21": Object {}, + "22": Object {}, + "23": Object {}, + "24": Object {}, + "25": Object {}, + "26": Object {}, + "27": Object {}, + "28": Object {}, + "29": Object {}, + "3": Object {}, + "30": Object {}, + "31": Object {}, + "32": Object {}, + "33": Object {}, + "34": Object {}, + "35": Object {}, + "36": Object {}, + "37": Object {}, + "38": Object {}, + "39": Object {}, + "4": Object {}, + "40": Object {}, + "41": Object {}, + "42": Object {}, + "43": Object {}, + "44": Object {}, + "45": Object {}, + "46": Object {}, + "47": Object {}, + "48": Object {}, + "49": Object {}, + "5": Object {}, + "50": Object {}, + "51": Object {}, + "52": Object {}, + "53": Object {}, + "54": Object {}, + "55": Object {}, + "56": Object {}, + "57": Object {}, + "58": Object {}, + "59": Object {}, + "6": Object {}, + "60": Object {}, + "61": Object {}, + "62": Object {}, + "63": Object {}, + "64": Object {}, + "65": Object {}, + "66": Object {}, + "67": Object {}, + "68": Object {}, + "69": Object {}, + "7": Object {}, + "70": Object {}, + "71": Object {}, + "72": Object {}, + "73": Object {}, + "74": Object {}, + "75": Object {}, + "76": Object {}, + "77": Object {}, + "78": Object {}, + "79": Object {}, + "8": Object {}, + "80": Object {}, + "81": Object {}, + "82": Object {}, + "83": Object {}, + "84": Object {}, + "85": Object {}, + "86": Object {}, + "87": Object {}, + "88": Object {}, + "89": Object {}, + "9": Object {}, + "90": Object {}, + "91": Object {}, + "92": Object {}, + "93": Object {}, + "94": Object {}, + "95": Object {}, + }, "p10MultiId": Object { "0": Object {}, "1": Object {}, diff --git a/step-generation/src/__tests__/fixtureGeneration.test.ts b/step-generation/src/__tests__/fixtureGeneration.test.ts index 5f14449b473..158f0cfb6ed 100644 --- a/step-generation/src/__tests__/fixtureGeneration.test.ts +++ b/step-generation/src/__tests__/fixtureGeneration.test.ts @@ -18,6 +18,18 @@ describe('snapshot tests', () => { sourcePlateId: { slot: '4', }, + tiprack4AdapterId: { + slot: '7', + }, + tiprack5AdapterId: { + slot: '8', + }, + tiprack4Id: { + slot: 'tiprack4AdapterId', + }, + tiprack5Id: { + slot: 'tiprack5AdapterId', + }, fixedTrash: { slot: '12', }, diff --git a/step-generation/src/__tests__/replaceTip.test.ts b/step-generation/src/__tests__/replaceTip.test.ts index c97bc008eb8..c1044d38432 100644 --- a/step-generation/src/__tests__/replaceTip.test.ts +++ b/step-generation/src/__tests__/replaceTip.test.ts @@ -7,6 +7,8 @@ import { getSuccessResult, pickUpTipHelper, dropTipHelper, + dropTipInPlaceHelper, + moveToAddressableAreaHelper, DEFAULT_PIPETTE, } from '../fixtures' import { FIXED_TRASH_ID } from '..' @@ -15,8 +17,12 @@ import type { InvariantContext, RobotState } from '../types' const tiprack1Id = 'tiprack1Id' const tiprack2Id = 'tiprack2Id' +const tiprack4Id = 'tiprack4Id' +const tiprack5Id = 'tiprack5Id' const p300SingleId = DEFAULT_PIPETTE const p300MultiId = 'p300MultiId' +const p100096Id = 'p100096Id' +const wasteChuteId = 'wasteChuteId' describe('replaceTip', () => { let invariantContext: InvariantContext let initialRobotState: RobotState @@ -132,6 +138,44 @@ describe('replaceTip', () => { }), ]) }) + it('Single-channel: dropping tips in waste chute', () => { + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + wasteChuteId: { + name: 'wasteChute', + id: wasteChuteId, + location: 'cutoutD3', + }, + }, + } + const initialTestRobotState = merge({}, initialRobotState, { + tipState: { + tipracks: { + [tiprack1Id]: { + A1: false, + }, + }, + pipettes: { + p300SingleId: true, + }, + }, + }) + const result = replaceTip( + { + pipette: p300SingleId, + dropTipLocation: 'wasteChuteId', + }, + invariantContext, + initialTestRobotState + ) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + moveToAddressableAreaHelper(), + dropTipInPlaceHelper(), + pickUpTipHelper('B1'), + ]) + }) }) describe('replaceTip: multi-channel', () => { it('multi-channel, all tipracks have tips', () => { @@ -205,4 +249,49 @@ describe('replaceTip', () => { ]) }) }) + describe('replaceTip: 96-channel', () => { + it('96-channel, dropping tips in waste chute', () => { + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + wasteChuteId: { + name: 'wasteChute', + id: wasteChuteId, + location: 'cutoutD3', + }, + }, + } + const initialTestRobotState = merge({}, initialRobotState, { + tipState: { + tipracks: { + [tiprack4Id]: getTiprackTipstate(false), + [tiprack5Id]: getTiprackTipstate(true), + }, + pipettes: { + p100096Id: true, + }, + }, + }) + const result = replaceTip( + { + pipette: p100096Id, + dropTipLocation: 'wasteChuteId', + }, + invariantContext, + initialTestRobotState + ) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + moveToAddressableAreaHelper({ + pipetteId: p100096Id, + addressableAreaName: '96ChannelWasteChute', + }), + dropTipInPlaceHelper({ pipetteId: p100096Id }), + pickUpTipHelper('A1', { + pipetteId: p100096Id, + labwareId: tiprack5Id, + }), + ]) + }) + }) }) diff --git a/step-generation/src/commandCreators/atomic/replaceTip.ts b/step-generation/src/commandCreators/atomic/replaceTip.ts index 3485fe1d654..55e3348b78d 100644 --- a/step-generation/src/commandCreators/atomic/replaceTip.ts +++ b/step-generation/src/commandCreators/atomic/replaceTip.ts @@ -11,6 +11,7 @@ import { pipetteAdjacentHeaterShakerWhileShaking, getIsHeaterShakerEastWestWithLatchOpen, getIsHeaterShakerEastWestMultiChannelPipette, + wasteChuteCommandsUtil, } from '../../utils' import type { CommandCreatorError, @@ -104,6 +105,9 @@ export const replaceTip: CommandCreator = ( const labwareDef = invariantContext.labwareEntities[nextTiprack.tiprackId]?.def + const isWasteChute = + invariantContext.additionalEquipmentEntities[dropTipLocation] != null + if (!labwareDef) { return { errors: [ @@ -164,17 +168,35 @@ export const replaceTip: CommandCreator = ( } } - const commandCreators: CurriedCommandCreator[] = [ - curryCommandCreator(dropTip, { - pipette, - dropTipLocation, - }), - curryCommandCreator(_pickUpTip, { - pipette, - tiprack: nextTiprack.tiprackId, - well: nextTiprack.well, - }), - ] + const addressableAreaName = + pipetteSpec.channels === 96 + ? '96ChannelWasteChute' + : '1and8ChannelWasteChute' + + const commandCreators: CurriedCommandCreator[] = isWasteChute + ? [ + curryCommandCreator(wasteChuteCommandsUtil, { + type: 'dropTip', + pipetteId: pipette, + addressableAreaName, + }), + curryCommandCreator(_pickUpTip, { + pipette, + tiprack: nextTiprack.tiprackId, + well: nextTiprack.well, + }), + ] + : [ + curryCommandCreator(dropTip, { + pipette, + dropTipLocation, + }), + curryCommandCreator(_pickUpTip, { + pipette, + tiprack: nextTiprack.tiprackId, + well: nextTiprack.well, + }), + ] return reduceCommandCreators( commandCreators, diff --git a/step-generation/src/fixtures/commandFixtures.ts b/step-generation/src/fixtures/commandFixtures.ts index b3e881fa5e9..2261323f430 100644 --- a/step-generation/src/fixtures/commandFixtures.ts +++ b/step-generation/src/fixtures/commandFixtures.ts @@ -90,6 +90,7 @@ export const getFlowRateAndOffsetParamsMix = (): FlowRateAndOffsetParamsMix => ( // ================= export const DEFAULT_PIPETTE = 'p300SingleId' export const MULTI_PIPETTE = 'p300MultiId' +export const PIPETTE_96 = 'p100096Id' export const SOURCE_LABWARE = 'sourcePlateId' export const DEST_LABWARE = 'destPlateId' export const TROUGH_LABWARE = 'troughId' @@ -310,3 +311,25 @@ export const pickUpTipHelper = ( wellName: typeof tip === 'string' ? tip : tiprackWellNamesFlat[tip], }, }) +export const dropTipInPlaceHelper = (params?: { + pipetteId?: string +}): CreateCommand => ({ + commandType: 'dropTipInPlace', + key: expect.any(String), + params: { + pipetteId: DEFAULT_PIPETTE, + ...params, + }, +}) +export const moveToAddressableAreaHelper = (params?: { + pipetteId?: string + addressableAreaName: string +}): CreateCommand => ({ + commandType: 'moveToAddressableArea', + key: expect.any(String), + params: { + pipetteId: DEFAULT_PIPETTE, + addressableAreaName: '1and8ChannelWasteChute', + ...params, + }, +}) diff --git a/step-generation/src/fixtures/robotStateFixtures.ts b/step-generation/src/fixtures/robotStateFixtures.ts index aa44e110dd3..42b1fa7fa58 100644 --- a/step-generation/src/fixtures/robotStateFixtures.ts +++ b/step-generation/src/fixtures/robotStateFixtures.ts @@ -10,12 +10,15 @@ import { fixtureP10Multi as _fixtureP10Multi, fixtureP300Single as _fixtureP300Single, fixtureP300Multi as _fixtureP300Multi, + fixtureP100096 as _fixtureP100096, } from '@opentrons/shared-data/pipette/fixtures/name' import _fixtureTrash from '@opentrons/shared-data/labware/fixtures/2/fixture_trash.json' import _fixture96Plate from '@opentrons/shared-data/labware/fixtures/2/fixture_96_plate.json' import _fixture12Trough from '@opentrons/shared-data/labware/fixtures/2/fixture_12_trough.json' import _fixtureTiprack10ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_10_ul.json' import _fixtureTiprack300ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' +import _fixtureTiprack1000ul from '@opentrons/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_1000ul.json' +import _fixtureTiprackAdapter from '@opentrons/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_adapter.json' import { TEMPERATURE_APPROACHING_TARGET, TEMPERATURE_AT_TARGET, @@ -26,6 +29,7 @@ import { import { DEFAULT_PIPETTE, MULTI_PIPETTE, + PIPETTE_96, SOURCE_LABWARE, DEST_LABWARE, TROUGH_LABWARE, @@ -47,12 +51,15 @@ const fixtureP10Single = _fixtureP10Single const fixtureP10Multi = _fixtureP10Multi const fixtureP300Single = _fixtureP300Single const fixtureP300Multi = _fixtureP300Multi +const fixtureP100096 = _fixtureP100096 const fixtureTrash = _fixtureTrash as LabwareDefinition2 const fixture96Plate = _fixture96Plate as LabwareDefinition2 const fixture12Trough = _fixture12Trough as LabwareDefinition2 const fixtureTiprack10ul = _fixtureTiprack10ul as LabwareDefinition2 const fixtureTiprack300ul = _fixtureTiprack300ul as LabwareDefinition2 +const fixtureTiprack1000ul = _fixtureTiprack1000ul as LabwareDefinition2 +const fixtureTiprackAdapter = _fixtureTiprackAdapter as LabwareDefinition2 export const DEFAULT_CONFIG: Config = { OT_PD_DISABLE_MODULE_RESTRICTIONS: false, @@ -120,6 +127,30 @@ export function makeContext(): InvariantContext { labwareDefURI: getLabwareDefURI(fixtureTiprack300ul), def: fixtureTiprack300ul, }, + tiprack4AdapterId: { + id: 'tiprack4AdapterId', + + labwareDefURI: getLabwareDefURI(fixtureTiprackAdapter), + def: fixtureTiprackAdapter, + }, + tiprack5AdapterId: { + id: 'tiprack5AdapterId', + + labwareDefURI: getLabwareDefURI(fixtureTiprackAdapter), + def: fixtureTiprackAdapter, + }, + tiprack4Id: { + id: 'tiprack4Id', + + labwareDefURI: getLabwareDefURI(fixtureTiprack1000ul), + def: fixtureTiprack1000ul, + }, + tiprack5Id: { + id: 'tiprack5Id', + + labwareDefURI: getLabwareDefURI(fixtureTiprack1000ul), + def: fixtureTiprack1000ul, + }, } const moduleEntities: ModuleEntities = {} const additionalEquipmentEntities: AdditionalEquipmentEntities = {} @@ -156,6 +187,14 @@ export function makeContext(): InvariantContext { tiprackLabwareDef: fixtureTiprack300ul, spec: fixtureP300Multi, }, + [PIPETTE_96]: { + name: 'p1000_96', + id: PIPETTE_96, + + tiprackDefURI: getLabwareDefURI(fixtureTiprack1000ul), + tiprackLabwareDef: fixtureTiprack1000ul, + spec: fixtureP100096, + }, } return { labwareEntities, @@ -213,6 +252,18 @@ export const makeStateArgsStandard = (): StandardMakeStateArgs => ({ tiprack2Id: { slot: '5', }, + tiprack4AdapterId: { + slot: '7', + }, + tiprack5AdapterId: { + slot: '8', + }, + tiprack4Id: { + slot: 'tiprack4AdapterId', + }, + tiprack5Id: { + slot: 'tiprack5AdapterId', + }, sourcePlateId: { slot: '2', }, diff --git a/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts b/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts index 19a20c04db3..19028db007f 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts @@ -24,5 +24,7 @@ export const forDropTipInPlace = ( invariantContext: InvariantContext, robotStateAndWarnings: RobotStateAndWarnings ): void => { - // TODO(jr, 11/6/23): update state + const { pipetteId } = params + const { robotState } = robotStateAndWarnings + robotState.tipState.pipettes[pipetteId] = false } diff --git a/step-generation/src/utils/wasteChuteCommandsUtil.ts b/step-generation/src/utils/wasteChuteCommandsUtil.ts index 29df4f550ca..1473026450d 100644 --- a/step-generation/src/utils/wasteChuteCommandsUtil.ts +++ b/step-generation/src/utils/wasteChuteCommandsUtil.ts @@ -62,15 +62,18 @@ export const wasteChuteCommandsUtil: CommandCreator = ( let commands: CurriedCommandCreator[] = [] switch (type) { case 'dropTip': { - commands = [ - curryCommandCreator(moveToAddressableArea, { - pipetteId, - addressableAreaName, - }), - curryCommandCreator(dropTipInPlace, { - pipetteId, - }), - ] + commands = !prevRobotState.tipState.pipettes[pipetteId] + ? [] + : [ + curryCommandCreator(moveToAddressableArea, { + pipetteId, + addressableAreaName, + }), + curryCommandCreator(dropTipInPlace, { + pipetteId, + }), + ] + break } case 'dispense': { From e5eb78bb740f3526ce2e73dffef6a24c19335883 Mon Sep 17 00:00:00 2001 From: Brent Hagen Date: Tue, 14 Nov 2023 22:54:43 -0500 Subject: [PATCH 11/46] refactor(app): remove app fixture stubs (#13980) remove fixture stubs throughout desktop/ODD app, update types, and add deck conflict compatibility checks closes RAUT-859 --------- Co-authored-by: Brian Cooper --- .../src/deck_configuration/__stubs__/index.ts | 102 ++++---- .../createDeckConfiguration.ts | 29 --- .../deleteDeckConfiguration.ts | 30 --- .../getDeckConfiguration.ts | 2 +- api-client/src/deck_configuration/index.ts | 2 - .../updateDeckConfiguration.ts | 31 +-- .../__tests__/DeckThumbnail.test.tsx | 20 +- app/src/molecules/DeckThumbnail/index.tsx | 4 +- .../AddFixtureModal.tsx | 83 +++--- .../__tests__/AddFixtureModal.test.tsx | 52 ++-- .../DeviceDetailsDeckConfiguration/index.tsx | 51 ++-- .../Devices/ProtocolRun/ProtocolRunSetup.tsx | 67 +---- .../SetupLabware/SetupLabwareMap.tsx | 4 +- .../SetupLiquids/SetupLiquidsMap.tsx | 4 +- .../__tests__/SetupLiquidsMap.test.tsx | 13 +- .../LocationConflictModal.tsx | 77 +++--- .../SetupModuleAndDeck/NotConfiguredModal.tsx | 29 ++- .../SetupModuleAndDeck/SetupFixtureList.tsx | 95 +++---- .../SetupModuleAndDeck/SetupModulesList.tsx | 24 +- .../SetupModuleAndDeck/SetupModulesMap.tsx | 9 +- .../__tests__/LocationConflictModal.test.tsx | 29 +-- .../__tests__/NotConfiguredModal.test.tsx | 24 +- .../__tests__/SetupFixtureList.test.tsx | 88 +++---- .../__tests__/SetupModulesAndDeck.test.tsx | 21 +- .../__tests__/SetupModulesList.test.tsx | 16 +- .../__tests__/utils.test.ts | 6 +- .../ProtocolRun/SetupModuleAndDeck/index.tsx | 36 ++- .../ProtocolRun/SetupModuleAndDeck/utils.ts | 47 ++-- .../__tests__/ProtocolRunSetup.test.tsx | 7 + ...seModuleRenderInfoForProtocolById.test.tsx | 51 ++-- .../useModuleRenderInfoForProtocolById.ts | 32 ++- .../RobotConfigurationDetails.tsx | 21 +- app/src/organisms/ProtocolDetails/index.tsx | 39 +-- .../ProtocolSetupDeckConfiguration.test.tsx | 28 +- .../ProtocolSetupDeckConfiguration/index.tsx | 70 ++--- .../LabwareMapViewModal.tsx | 4 +- .../__tests__/LabwareMapViewModal.test.tsx | 8 +- .../FixtureTable.tsx | 241 +++++++++--------- .../ModulesAndDeckMapViewModal.tsx | 4 +- .../__tests__/FixtureTable.test.tsx | 127 ++++----- .../ModulesAndDeckMapViewModal.test.tsx | 17 +- .../ProtocolSetupModulesAndDeck.test.tsx | 22 +- .../ProtocolSetupModulesAndDeck/index.tsx | 41 ++- .../__tests__/DeckConfiguration.test.tsx | 19 +- app/src/pages/DeckConfiguration/index.tsx | 41 +-- .../ProtocolDetails/Hardware.tsx | 2 +- .../__tests__/Hardware.test.tsx | 12 +- .../__tests__/ProtocolSetup.test.tsx | 16 +- .../OnDeviceDisplay/ProtocolSetup/index.tsx | 14 +- app/src/pages/Protocols/hooks/index.ts | 100 ++++---- .../__tests__/hooks.test.ts | 123 ++------- app/src/resources/deck_configuration/hooks.ts | 100 ++++---- app/src/resources/deck_configuration/types.ts | 11 + app/src/resources/deck_configuration/utils.ts | 161 ++++++------ .../src/hardware-sim/BaseDeck/BaseDeck.tsx | 36 +-- .../BaseDeck/__fixtures__/index.ts | 192 ++++++-------- .../hardware-sim/DeckConfigurator/index.tsx | 62 ++--- .../src/components/DeckSetup/constants.ts | 23 -- .../src/components/DeckSetup/index.tsx | 33 +-- .../CreateFileWizard/StagingAreaTile.tsx | 65 +++-- .../components/modules/StagingAreasModal.tsx | 51 ++-- ...seCreateDeckConfigurationMutation.test.tsx | 89 ------- .../src/deck_configuration/index.ts | 1 - .../useCreateDeckConfigurationMutation.ts | 63 ----- .../useUpdateDeckConfigurationMutation.ts | 20 +- shared-data/command/types/setup.ts | 3 +- shared-data/deck/types/schemaV4.ts | 3 + shared-data/js/constants.ts | 41 ++- shared-data/js/fixtures.ts | 39 ++- shared-data/js/types.ts | 55 +--- .../protocol/types/schemaV7/command/setup.ts | 3 +- 71 files changed, 1324 insertions(+), 1761 deletions(-) delete mode 100644 api-client/src/deck_configuration/createDeckConfiguration.ts delete mode 100644 api-client/src/deck_configuration/deleteDeckConfiguration.ts create mode 100644 app/src/resources/deck_configuration/types.ts delete mode 100644 react-api-client/src/deck_configuration/__tests__/useCreateDeckConfigurationMutation.test.tsx delete mode 100644 react-api-client/src/deck_configuration/useCreateDeckConfigurationMutation.ts diff --git a/api-client/src/deck_configuration/__stubs__/index.ts b/api-client/src/deck_configuration/__stubs__/index.ts index 2197c25baaa..1e23939c0ee 100644 --- a/api-client/src/deck_configuration/__stubs__/index.ts +++ b/api-client/src/deck_configuration/__stubs__/index.ts @@ -1,73 +1,61 @@ -import { v4 as uuidv4 } from 'uuid' - import { - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_CENTER_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, } from '@opentrons/shared-data' -import type { Fixture } from '@opentrons/shared-data' +import type { DeckConfiguration } from '@opentrons/shared-data' -export const DECK_CONFIG_STUB: { [fixtureLocation: string]: Fixture } = { - cutoutA1: { - fixtureLocation: 'cutoutA1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), +export const DECK_CONFIG_STUB: DeckConfiguration = [ + { + cutoutId: 'cutoutA1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, - cutoutB1: { - fixtureLocation: 'cutoutB1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutB1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, - cutoutC1: { - fixtureLocation: 'cutoutC1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutC1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, - cutoutD1: { - fixtureLocation: 'cutoutD1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutD1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, - cutoutA2: { - fixtureLocation: 'cutoutA2', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutA2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, }, - cutoutB2: { - fixtureLocation: 'cutoutB2', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutB2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, }, - cutoutC2: { - fixtureLocation: 'cutoutC2', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutC2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, }, - cutoutD2: { - fixtureLocation: 'cutoutD2', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutD2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, }, - cutoutA3: { - fixtureLocation: 'cutoutA3', - loadName: TRASH_BIN_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutA3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, }, - cutoutB3: { - fixtureLocation: 'cutoutB3', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutB3', + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, }, - cutoutC3: { - fixtureLocation: 'cutoutC3', - loadName: STAGING_AREA_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutC3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, }, - cutoutD3: { - fixtureLocation: 'cutoutD3', - loadName: WASTE_CHUTE_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutD3', + cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, }, -} +] diff --git a/api-client/src/deck_configuration/createDeckConfiguration.ts b/api-client/src/deck_configuration/createDeckConfiguration.ts deleted file mode 100644 index 09a2f3b73d7..00000000000 --- a/api-client/src/deck_configuration/createDeckConfiguration.ts +++ /dev/null @@ -1,29 +0,0 @@ -// import { POST, request } from '../request' -import { DECK_CONFIG_STUB } from './__stubs__' - -import type { DeckConfiguration } from '@opentrons/shared-data' -// import type { ResponsePromise } from '../request' -import type { HostConfig } from '../types' - -// TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready -// export function createDeckConfiguration( -// config: HostConfig, -// data: DeckConfiguration -// ): ResponsePromise { -// return request( -// POST, -// `/deck_configuration`, -// { data }, -// config -// ) -// } - -export function createDeckConfiguration( - config: HostConfig, - data: DeckConfiguration -): Promise<{ data: DeckConfiguration }> { - data.forEach(fixture => { - DECK_CONFIG_STUB[fixture.fixtureLocation] = fixture - }) - return Promise.resolve({ data: Object.values(DECK_CONFIG_STUB) }) -} diff --git a/api-client/src/deck_configuration/deleteDeckConfiguration.ts b/api-client/src/deck_configuration/deleteDeckConfiguration.ts deleted file mode 100644 index e3689f01559..00000000000 --- a/api-client/src/deck_configuration/deleteDeckConfiguration.ts +++ /dev/null @@ -1,30 +0,0 @@ -// import { DELETE, request } from '../request' -import { DECK_CONFIG_STUB } from './__stubs__' - -import type { Fixture } from '@opentrons/shared-data' -// import type { ResponsePromise } from '../request' -import type { EmptyResponse, HostConfig } from '../types' - -// TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready -// export function deleteDeckConfiguration( -// config: HostConfig, -// data: Fixture -// ): ResponsePromise { -// const { fixtureLocation, ...rest } = data -// return request }>( -// DELETE, -// `/deck_configuration/${fixtureLocation}`, -// { data: rest }, -// config -// ) -// } - -export function deleteDeckConfiguration( - config: HostConfig, - data: Fixture -): Promise { - const { fixtureLocation } = data - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete DECK_CONFIG_STUB[fixtureLocation] - return Promise.resolve({ data: null }) -} diff --git a/api-client/src/deck_configuration/getDeckConfiguration.ts b/api-client/src/deck_configuration/getDeckConfiguration.ts index bc8c556e255..83f0734cbdb 100644 --- a/api-client/src/deck_configuration/getDeckConfiguration.ts +++ b/api-client/src/deck_configuration/getDeckConfiguration.ts @@ -15,5 +15,5 @@ import type { HostConfig } from '../types' export function getDeckConfiguration( config: HostConfig ): Promise<{ data: DeckConfiguration }> { - return Promise.resolve({ data: Object.values(DECK_CONFIG_STUB) }) + return Promise.resolve({ data: DECK_CONFIG_STUB }) } diff --git a/api-client/src/deck_configuration/index.ts b/api-client/src/deck_configuration/index.ts index c22cba0ae78..fd7ba31b0f0 100644 --- a/api-client/src/deck_configuration/index.ts +++ b/api-client/src/deck_configuration/index.ts @@ -1,4 +1,2 @@ -export { createDeckConfiguration } from './createDeckConfiguration' -export { deleteDeckConfiguration } from './deleteDeckConfiguration' export { getDeckConfiguration } from './getDeckConfiguration' export { updateDeckConfiguration } from './updateDeckConfiguration' diff --git a/api-client/src/deck_configuration/updateDeckConfiguration.ts b/api-client/src/deck_configuration/updateDeckConfiguration.ts index a02fb1af4b0..a61d56372a3 100644 --- a/api-client/src/deck_configuration/updateDeckConfiguration.ts +++ b/api-client/src/deck_configuration/updateDeckConfiguration.ts @@ -1,32 +1,25 @@ -import { v4 as uuidv4 } from 'uuid' - -// import { PATCH, request } from '../request' +// import { PUT, request } from '../request' import { DECK_CONFIG_STUB } from './__stubs__' -import type { Fixture } from '@opentrons/shared-data' +import type { DeckConfiguration } from '@opentrons/shared-data' // import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' // TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready // export function updateDeckConfiguration( // config: HostConfig, -// data: Omit -// ): ResponsePromise { -// const { fixtureLocation, ...rest } = data -// return request }>( -// PATCH, -// `/deck_configuration/${fixtureLocation}`, -// { data: rest }, -// config -// ) +// data: DeckConfiguration +// ): ResponsePromise { +// return request(PUT, '/deck_configuration', data, config) // } export function updateDeckConfiguration( config: HostConfig, - data: Omit -): Promise<{ data: Fixture }> { - const { fixtureLocation } = data - const fixtureId = uuidv4() - DECK_CONFIG_STUB[fixtureLocation] = { ...data, fixtureId } - return Promise.resolve({ data: DECK_CONFIG_STUB[fixtureLocation] }) + data: DeckConfiguration +): Promise<{ data: DeckConfiguration }> { + data.forEach((fixture, i) => { + DECK_CONFIG_STUB[i] = fixture + }) + + return Promise.resolve({ data: DECK_CONFIG_STUB }) } diff --git a/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx b/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx index 2c47a31532e..16b2555fda2 100644 --- a/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx +++ b/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx @@ -11,7 +11,6 @@ import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_st import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' import { BaseDeck, - EXTENDED_DECK_CONFIG_FIXTURE, partialComponentPropsMatcher, renderWithProviders, } from '@opentrons/components' @@ -24,7 +23,7 @@ import { import { i18n } from '../../../i18n' import { useAttachedModules } from '../../../organisms/Devices/hooks' import { getStandardDeckViewLayerBlockList } from '../utils/getStandardDeckViewLayerBlockList' -import { getDeckConfigFromProtocolCommands } from '../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../resources/deck_configuration/utils' import { getAttachedProtocolModuleMatches } from '../../../organisms/ProtocolSetupModulesAndDeck/utils' import { getProtocolModulesInfo } from '../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { getLabwareRenderInfo } from '../../../organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo' @@ -66,8 +65,8 @@ const mockParseLabwareInfoByLiquidId = parseLabwareInfoByLiquidId as jest.Mocked const mockUseAttachedModules = useAttachedModules as jest.MockedFunction< typeof useAttachedModules > -const mockGetDeckConfigFromProtocolCommands = getDeckConfigFromProtocolCommands as jest.MockedFunction< - typeof getDeckConfigFromProtocolCommands +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands > const mockGetLabwareRenderInfo = getLabwareRenderInfo as jest.MockedFunction< typeof getLabwareRenderInfo @@ -127,9 +126,11 @@ describe('DeckThumbnail', () => { mockUseAttachedModules.mockReturnValue( mockFetchModulesSuccessActionPayloadModules ) - when(mockGetDeckConfigFromProtocolCommands) + when(mockGetSimplestDeckConfigForProtocolCommands) .calledWith(commands) - .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) + .mockReturnValue([]) + // TODO(bh, 2023-11-13): mock the cutout config protocol spec + // .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) when(mockGetLabwareRenderInfo).mockReturnValue({}) when(mockGetProtocolModulesInfo) .calledWith(protocolAnalysis, ot2StandardDeckDef as any) @@ -239,9 +240,11 @@ describe('DeckThumbnail', () => { mockUseAttachedModules.mockReturnValue( mockFetchModulesSuccessActionPayloadModules ) - when(mockGetDeckConfigFromProtocolCommands) + when(mockGetSimplestDeckConfigForProtocolCommands) .calledWith(commands) - .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) + .mockReturnValue([]) + // TODO(bh, 2023-11-13): mock the cutout config protocol spec + // .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) when(mockGetLabwareRenderInfo).mockReturnValue({ [MOCK_300_UL_TIPRACK_ID]: { labwareDef: fixture_tiprack_300_ul as LabwareDefinition2, @@ -295,7 +298,6 @@ describe('DeckThumbnail', () => { deckLayerBlocklist: getStandardDeckViewLayerBlockList( FLEX_ROBOT_TYPE ), - deckConfig: EXTENDED_DECK_CONFIG_FIXTURE, labwareLocations: expect.anything(), moduleLocations: expect.anything(), }) diff --git a/app/src/molecules/DeckThumbnail/index.tsx b/app/src/molecules/DeckThumbnail/index.tsx index f4295fa8dd9..e75e244dca1 100644 --- a/app/src/molecules/DeckThumbnail/index.tsx +++ b/app/src/molecules/DeckThumbnail/index.tsx @@ -13,7 +13,7 @@ import { } from '@opentrons/api-client' import { getStandardDeckViewLayerBlockList } from './utils/getStandardDeckViewLayerBlockList' -import { getDeckConfigFromProtocolCommands } from '../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { getLabwareRenderInfo } from '../../organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo' import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { useAttachedModules } from '../../organisms/Devices/hooks' @@ -43,7 +43,7 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element | null { protocolAnalysis.commands ) - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) const liquids = protocolAnalysis.liquids diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx index 9fec31200e9..a33f55db2a2 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx @@ -14,13 +14,18 @@ import { SPACING, TYPOGRAPHY, } from '@opentrons/components' -import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' +import { + useDeckConfigurationQuery, + useUpdateDeckConfigurationMutation, +} from '@opentrons/react-api-client' import { getCutoutDisplayName, getFixtureDisplayName, - STAGING_AREA_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + STAGING_AREA_CUTOUTS, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_CUTOUT, + WASTE_CHUTE_FIXTURES, } from '@opentrons/shared-data' import { StyledText } from '../../atoms/text' @@ -30,24 +35,24 @@ import { Modal } from '../../molecules/Modal' import { LegacyModal } from '../../molecules/LegacyModal' import type { - Cutout, + CutoutConfig, + CutoutId, + CutoutFixtureId, DeckConfiguration, - Fixture, - FixtureLoadName, } from '@opentrons/shared-data' import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' import type { LegacyModalProps } from '../../molecules/LegacyModal' interface AddFixtureModalProps { - fixtureLocation: Cutout + cutoutId: CutoutId setShowAddFixtureModal: (showAddFixtureModal: boolean) => void - setCurrentDeckConfig?: React.Dispatch> - providedFixtureOptions?: FixtureLoadName[] + setCurrentDeckConfig?: React.Dispatch> + providedFixtureOptions?: CutoutFixtureId[] isOnDevice?: boolean } export function AddFixtureModal({ - fixtureLocation, + cutoutId, setShowAddFixtureModal, setCurrentDeckConfig, providedFixtureOptions, @@ -55,10 +60,11 @@ export function AddFixtureModal({ }: AddFixtureModalProps): JSX.Element { const { t } = useTranslation('device_details') const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() + const deckConfig = useDeckConfigurationQuery()?.data ?? [] const modalHeader: ModalHeaderBaseProps = { title: t('add_to_slot', { - slotName: getCutoutDisplayName(fixtureLocation), + slotName: getCutoutDisplayName(cutoutId), }), hasExitIcon: true, onClick: () => setShowAddFixtureModal(false), @@ -66,7 +72,7 @@ export function AddFixtureModal({ const modalProps: LegacyModalProps = { title: t('add_to_slot', { - slotName: getCutoutDisplayName(fixtureLocation), + slotName: getCutoutDisplayName(cutoutId), }), onClose: () => setShowAddFixtureModal(false), closeOnOutsideClick: true, @@ -74,26 +80,22 @@ export function AddFixtureModal({ width: '23.125rem', } - const availableFixtures: FixtureLoadName[] = [TRASH_BIN_LOAD_NAME] - if ( - fixtureLocation === 'cutoutA3' || - fixtureLocation === 'cutoutB3' || - fixtureLocation === 'cutoutC3' - ) { - availableFixtures.push(STAGING_AREA_LOAD_NAME) + const availableFixtures: CutoutFixtureId[] = [TRASH_BIN_ADAPTER_FIXTURE] + if (STAGING_AREA_CUTOUTS.includes(cutoutId)) { + availableFixtures.push(STAGING_AREA_RIGHT_SLOT_FIXTURE) } - if (fixtureLocation === 'cutoutD3') { - availableFixtures.push(STAGING_AREA_LOAD_NAME, WASTE_CHUTE_LOAD_NAME) + if (cutoutId === WASTE_CHUTE_CUTOUT) { + availableFixtures.push(...WASTE_CHUTE_FIXTURES) } // For Touchscreen app - const handleTapAdd = (fixtureLoadName: FixtureLoadName): void => { + const handleTapAdd = (requiredFixtureId: CutoutFixtureId): void => { if (setCurrentDeckConfig != null) setCurrentDeckConfig( (prevDeckConfig: DeckConfiguration): DeckConfiguration => - prevDeckConfig.map((fixture: Fixture) => - fixture.fixtureLocation === fixtureLocation - ? { ...fixture, loadName: fixtureLoadName } + prevDeckConfig.map((fixture: CutoutConfig) => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: requiredFixtureId } : fixture ) ) @@ -104,11 +106,14 @@ export function AddFixtureModal({ // For Desktop app const fixtureOptions = providedFixtureOptions ?? availableFixtures - const handleClickAdd = (fixtureLoadName: FixtureLoadName): void => { - updateDeckConfiguration({ - fixtureLocation, - loadName: fixtureLoadName, - }) + const handleClickAdd = (requiredFixtureId: CutoutFixtureId): void => { + const newDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: requiredFixtureId } + : fixture + ) + + updateDeckConfiguration(newDeckConfig) setShowAddFixtureModal(false) } @@ -122,10 +127,10 @@ export function AddFixtureModal({ {t('add_to_slot_description')} - {fixtureOptions.map(fixture => ( - + {fixtureOptions.map(cutoutFixtureId => ( + @@ -166,18 +171,18 @@ export function AddFixtureModal({ } interface AddFixtureButtonProps { - fixtureLoadName: FixtureLoadName - handleClickAdd: (fixtureLoadName: FixtureLoadName) => void + cutoutFixtureId: CutoutFixtureId + handleClickAdd: (cutoutFixtureId: CutoutFixtureId) => void } function AddFixtureButton({ - fixtureLoadName, + cutoutFixtureId, handleClickAdd, }: AddFixtureButtonProps): JSX.Element { const { t } = useTranslation('device_details') return ( handleClickAdd(fixtureLoadName)} + onClick={() => handleClickAdd(cutoutFixtureId)} display="flex" justifyContent={JUSTIFY_SPACE_BETWEEN} flexDirection={DIRECTION_ROW} @@ -186,7 +191,7 @@ function AddFixtureButton({ css={FIXTURE_BUTTON_STYLE} > - {getFixtureDisplayName(fixtureLoadName)} + {getFixtureDisplayName(cutoutFixtureId)} {t('add')} diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx index f20f887bfd4..e31de9bb461 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx @@ -1,12 +1,17 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' -import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' -import { TRASH_BIN_LOAD_NAME } from '@opentrons/shared-data' +import { + useDeckConfigurationQuery, + useUpdateDeckConfigurationMutation, +} from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { AddFixtureModal } from '../AddFixtureModal' +import type { UseQueryResult } from 'react-query' +import type { DeckConfiguration } from '@opentrons/shared-data' + jest.mock('@opentrons/react-api-client') const mockSetShowAddFixtureModal = jest.fn() const mockUpdateDeckConfiguration = jest.fn() @@ -15,6 +20,9 @@ const mockSetCurrentDeckConfig = jest.fn() const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction< typeof useUpdateDeckConfigurationMutation > +const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction< + typeof useDeckConfigurationQuery +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -27,7 +35,7 @@ describe('Touchscreen AddFixtureModal', () => { beforeEach(() => { props = { - fixtureLocation: 'cutoutD3', + cutoutId: 'cutoutD3', setShowAddFixtureModal: mockSetShowAddFixtureModal, setCurrentDeckConfig: mockSetCurrentDeckConfig, isOnDevice: true, @@ -35,6 +43,9 @@ describe('Touchscreen AddFixtureModal', () => { mockUseUpdateDeckConfigurationMutation.mockReturnValue({ updateDeckConfiguration: mockUpdateDeckConfiguration, } as any) + mockUseDeckConfigurationQuery.mockReturnValue(({ + data: [], + } as unknown) as UseQueryResult) }) it('should render text and buttons', () => { @@ -43,10 +54,10 @@ describe('Touchscreen AddFixtureModal', () => { getByText( 'Choose a fixture below to add to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Staging Area Slot') - getByText('Trash Bin') - getByText('Waste Chute') - expect(getAllByText('Add').length).toBe(3) + getByText('Staging area slot') + getByText('Trash bin') + getByText('Waste chute only') + expect(getAllByText('Add').length).toBe(6) }) it('should a mock function when tapping app button', () => { @@ -61,7 +72,7 @@ describe('Desktop AddFixtureModal', () => { beforeEach(() => { props = { - fixtureLocation: 'cutoutD3', + cutoutId: 'cutoutD3', setShowAddFixtureModal: mockSetShowAddFixtureModal, } mockUseUpdateDeckConfigurationMutation.mockReturnValue({ @@ -79,42 +90,39 @@ describe('Desktop AddFixtureModal', () => { getByText( 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Staging Area Slot') - getByText('Trash Bin') - getByText('Waste Chute') - expect(getAllByRole('button', { name: 'Add' }).length).toBe(3) + getByText('Staging area slot') + getByText('Trash bin') + getByText('Waste chute only') + expect(getAllByRole('button', { name: 'Add' }).length).toBe(6) }) it('should render text and buttons slot A1', () => { - props = { ...props, fixtureLocation: 'cutoutA1' } + props = { ...props, cutoutId: 'cutoutA1' } const [{ getByText, getByRole }] = render(props) getByText('Add to slot A1') getByText( 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Trash Bin') + getByText('Trash bin') getByRole('button', { name: 'Add' }) }) it('should render text and buttons slot B3', () => { - props = { ...props, fixtureLocation: 'cutoutB3' } + props = { ...props, cutoutId: 'cutoutB3' } const [{ getByText, getAllByRole }] = render(props) getByText('Add to slot B3') getByText( 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Staging Area Slot') - getByText('Trash Bin') + getByText('Staging area slot') + getByText('Trash bin') expect(getAllByRole('button', { name: 'Add' }).length).toBe(2) }) it('should call a mock function when clicking add button', () => { - props = { ...props, fixtureLocation: 'cutoutA1' } + props = { ...props, cutoutId: 'cutoutA1' } const [{ getByRole }] = render(props) getByRole('button', { name: 'Add' }).click() - expect(mockUpdateDeckConfiguration).toHaveBeenCalledWith({ - fixtureLocation: 'cutoutA1', - loadName: TRASH_BIN_LOAD_NAME, - }) + expect(mockUpdateDeckConfiguration).toHaveBeenCalled() }) }) diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx index c57cef374f8..e925a0d057f 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx @@ -24,7 +24,10 @@ import { import { getCutoutDisplayName, getFixtureDisplayName, - STANDARD_SLOT_LOAD_NAME, + SINGLE_RIGHT_CUTOUTS, + SINGLE_SLOT_FIXTURES, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { StyledText } from '../../atoms/text' @@ -33,7 +36,7 @@ import { DeckFixtureSetupInstructionsModal } from './DeckFixtureSetupInstruction import { AddFixtureModal } from './AddFixtureModal' import { useRunStatuses } from '../Devices/hooks' -import type { Cutout } from '@opentrons/shared-data' +import type { CutoutId } from '@opentrons/shared-data' const RUN_REFETCH_INTERVAL = 5000 @@ -52,10 +55,9 @@ export function DeviceDetailsDeckConfiguration({ const [showAddFixtureModal, setShowAddFixtureModal] = React.useState( false ) - const [ - targetFixtureLocation, - setTargetFixtureLocation, - ] = React.useState(null) + const [targetCutoutId, setTargetCutoutId] = React.useState( + null + ) const deckConfig = useDeckConfigurationQuery().data ?? [] const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() @@ -65,29 +67,38 @@ export function DeviceDetailsDeckConfiguration({ }) const isMaintenanceRunExisting = maintenanceRunData?.data?.id != null - const handleClickAdd = (fixtureLocation: Cutout): void => { - setTargetFixtureLocation(fixtureLocation) + const handleClickAdd = (cutoutId: CutoutId): void => { + setTargetCutoutId(cutoutId) setShowAddFixtureModal(true) } - const handleClickRemove = (fixtureLocation: Cutout): void => { - console.log('remove fixtureLocation', fixtureLocation) - updateDeckConfiguration({ - fixtureLocation, - loadName: STANDARD_SLOT_LOAD_NAME, - }) + const handleClickRemove = (cutoutId: CutoutId): void => { + const isRightCutout = SINGLE_RIGHT_CUTOUTS.includes(cutoutId) + const singleSlotFixture = isRightCutout + ? SINGLE_RIGHT_SLOT_FIXTURE + : SINGLE_LEFT_SLOT_FIXTURE + + const newDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: singleSlotFixture } + : fixture + ) + + updateDeckConfiguration(newDeckConfig) } // do not show standard slot in fixture display list const fixtureDisplayList = deckConfig.filter( - fixture => fixture.loadName !== STANDARD_SLOT_LOAD_NAME + fixture => + fixture.cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) ) return ( <> - {showAddFixtureModal && targetFixtureLocation != null ? ( + {showAddFixtureModal && targetCutoutId != null ? ( ) : null} @@ -182,7 +193,7 @@ export function DeviceDetailsDeckConfiguration({ {fixtureDisplayList.map(fixture => { return ( - {getCutoutDisplayName(fixture.fixtureLocation)} + {getCutoutDisplayName(fixture.cutoutId)} - {getFixtureDisplayName(fixture.loadName)} + {getFixtureDisplayName(fixture.cutoutFixtureId)} ) diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx index bf059c06930..aa45ca4191d 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -1,10 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { - LoadedFixturesBySlot, - parseAllRequiredModuleModels, -} from '@opentrons/api-client' +import { parseAllRequiredModuleModels } from '@opentrons/api-client' import { Flex, ALIGN_CENTER, @@ -17,15 +14,11 @@ import { TYPOGRAPHY, Link, } from '@opentrons/components' -import { - STAGING_AREA_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, -} from '@opentrons/shared-data' import { Line } from '../../../atoms/structure' import { StyledText } from '../../../atoms/text' import { InfoMessage } from '../../../molecules/InfoMessage' +import { getSimplestDeckConfigForProtocolCommands } from '../../../resources/deck_configuration/utils' import { useIsFlex, useRobot, @@ -76,52 +69,6 @@ export function ProtocolRunSetup({ const protocolData = robotProtocolAnalysis ?? storedProtocolAnalysis const modules = parseAllRequiredModuleModels(protocolData?.commands ?? []) - // TODO(Jr, 10/4/23): stubbed in the fixtures for now - delete IMMEDIATELY - // const loadedFixturesBySlot = parseInitialLoadedFixturesByCutout( - // protocolData?.commands ?? [] - // ) - - const STUBBED_LOAD_FIXTURE_BY_SLOT: LoadedFixturesBySlot = { - D3: { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - }, - B3: { - id: 'stubbed_load_fixture_2', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId_2', - loadName: STAGING_AREA_LOAD_NAME, - location: { cutout: 'cutoutB3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - }, - C3: { - id: 'stubbed_load_fixture_3', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId_3', - loadName: TRASH_BIN_LOAD_NAME, - location: { cutout: 'cutoutC3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - }, - } const robot = useRobot(robotName) const calibrationStatusRobot = useRunCalibrationStatus(robotName, runId) const calibrationStatusModules = useModuleCalibrationStatus(robotName, runId) @@ -168,8 +115,12 @@ export function ProtocolRunSetup({ if (robot == null) return null const hasLiquids = protocolData != null && protocolData.liquids?.length > 0 const hasModules = protocolData != null && modules.length > 0 - const hasFixtures = - protocolData != null && Object.keys(STUBBED_LOAD_FIXTURE_BY_SLOT).length > 0 + + const protocolDeckConfig = getSimplestDeckConfigForProtocolCommands( + protocolData?.commands ?? [] + ) + + const hasFixtures = protocolDeckConfig.length > 0 let moduleDescription: string = t(`${MODULE_SETUP_KEY}_description`, { count: modules.length, @@ -213,8 +164,8 @@ export function ProtocolRunSetup({ expandLabwarePositionCheckStep={() => setExpandedStepKey(LPC_KEY)} robotName={robotName} runId={runId} - loadedFixturesBySlot={STUBBED_LOAD_FIXTURE_BY_SLOT} hasModules={hasModules} + commands={protocolData?.commands ?? []} /> ), description: moduleDescription, diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx index 0225ff8e575..cdaaddce611 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx @@ -17,7 +17,7 @@ import { } from '@opentrons/shared-data' import { getLabwareSetupItemGroups } from '../../../../pages/Protocols/utils' -import { getDeckConfigFromProtocolCommands } from '../../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils' import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils' import { useAttachedModules } from '../../hooks' import { LabwareInfoOverlay } from '../LabwareInfoOverlay' @@ -107,7 +107,7 @@ export function SetupLabwareMap({ const { offDeckItems } = getLabwareSetupItemGroups(commands) - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx index 3b718b29d52..0c6a4ac80b4 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx @@ -25,7 +25,7 @@ import { LabwareInfoOverlay } from '../LabwareInfoOverlay' import { LiquidsLabwareDetailsModal } from './LiquidsLabwareDetailsModal' import { getWellFillFromLabwareId } from './utils' import { getLabwareRenderInfo } from '../utils/getLabwareRenderInfo' -import { getDeckConfigFromProtocolCommands } from '../../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils' import { getStandardDeckViewLayerBlockList } from '../utils/getStandardDeckViewLayerBlockList' import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils' import { getProtocolModulesInfo } from '../utils/getProtocolModulesInfo' @@ -70,7 +70,7 @@ export function SetupLiquidsMap( const labwareByLiquidId = parseLabwareInfoByLiquidId( protocolAnalysis.commands ?? [] ) - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) const deckLayerBlocklist = getStandardDeckViewLayerBlockList(robotType) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx index 98e51b6ba4d..e4786c8d522 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx @@ -6,7 +6,6 @@ import { renderWithProviders, partialComponentPropsMatcher, LabwareRender, - EXTENDED_DECK_CONFIG_FIXTURE, } from '@opentrons/components' import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' @@ -31,7 +30,7 @@ import { getLabwareRenderInfo } from '../../utils/getLabwareRenderInfo' import { getStandardDeckViewLayerBlockList } from '../../utils/getStandardDeckViewLayerBlockList' import { getAttachedProtocolModuleMatches } from '../../../../ProtocolSetupModulesAndDeck/utils' import { getProtocolModulesInfo } from '../../utils/getProtocolModulesInfo' -import { getDeckConfigFromProtocolCommands } from '../../../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../../resources/deck_configuration/utils' import { mockProtocolModuleInfo } from '../../../../ProtocolSetupLabware/__fixtures__' import { mockFetchModulesSuccessActionPayloadModules } from '../../../../../redux/modules/__fixtures__' @@ -97,8 +96,8 @@ const mockGetAttachedProtocolModuleMatches = getAttachedProtocolModuleMatches as const mockGetProtocolModulesInfo = getProtocolModulesInfo as jest.MockedFunction< typeof getProtocolModulesInfo > -const mockGetDeckConfigFromProtocolCommands = getDeckConfigFromProtocolCommands as jest.MockedFunction< - typeof getDeckConfigFromProtocolCommands +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands > const RUN_ID = '1' @@ -164,9 +163,10 @@ describe('SetupLiquidsMap', () => { when(mockGetLabwareRenderInfo) .calledWith(simpleAnalysisFileFixture as any, ot2StandardDeckDef as any) .mockReturnValue({}) - when(mockGetDeckConfigFromProtocolCommands) + when(mockGetSimplestDeckConfigForProtocolCommands) .calledWith(simpleAnalysisFileFixture.commands as RunTimeCommand[]) - .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) + // TODO(bh, 2023-11-13): mock the cutout config protocol spec + .mockReturnValue([]) when(mockGetRobotTypeFromLoadedLabware) .calledWith(simpleAnalysisFileFixture.labware as any) .mockReturnValue(FLEX_ROBOT_TYPE) @@ -335,7 +335,6 @@ describe('SetupLiquidsMap', () => { when(mockBaseDeck) .calledWith( partialComponentPropsMatcher({ - deckConfig: EXTENDED_DECK_CONFIG_FIXTURE, deckLayerBlocklist: getStandardDeckViewLayerBlockList( FLEX_ROBOT_TYPE ), diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx index c4960344fa3..10f3dfeb7e3 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx @@ -24,7 +24,9 @@ import { getCutoutDisplayName, getFixtureDisplayName, getModuleDisplayName, - STANDARD_SLOT_LOAD_NAME, + SINGLE_RIGHT_CUTOUTS, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { Portal } from '../../../../App/portal' import { LegacyModal } from '../../../../molecules/LegacyModal' @@ -33,16 +35,16 @@ import { Modal } from '../../../../molecules/Modal' import { SmallButton } from '../../../../atoms/buttons/SmallButton' import type { - Cutout, - Fixture, - FixtureLoadName, + CutoutConfig, + CutoutId, + CutoutFixtureId, ModuleModel, } from '@opentrons/shared-data' interface LocationConflictModalProps { onCloseClick: () => void - cutout: Cutout - requiredFixture?: FixtureLoadName + cutoutId: CutoutId + requiredFixtureId?: CutoutFixtureId requiredModule?: ModuleModel isOnDevice?: boolean } @@ -52,33 +54,44 @@ export const LocationConflictModal = ( ): JSX.Element => { const { onCloseClick, - cutout, - requiredFixture, + cutoutId, + requiredFixtureId, requiredModule, isOnDevice = false, } = props const { t, i18n } = useTranslation(['protocol_setup', 'shared']) const deckConfig = useDeckConfigurationQuery().data ?? [] const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() - const deckConfigurationAtLocationLoadName = deckConfig.find( - (deckFixture: Fixture) => deckFixture.fixtureLocation === cutout - )?.loadName + const deckConfigurationAtLocationFixtureId = deckConfig.find( + (deckFixture: CutoutConfig) => deckFixture.cutoutId === cutoutId + )?.cutoutFixtureId const currentFixtureDisplayName = - deckConfigurationAtLocationLoadName != null - ? getFixtureDisplayName(deckConfigurationAtLocationLoadName) + deckConfigurationAtLocationFixtureId != null + ? getFixtureDisplayName(deckConfigurationAtLocationFixtureId) : '' const handleUpdateDeck = (): void => { - if (requiredFixture != null) { - updateDeckConfiguration({ - fixtureLocation: cutout, - loadName: requiredFixture, - }) + if (requiredFixtureId != null) { + const newRequiredFixtureDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: requiredFixtureId } + : fixture + ) + + updateDeckConfiguration(newRequiredFixtureDeckConfig) } else { - updateDeckConfiguration({ - fixtureLocation: cutout, - loadName: STANDARD_SLOT_LOAD_NAME, - }) + const isRightCutout = SINGLE_RIGHT_CUTOUTS.includes(cutoutId) + const singleSlotFixture = isRightCutout + ? SINGLE_RIGHT_SLOT_FIXTURE + : SINGLE_LEFT_SLOT_FIXTURE + + const newSingleSlotDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: singleSlotFixture } + : fixture + ) + + updateDeckConfiguration(newSingleSlotDeckConfig) } onCloseClick() } @@ -102,7 +115,7 @@ export const LocationConflictModal = ( i18nKey="deck_conflict_info" values={{ currentFixture: currentFixtureDisplayName, - cutout: getCutoutDisplayName(cutout), + cutout: getCutoutDisplayName(cutoutId), }} components={{ block: , @@ -115,7 +128,9 @@ export const LocationConflictModal = ( fontWeight={TYPOGRAPHY.fontWeightBold} paddingBottom={SPACING.spacing8} > - {t('slot_location', { slotName: getCutoutDisplayName(cutout) })} + {t('slot_location', { + slotName: getCutoutDisplayName(cutoutId), + })} - {requiredFixture != null && - getFixtureDisplayName(requiredFixture)} + {requiredFixtureId != null && + getFixtureDisplayName(requiredFixtureId)} {requiredModule != null && getModuleDisplayName(requiredModule)} @@ -199,7 +214,7 @@ export const LocationConflictModal = ( i18nKey="deck_conflict_info" values={{ currentFixture: currentFixtureDisplayName, - cutout: getCutoutDisplayName(cutout), + cutout: getCutoutDisplayName(cutoutId), }} components={{ block: , @@ -211,7 +226,9 @@ export const LocationConflictModal = ( fontSize={TYPOGRAPHY.fontSizeH4} fontWeight={TYPOGRAPHY.fontWeightBold} > - {t('slot_location', { slotName: getCutoutDisplayName(cutout) })} + {t('slot_location', { + slotName: getCutoutDisplayName(cutoutId), + })} - {requiredFixture != null && - getFixtureDisplayName(requiredFixture)} + {requiredFixtureId != null && + getFixtureDisplayName(requiredFixtureId)} {requiredModule != null && getModuleDisplayName(requiredModule)} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx index c568ef1fe95..dbed3152c1a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx @@ -1,6 +1,9 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client/src/deck_configuration' +import { + useDeckConfigurationQuery, + useUpdateDeckConfigurationMutation, +} from '@opentrons/react-api-client/src/deck_configuration' import { Flex, DIRECTION_COLUMN, @@ -17,26 +20,30 @@ import { Portal } from '../../../../App/portal' import { LegacyModal } from '../../../../molecules/LegacyModal' import { StyledText } from '../../../../atoms/text' -import type { Cutout, FixtureLoadName } from '@opentrons/shared-data' +import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' interface NotConfiguredModalProps { onCloseClick: () => void - requiredFixture: FixtureLoadName - cutout: Cutout + requiredFixtureId: CutoutFixtureId + cutoutId: CutoutId } export const NotConfiguredModal = ( props: NotConfiguredModalProps ): JSX.Element => { - const { onCloseClick, cutout, requiredFixture } = props + const { onCloseClick, cutoutId, requiredFixtureId } = props const { t, i18n } = useTranslation(['protocol_setup', 'shared']) const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() + const deckConfig = useDeckConfigurationQuery()?.data ?? [] const handleUpdateDeck = (): void => { - updateDeckConfiguration({ - fixtureLocation: cutout, - loadName: requiredFixture, - }) + const newDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: requiredFixtureId } + : fixture + ) + + updateDeckConfiguration(newDeckConfig) onCloseClick() } @@ -46,7 +53,7 @@ export const NotConfiguredModal = ( title={ {t('add_fixture', { - fixtureName: getFixtureDisplayName(requiredFixture), + fixtureName: getFixtureDisplayName(requiredFixtureId), })} } @@ -64,7 +71,7 @@ export const NotConfiguredModal = ( justifyContent={JUSTIFY_SPACE_BETWEEN} > - {getFixtureDisplayName(requiredFixture)} + {getFixtureDisplayName(requiredFixtureId)} {i18n.format(t('add'), 'capitalize')} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx index 4817fc8ca09..80f720c51fe 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import map from 'lodash/map' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { @@ -16,16 +15,10 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { - FixtureLoadName, + SINGLE_SLOT_FIXTURES, + getCutoutDisplayName, getFixtureDisplayName, - LoadFixtureRunTimeCommand, } from '@opentrons/shared-data' -import { - useLoadedFixturesConfigStatus, - CONFIGURED, - CONFLICTING, - NOT_CONFIGURED, -} from '../../../../resources/deck_configuration/hooks' import { StyledText } from '../../../../atoms/text' import { StatusLabel } from '../../../../atoms/StatusLabel' import { TertiaryButton } from '../../../../atoms/buttons/TertiaryButton' @@ -33,15 +26,14 @@ import { LocationConflictModal } from './LocationConflictModal' import { NotConfiguredModal } from './NotConfiguredModal' import { getFixtureImage } from './utils' -import type { LoadedFixturesBySlot } from '@opentrons/api-client' -import type { Cutout } from '@opentrons/shared-data' +import type { CutoutConfigAndCompatibility } from '../../../../resources/deck_configuration/hooks' interface SetupFixtureListProps { - loadedFixturesBySlot: LoadedFixturesBySlot + deckConfigCompatibility: CutoutConfigAndCompatibility[] } export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { - const { loadedFixturesBySlot } = props + const { deckConfigCompatibility } = props const { t, i18n } = useTranslation('protocol_setup') return ( <> @@ -81,15 +73,11 @@ export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { gridGap={SPACING.spacing4} marginBottom={SPACING.spacing24} > - {map(loadedFixturesBySlot, ({ params, id }) => { - const { loadName, location } = params + {deckConfigCompatibility.map(cutoutConfigAndCompatibility => { return ( ) })} @@ -98,54 +86,43 @@ export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { ) } -interface FixtureListItemProps { - loadedFixtures: LoadFixtureRunTimeCommand[] - loadName: FixtureLoadName - cutout: Cutout - commandId: string -} +interface FixtureListItemProps extends CutoutConfigAndCompatibility {} export function FixtureListItem({ - loadedFixtures, - loadName, - cutout, - commandId, + cutoutId, + cutoutFixtureId, + compatibleCutoutFixtureIds, }: FixtureListItemProps): JSX.Element { const { t } = useTranslation('protocol_setup') - const configuration = useLoadedFixturesConfigStatus(loadedFixtures) - const configurationStatus = configuration.find( - config => config.id === commandId - )?.configurationStatus + const isCurrentFixtureCompatible = + cutoutFixtureId != null && + compatibleCutoutFixtureIds.includes(cutoutFixtureId) + const isConflictingFixtureConfigured = + cutoutFixtureId != null && !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) let statusLabel - if ( - configurationStatus === CONFLICTING || - configurationStatus === NOT_CONFIGURED - ) { + if (!isCurrentFixtureCompatible) { statusLabel = ( ) - } else if (configurationStatus === CONFIGURED) { + } else { statusLabel = ( ) - // shouldn't run into this case - } else { - statusLabel = 'status label unknown' } const [ @@ -159,18 +136,18 @@ export function FixtureListItem({ return ( <> - {showNotConfiguredModal ? ( + {showNotConfiguredModal && cutoutFixtureId != null ? ( setShowNotConfiguredModal(false)} - cutout={cutout} - requiredFixture={loadName} + cutoutId={cutoutId} + requiredFixtureId={compatibleCutoutFixtureIds[0]} /> ) : null} - {showLocationConflictModal ? ( + {showLocationConflictModal && cutoutFixtureId != null ? ( setShowLocationConflictModal(false)} - cutout={cutout} - requiredFixture={loadName} + cutoutId={cutoutId} + requiredFixtureId={compatibleCutoutFixtureIds[0]} /> ) : null} - + {cutoutFixtureId != null ? ( + + ) : null} - {getFixtureDisplayName(loadName)} + {getFixtureDisplayName(cutoutFixtureId)} - {cutout} + {getCutoutDisplayName(cutoutId)} {statusLabel} - {configurationStatus !== CONFIGURED ? ( + {!isCurrentFixtureCompatible ? ( - configurationStatus === CONFLICTING + isConflictingFixtureConfigured ? setShowLocationConflictModal(true) : setShowNotConfiguredModal(true) } diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx index 0f52f417742..df50b6bf721 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx @@ -20,6 +20,8 @@ import { TOOLTIP_LEFT, } from '@opentrons/components' import { + FLEX_ROBOT_TYPE, + getDeckDefFromRobotType, getModuleType, HEATERSHAKER_MODULE_TYPE, HEATERSHAKER_MODULE_V1, @@ -33,6 +35,7 @@ import { TertiaryButton } from '../../../../atoms/buttons' import { StatusLabel } from '../../../../atoms/StatusLabel' import { StyledText } from '../../../../atoms/text' import { Tooltip } from '../../../../atoms/Tooltip' +import { getCutoutIdForSlotName } from '../../../../resources/deck_configuration/utils' import { useChainLiveCommands } from '../../../../resources/runs/hooks' import { ModuleSetupModal } from '../../../ModuleCard/ModuleSetupModal' import { ModuleWizardFlows } from '../../../ModuleWizardFlows' @@ -42,6 +45,7 @@ import { ModuleRenderInfoForProtocol, useIsFlex, useModuleRenderInfoForProtocolById, + useRobot, useUnmatchedModulesForProtocol, useRunCalibrationStatus, } from '../../hooks' @@ -50,7 +54,11 @@ import { MultipleModulesModal } from './MultipleModulesModal' import { UnMatchedModuleWarning } from './UnMatchedModuleWarning' import { getModuleImage } from './utils' -import type { Cutout, ModuleModel, Fixture } from '@opentrons/shared-data' +import type { + CutoutConfig, + DeckDefinition, + ModuleModel, +} from '@opentrons/shared-data' import type { AttachedModule } from '../../../../redux/modules/types' import type { ProtocolCalibrationStatus } from '../../hooks' @@ -72,6 +80,8 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { } = useUnmatchedModulesForProtocol(robotName, runId) const isFlex = useIsFlex(robotName) + const { robotModel } = useRobot(robotName) ?? {} + const deckDef = getDeckDefFromRobotType(robotModel ?? FLEX_ROBOT_TYPE) const calibrationStatus = useRunCalibrationStatus(robotName, runId) @@ -188,6 +198,7 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { isFlex={isFlex} calibrationStatus={calibrationStatus} conflictedFixture={conflictedFixture} + deckDef={deckDef} /> ) } @@ -205,7 +216,8 @@ interface ModulesListItemProps { heaterShakerModuleFromProtocol: ModuleRenderInfoForProtocol | null isFlex: boolean calibrationStatus: ProtocolCalibrationStatus - conflictedFixture?: Fixture + deckDef: DeckDefinition + conflictedFixture?: CutoutConfig } export function ModulesListItem({ @@ -217,6 +229,7 @@ export function ModulesListItem({ isFlex, calibrationStatus, conflictedFixture, + deckDef, }: ModulesListItemProps): JSX.Element { const { t } = useTranslation(['protocol_setup', 'module_wizard_flows']) const moduleConnectionStatus = @@ -346,13 +359,16 @@ export function ModulesListItem({ ) } + // convert slot name to cutout id + const cutoutIdForSlotName = getCutoutIdForSlotName(slotName, deckDef) + return ( <> - {showLocationConflictModal ? ( + {showLocationConflictModal && cutoutIdForSlotName != null ? ( setShowLocationConflictModal(false)} // TODO(bh, 2023-10-10): when module caddies are fixtures, narrow slotName to Cutout and remove type assertion - cutout={slotName as Cutout} + cutoutId={cutoutIdForSlotName} requiredModule={moduleModel} /> ) : null} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx index 6d24f7b6eee..d441a602e2a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx @@ -12,7 +12,7 @@ import { getRobotTypeFromLoadedLabware, } from '@opentrons/shared-data' -import { getDeckConfigFromProtocolCommands } from '../../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils' import { ModuleInfo } from '../../ModuleInfo' @@ -64,7 +64,7 @@ export const SetupModulesMap = ({ ), })) - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) @@ -77,7 +77,10 @@ export const SetupModulesMap = ({ > ({ + cutoutId, + cutoutFixtureId, + }))} deckLayerBlocklist={getStandardDeckViewLayerBlockList(robotType)} robotType={robotType} labwareLocations={[]} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx index a131b3c314a..06d2e1507c7 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx @@ -2,10 +2,8 @@ import * as React from 'react' import { UseQueryResult } from 'react-query' import { renderWithProviders } from '@opentrons/components' import { - DeckConfiguration, - STAGING_AREA_LOAD_NAME, - Fixture, - TRASH_BIN_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, } from '@opentrons/shared-data' import { useDeckConfigurationQuery, @@ -14,6 +12,8 @@ import { import { i18n } from '../../../../../i18n' import { LocationConflictModal } from '../LocationConflictModal' +import type { DeckConfiguration } from '@opentrons/shared-data' + jest.mock('@opentrons/react-api-client/src/deck_configuration') const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction< @@ -24,10 +24,9 @@ const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutatio > const mockFixture = { - fixtureId: 'mockId', - fixtureLocation: 'cutoutB3', - loadName: STAGING_AREA_LOAD_NAME, -} as Fixture + cutoutId: 'cutoutB3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, +} const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -41,8 +40,8 @@ describe('LocationConflictModal', () => { beforeEach(() => { props = { onCloseClick: jest.fn(), - cutout: 'cutoutB3', - requiredFixture: TRASH_BIN_LOAD_NAME, + cutoutId: 'cutoutB3', + requiredFixtureId: TRASH_BIN_ADAPTER_FIXTURE, } mockUseDeckConfigurationQuery.mockReturnValue({ data: [mockFixture], @@ -57,8 +56,8 @@ describe('LocationConflictModal', () => { getByText('Slot B3') getByText('Protocol specifies') getByText('Currently configured') - getAllByText('Staging Area Slot') - getByText('Trash Bin') + getAllByText('Staging area slot') + getByText('Trash bin') getByRole('button', { name: 'Cancel' }).click() expect(props.onCloseClick).toHaveBeenCalled() getByRole('button', { name: 'Update deck' }).click() @@ -67,7 +66,7 @@ describe('LocationConflictModal', () => { it('should render the modal information for a module fixture conflict', () => { props = { onCloseClick: jest.fn(), - cutout: 'cutoutB3', + cutoutId: 'cutoutB3', requiredModule: 'heaterShakerModuleV1', } const { getByText, getByRole } = render(props) @@ -89,8 +88,8 @@ describe('LocationConflictModal', () => { getByText('Slot B3') getByText('Protocol specifies') getByText('Currently configured') - getAllByText('Staging Area Slot') - getByText('Trash Bin') + getAllByText('Staging area slot') + getByText('Trash bin') getByText('Cancel').click() expect(props.onCloseClick).toHaveBeenCalled() getByText('Confirm removal').click() diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx index 6a000013101..f2d5fb5ca38 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx @@ -1,15 +1,24 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' -import { TRASH_BIN_LOAD_NAME } from '@opentrons/shared-data' -import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client/src/deck_configuration' +import { TRASH_BIN_ADAPTER_FIXTURE } from '@opentrons/shared-data' +import { + useDeckConfigurationQuery, + useUpdateDeckConfigurationMutation, +} from '@opentrons/react-api-client/src/deck_configuration' import { i18n } from '../../../../../i18n' import { NotConfiguredModal } from '../NotConfiguredModal' +import type { UseQueryResult } from 'react-query' +import type { DeckConfiguration } from '@opentrons/shared-data' + jest.mock('@opentrons/react-api-client/src/deck_configuration') const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction< typeof useUpdateDeckConfigurationMutation > +const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction< + typeof useDeckConfigurationQuery +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -23,20 +32,23 @@ describe('NotConfiguredModal', () => { beforeEach(() => { props = { onCloseClick: jest.fn(), - cutout: 'cutoutB3', - requiredFixture: TRASH_BIN_LOAD_NAME, + cutoutId: 'cutoutB3', + requiredFixtureId: TRASH_BIN_ADAPTER_FIXTURE, } mockUseUpdateDeckConfigurationMutation.mockReturnValue({ updateDeckConfiguration: mockUpdate, } as any) + mockUseDeckConfigurationQuery.mockReturnValue(({ + data: [], + } as unknown) as UseQueryResult) }) it('renders the correct text and button works as expected', () => { const { getByText, getByRole } = render(props) - getByText('Add Trash Bin to deck configuration') + getByText('Add Trash bin to deck configuration') getByText( 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Trash Bin') + getByText('Trash bin') getByRole('button', { name: 'Add' }).click() expect(mockUpdate).toHaveBeenCalled() }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx index f35e647914b..d40128fffd6 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx @@ -1,47 +1,34 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' -import { - LoadFixtureRunTimeCommand, - WASTE_CHUTE_LOAD_NAME, - WASTE_CHUTE_CUTOUT, -} from '@opentrons/shared-data' +import { STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE } from '@opentrons/shared-data' import { i18n } from '../../../../../i18n' -import { useLoadedFixturesConfigStatus } from '../../../../../resources/deck_configuration/hooks' import { SetupFixtureList } from '../SetupFixtureList' import { NotConfiguredModal } from '../NotConfiguredModal' import { LocationConflictModal } from '../LocationConflictModal' -import type { LoadedFixturesBySlot } from '@opentrons/api-client' + +import type { CutoutConfigAndCompatibility } from '../../../../../resources/deck_configuration/hooks' jest.mock('../../../../../resources/deck_configuration/hooks') jest.mock('../LocationConflictModal') jest.mock('../NotConfiguredModal') -const mockUseLoadedFixturesConfigStatus = useLoadedFixturesConfigStatus as jest.MockedFunction< - typeof useLoadedFixturesConfigStatus -> const mockLocationConflictModal = LocationConflictModal as jest.MockedFunction< typeof LocationConflictModal > const mockNotConfiguredModal = NotConfiguredModal as jest.MockedFunction< typeof NotConfiguredModal > -const mockLoadedFixture = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} as LoadFixtureRunTimeCommand -const mockLoadedFixturesBySlot: LoadedFixturesBySlot = { - D3: mockLoadedFixture, -} +const mockDeckConfigCompatibility: CutoutConfigAndCompatibility[] = [ + { + cutoutId: 'cutoutD3', + cutoutFixtureId: STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + requiredAddressableAreas: ['D4'], + compatibleCutoutFixtureIds: [ + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + ], + }, +] const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -53,14 +40,8 @@ describe('SetupFixtureList', () => { let props: React.ComponentProps beforeEach(() => { props = { - loadedFixturesBySlot: mockLoadedFixturesBySlot, + deckConfigCompatibility: mockDeckConfigCompatibility, } - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { - ...mockLoadedFixture, - configurationStatus: 'configured', - }, - ]) mockLocationConflictModal.mockReturnValue(
    mock location conflict modal
    ) @@ -72,33 +53,22 @@ describe('SetupFixtureList', () => { getByText('Fixture') getByText('Location') getByText('Status') - getByText('Waste Chute') + getByText('Waste chute with staging area slot') getByRole('button', { name: 'View setup instructions' }) - getByText(WASTE_CHUTE_CUTOUT) + getByText('D3') getByText('Configured') }) - it('should render the headers and a fixture with conflicted status', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { - ...mockLoadedFixture, - configurationStatus: 'conflicting', - }, - ]) - const { getByText, getByRole } = render(props)[0] - getByText('Location conflict') - getByRole('button', { name: 'Update deck' }).click() - getByText('mock location conflict modal') - }) - it('should render the headers and a fixture with not configured status and button', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { - ...mockLoadedFixture, - configurationStatus: 'not configured', - }, - ]) - const { getByText, getByRole } = render(props)[0] - getByText('Not configured') - getByRole('button', { name: 'Update deck' }).click() - getByText('mock not configured modal') - }) + // TODO(bh, 2023-11-14): implement test cases when example JSON protocol fixtures exist + // it('should render the headers and a fixture with conflicted status', () => { + // const { getByText, getByRole } = render(props)[0] + // getByText('Location conflict') + // getByRole('button', { name: 'Update deck' }).click() + // getByText('mock location conflict modal') + // }) + // it('should render the headers and a fixture with not configured status and button', () => { + // const { getByText, getByRole } = render(props)[0] + // getByText('Not configured') + // getByRole('button', { name: 'Update deck' }).click() + // getByText('mock not configured modal') + // }) }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx index 4fd6e703904..07797b57c0d 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import { fireEvent } from '@testing-library/react' import { when } from 'jest-when' import { renderWithProviders } from '@opentrons/components' -import { WASTE_CHUTE_LOAD_NAME } from '@opentrons/shared-data' import { i18n } from '../../../../../i18n' import { useIsFlex, @@ -15,23 +14,6 @@ import { SetupModulesList } from '../SetupModulesList' import { SetupModulesMap } from '../SetupModulesMap' import { SetupFixtureList } from '../SetupFixtureList' import { mockTemperatureModule } from '../../../../../redux/modules/__fixtures__' -import { LoadedFixturesBySlot } from '@opentrons/api-client' - -const mockLoadedFixturesBySlot: LoadedFixturesBySlot = { - D3: { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - }, -} jest.mock('../../../hooks') jest.mock('../SetupModulesList') @@ -75,7 +57,7 @@ describe('SetupModuleAndDeck', () => { runId: MOCK_RUN_ID, expandLabwarePositionCheckStep: () => jest.fn(), hasModules: true, - loadedFixturesBySlot: {}, + commands: [], } mockSetupFixtureList.mockReturnValue(
    Mock setup fixture list
    ) mockSetupModulesList.mockReturnValue(
    Mock setup modules list
    ) @@ -141,7 +123,6 @@ describe('SetupModuleAndDeck', () => { it('should render the SetupModulesList and SetupFixtureList component when clicking List View for Flex', () => { when(mockUseIsFlex).calledWith(MOCK_ROBOT_NAME).mockReturnValue(true) - props.loadedFixturesBySlot = mockLoadedFixturesBySlot const { getByRole, getByText } = render(props) const button = getByRole('button', { name: 'List View' }) fireEvent.click(button) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx index 4e2fc01c73c..efe07a3f6b0 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' import { fireEvent, waitFor } from '@testing-library/react' import { renderWithProviders } from '@opentrons/components' +import { STAGING_AREA_RIGHT_SLOT_FIXTURE } from '@opentrons/shared-data' import { i18n } from '../../../../../i18n' import { mockMagneticModule as mockMagneticModuleFixture, @@ -27,11 +28,7 @@ import { UnMatchedModuleWarning } from '../UnMatchedModuleWarning' import { SetupModulesList } from '../SetupModulesList' import { LocationConflictModal } from '../LocationConflictModal' -import { - ModuleModel, - ModuleType, - STAGING_AREA_LOAD_NAME, -} from '@opentrons/shared-data' +import type { ModuleModel, ModuleType } from '@opentrons/shared-data' jest.mock('@opentrons/react-api-client') jest.mock('../../../hooks') @@ -446,7 +443,7 @@ describe('SetupModulesList', () => { fireEvent.click(moduleSetup) getByText('mockModuleSetupModal') }) - it('shoulde render a magnetic block with a conflicted fixture', () => { + it('should render a magnetic block with a conflicted fixture', () => { when(mockUseIsFlex).calledWith(ROBOT_NAME).mockReturnValue(true) mockUseModuleRenderInfoForProtocolById.mockReturnValue({ [mockMagneticBlock.id]: { @@ -463,12 +460,11 @@ describe('SetupModulesList', () => { nestedLabwareDef: null, nestedLabwareId: null, protocolLoadOrder: 0, - slotName: '1', + slotName: 'B3', attachedModuleMatch: null, conflictedFixture: { - fixtureId: 'mockId', - fixtureLocation: '1', - loadName: STAGING_AREA_LOAD_NAME, + cutoutId: 'cutoutB3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, }, }, } as any) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts index 96e1470f78f..2388de2b936 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts @@ -44,15 +44,15 @@ describe('getModuleImage', () => { describe('getFixtureImage', () => { it('should render the staging area image', () => { - const result = getFixtureImage('stagingArea') + const result = getFixtureImage('stagingAreaRightSlot') expect(result).toEqual('staging_area_slot.png') }) it('should render the waste chute image', () => { - const result = getFixtureImage('wasteChute') + const result = getFixtureImage('wasteChuteRightAdapterNoCover') expect(result).toEqual('waste_chute.png') }) it('should render the trash binimage', () => { - const result = getFixtureImage('trashBin') + const result = getFixtureImage('trashBinAdapter') expect(result).toEqual('flex_trash_bin.png') }) // TODO(jr, 10/17/23): add rest of the test cases when we add the assets diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx index 3ef1a8c7603..213b1bc84f5 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx @@ -8,33 +8,37 @@ import { useHoverTooltip, PrimaryButton, } from '@opentrons/components' +import { FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS } from '@opentrons/shared-data' + import { useToggleGroup } from '../../../../molecules/ToggleGroup/useToggleGroup' +import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks' import { Tooltip } from '../../../../atoms/Tooltip' import { - useIsFlex, useRunHasStarted, useUnmatchedModulesForProtocol, useModuleCalibrationStatus, + useRobotType, } from '../../hooks' import { SetupModulesMap } from './SetupModulesMap' import { SetupModulesList } from './SetupModulesList' import { SetupFixtureList } from './SetupFixtureList' -import type { LoadedFixturesBySlot } from '@opentrons/api-client' + +import type { RunTimeCommand } from '@opentrons/shared-data' interface SetupModuleAndDeckProps { expandLabwarePositionCheckStep: () => void robotName: string runId: string - loadedFixturesBySlot: LoadedFixturesBySlot hasModules: boolean + commands: RunTimeCommand[] } export const SetupModuleAndDeck = ({ expandLabwarePositionCheckStep, robotName, runId, - loadedFixturesBySlot, hasModules, + commands, }: SetupModuleAndDeckProps): JSX.Element => { const { t } = useTranslation('protocol_setup') const [selectedValue, toggleGroup] = useToggleGroup( @@ -42,12 +46,28 @@ export const SetupModuleAndDeck = ({ t('map_view') ) - const isFlex = useIsFlex(robotName) + const robotType = useRobotType(robotName) const { missingModuleIds } = useUnmatchedModulesForProtocol(robotName, runId) const runHasStarted = useRunHasStarted(runId) const [targetProps, tooltipProps] = useHoverTooltip() const moduleCalibrationStatus = useModuleCalibrationStatus(robotName, runId) + const deckConfigCompatibility = useDeckConfigurationCompatibility( + robotType, + commands + ) + + const nonSingleSlotDeckConfigCompatibility = deckConfigCompatibility.filter( + ({ requiredAddressableAreas }) => + // required AA list includes a non-single-slot AA + !requiredAddressableAreas.every(aa => + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(aa) + ) + ) + // fixture includes at least 1 required AA + const requiredDeckConfigCompatibility = nonSingleSlotDeckConfigCompatibility.filter( + fixture => fixture.requiredAddressableAreas.length > 0 + ) return ( <> @@ -58,9 +78,9 @@ export const SetupModuleAndDeck = ({ {hasModules ? ( ) : null} - {Object.keys(loadedFixturesBySlot).length > 0 && isFlex ? ( - - ) : null} + ) : ( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts index 171a26199f4..ee5b72efee5 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts @@ -1,3 +1,10 @@ +import { + SINGLE_SLOT_FIXTURES, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_FIXTURES, +} from '@opentrons/shared-data' + import magneticModule from '../../../../assets/images/magnetic_module_gen_2_transparent.png' import temperatureModule from '../../../../assets/images/temp_deck_gen_2_transparent.png' import thermoModuleGen1 from '../../../../assets/images/thermocycler_closed.png' @@ -9,7 +16,13 @@ import stagingArea from '../../../../assets/images/staging_area_slot.png' import wasteChute from '../../../../assets/images/waste_chute.png' // TODO(jr, 10/17/23): figure out if we need this asset, I'm stubbing it in for now // import wasteChuteStagingArea from '../../../../assets/images/waste_chute_with_staging_area.png' -import type { FixtureLoadName, ModuleModel } from '@opentrons/shared-data' + +import type { + CutoutFixtureId, + ModuleModel, + SingleSlotCutoutFixtureId, + WasteChuteCutoutFixtureId, +} from '@opentrons/shared-data' export function getModuleImage(model: ModuleModel): string { switch (model) { @@ -33,21 +46,21 @@ export function getModuleImage(model: ModuleModel): string { } // TODO(jr, 10/4/23): add correct assets for trashBin, standardSlot, wasteChuteAndStagingArea -export function getFixtureImage(fixture: FixtureLoadName): string { - switch (fixture) { - case 'stagingArea': { - return stagingArea - } - case 'wasteChute': { - return wasteChute - } - case 'standardSlot': { - return stagingArea - } - case 'trashBin': { - return trashBin - } - default: - return 'Error: unknown fixture' +export function getFixtureImage(fixture: CutoutFixtureId): string { + if (fixture === STAGING_AREA_RIGHT_SLOT_FIXTURE) { + return stagingArea + } else if ( + WASTE_CHUTE_FIXTURES.includes(fixture as WasteChuteCutoutFixtureId) + ) { + return wasteChute + } else if ( + // TODO(bh, 2023-11-13): this asset probably won't exist + SINGLE_SLOT_FIXTURES.includes(fixture as SingleSlotCutoutFixtureId) + ) { + return stagingArea + } else if (fixture === TRASH_BIN_ADAPTER_FIXTURE) { + return trashBin + } else { + return 'Error: unknown fixture' } } diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 63aafc70d1d..ac542cccca2 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -12,6 +12,7 @@ import withModulesProtocol from '@opentrons/shared-data/protocol/fixtures/4/test import { i18n } from '../../../../i18n' import { mockConnectedRobot } from '../../../../redux/discovery/__fixtures__' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useIsFlex, @@ -39,6 +40,7 @@ jest.mock('../EmptySetupStep') jest.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') jest.mock('@opentrons/shared-data/js/helpers/parseProtocolData') jest.mock('../../../../redux/config') +jest.mock('../../../../resources/deck_configuration/utils') const mockUseIsFlex = useIsFlex as jest.MockedFunction const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction< @@ -78,6 +80,10 @@ const mockSetupLiquids = SetupLiquids as jest.MockedFunction< const mockEmptySetupStep = EmptySetupStep as jest.MockedFunction< typeof EmptySetupStep > +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands +> + const ROBOT_NAME = 'otie' const RUN_ID = '1' const MOCK_ROTOCOL_LIQUID_KEY = { liquids: [] } @@ -140,6 +146,7 @@ describe('ProtocolRunSetup', () => { when(mockSetupModuleAndDeck).mockReturnValue(
    Mock SetupModules
    ) when(mockSetupLiquids).mockReturnValue(
    Mock SetupLiquids
    ) when(mockEmptySetupStep).mockReturnValue(
    Mock EmptySetupStep
    ) + when(mockGetSimplestDeckConfigForProtocolCommands).mockReturnValue([]) }) afterEach(() => { resetAllWhenMocks() diff --git a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx index 17d70702dad..b1e92228117 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx @@ -2,13 +2,7 @@ import { renderHook } from '@testing-library/react-hooks' import { when, resetAllWhenMocks } from 'jest-when' import { UseQueryResult } from 'react-query' -import { - getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, - FLEX_ROBOT_TYPE, - STAGING_AREA_LOAD_NAME, -} from '@opentrons/shared-data' -import standardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json' +import { STAGING_AREA_RIGHT_SLOT_FIXTURE } from '@opentrons/shared-data' import _heaterShakerCommandsWithResultsKey from '@opentrons/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useDeckConfigurationQuery } from '@opentrons/react-api-client/src/deck_configuration' @@ -27,16 +21,14 @@ import { } from '..' import type { + CutoutConfig, DeckConfiguration, - DeckDefinition, - Fixture, ModuleModel, ModuleType, ProtocolAnalysisOutput, } from '@opentrons/shared-data' jest.mock('@opentrons/react-api-client/src/deck_configuration') -jest.mock('@opentrons/shared-data/js/helpers') jest.mock('../../ProtocolRun/utils/getProtocolModulesInfo') jest.mock('../useAttachedModules') jest.mock('../useProtocolDetailsForRun') @@ -49,12 +41,6 @@ const mockGetProtocolModulesInfo = getProtocolModulesInfo as jest.MockedFunction const mockUseAttachedModules = useAttachedModules as jest.MockedFunction< typeof useAttachedModules > -const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction< - typeof getDeckDefFromRobotType -> -const mockGetRobotTypeFromLoadedLabware = getRobotTypeFromLoadedLabware as jest.MockedFunction< - typeof getRobotTypeFromLoadedLabware -> const mockUseStoredProtocolAnalysis = useStoredProtocolAnalysis as jest.MockedFunction< typeof useStoredProtocolAnalysis > @@ -68,7 +54,15 @@ const heaterShakerCommandsWithResultsKey = (_heaterShakerCommandsWithResultsKey const PROTOCOL_DETAILS = { displayName: 'fake protocol', - protocolData: heaterShakerCommandsWithResultsKey, + protocolData: { + ...heaterShakerCommandsWithResultsKey, + labware: [ + { + displayName: 'Trash', + definitionId: 'opentrons/opentrons_1_trash_3200ml_fixed/1', + }, + ], + }, protocolKey: 'fakeProtocolKey', } @@ -132,11 +126,10 @@ const TEMPERATURE_MODULE_INFO = { slotName: 'D1', } -const mockFixture = { - fixtureId: 'mockId', - fixtureLocation: 'cutoutD1', - loadName: STAGING_AREA_LOAD_NAME, -} as Fixture +const mockFixture: CutoutConfig = { + cutoutId: 'cutoutD1', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, +} describe('useModuleRenderInfoForProtocolById hook', () => { beforeEach(() => { @@ -150,22 +143,16 @@ describe('useModuleRenderInfoForProtocolById hook', () => { mockTemperatureModuleGen2, mockThermocycler, ]) - when(mockGetDeckDefFromRobotType) - .calledWith(FLEX_ROBOT_TYPE) - .mockReturnValue((standardDeckDef as unknown) as DeckDefinition) - when(mockGetRobotTypeFromLoadedLabware).mockReturnValue(FLEX_ROBOT_TYPE) when(mockUseStoredProtocolAnalysis) .calledWith('1') .mockReturnValue((PROTOCOL_DETAILS as unknown) as ProtocolAnalysisOutput) when(mockUseMostRecentCompletedAnalysis) .calledWith('1') .mockReturnValue(PROTOCOL_DETAILS.protocolData as any) - when(mockGetProtocolModulesInfo) - .calledWith( - heaterShakerCommandsWithResultsKey, - (standardDeckDef as unknown) as DeckDefinition - ) - .mockReturnValue([TEMPERATURE_MODULE_INFO, MAGNETIC_MODULE_INFO]) + mockGetProtocolModulesInfo.mockReturnValue([ + TEMPERATURE_MODULE_INFO, + MAGNETIC_MODULE_INFO, + ]) }) afterEach(() => { diff --git a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts index 3c5d9404f74..317b6ed9209 100644 --- a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts +++ b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts @@ -1,23 +1,24 @@ import { checkModuleCompatibility, - Fixture, getDeckDefFromRobotType, getRobotTypeFromLoadedLabware, - STANDARD_SLOT_LOAD_NAME, + SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' import { useDeckConfigurationQuery } from '@opentrons/react-api-client/src/deck_configuration' +import { getCutoutIdForSlotName } from '../../../resources/deck_configuration/utils' import { getProtocolModulesInfo } from '../ProtocolRun/utils/getProtocolModulesInfo' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useAttachedModules } from './useAttachedModules' import { useStoredProtocolAnalysis } from './useStoredProtocolAnalysis' +import type { CutoutConfig } from '@opentrons/shared-data' import type { AttachedModule } from '../../../redux/modules/types' import type { ProtocolModuleInfo } from '../ProtocolRun/utils/getProtocolModulesInfo' export interface ModuleRenderInfoForProtocol extends ProtocolModuleInfo { attachedModuleMatch: AttachedModule | null - conflictedFixture?: Fixture + conflictedFixture?: CutoutConfig } export interface ModuleRenderInfoById { @@ -54,26 +55,31 @@ export function useModuleRenderInfoForProtocolById( protocolMod.moduleDef.model ) && !matchedAmod.find(m => m === attachedMod) ) ?? null + + const cutoutIdForSlotName = getCutoutIdForSlotName( + protocolMod.slotName, + deckDef + ) + + const conflictedFixture = deckConfig?.find( + fixture => + fixture.cutoutId === cutoutIdForSlotName && + fixture.cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) + ) + if (compatibleAttachedModule !== null) { matchedAmod = [...matchedAmod, compatibleAttachedModule] return { ...protocolMod, attachedModuleMatch: compatibleAttachedModule, - conflictedFixture: deckConfig?.find( - fixture => - fixture.fixtureLocation === protocolMod.slotName && - fixture.loadName !== STANDARD_SLOT_LOAD_NAME - ), + conflictedFixture, } } return { ...protocolMod, attachedModuleMatch: null, - conflictedFixture: deckConfig?.find( - fixture => - fixture.fixtureLocation === protocolMod.slotName && - fixture.loadName !== STANDARD_SLOT_LOAD_NAME - ), + conflictedFixture, } } ) diff --git a/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx b/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx index 6b87d735279..e4da0e001c7 100644 --- a/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx +++ b/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx @@ -13,10 +13,12 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + getCutoutDisplayName, getFixtureDisplayName, getModuleDisplayName, getModuleType, getPipetteNameSpecs, + SINGLE_SLOT_FIXTURES, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' @@ -28,17 +30,18 @@ import { getSlotsForThermocycler } from './utils' import type { LoadModuleRunTimeCommand, - LoadFixtureRunTimeCommand, PipetteName, RobotType, + SingleSlotCutoutFixtureId, } from '@opentrons/shared-data' +import type { CutoutConfigProtocolSpec } from '../../resources/deck_configuration/utils' interface RobotConfigurationDetailsProps { leftMountPipetteName: PipetteName | null rightMountPipetteName: PipetteName | null extensionInstrumentName: string | null requiredModuleDetails: LoadModuleRunTimeCommand[] - requiredFixtureDetails: LoadFixtureRunTimeCommand[] + requiredFixtureDetails: CutoutConfigProtocolSpec[] isLoading: boolean robotType: RobotType | null } @@ -110,6 +113,14 @@ export const RobotConfigurationDetails = ( emptyText ) + // filter out single slot fixtures + const nonStandardRequiredFixtureDetails = requiredFixtureDetails.filter( + fixture => + !SINGLE_SLOT_FIXTURES.includes( + fixture.cutoutFixtureId as SingleSlotCutoutFixtureId + ) + ) + return ( ) })} - {requiredFixtureDetails.map((fixture, index) => { + {nonStandardRequiredFixtureDetails.map((fixture, index) => { return ( - {getFixtureDisplayName(fixture.params.loadName)} + {getFixtureDisplayName(fixture.cutoutFixtureId)}
    } /> diff --git a/app/src/organisms/ProtocolDetails/index.tsx b/app/src/organisms/ProtocolDetails/index.tsx index 9b017166cb9..66917dcc1bd 100644 --- a/app/src/organisms/ProtocolDetails/index.tsx +++ b/app/src/organisms/ProtocolDetails/index.tsx @@ -37,12 +37,8 @@ import { parseInitialLoadedLabwareBySlot, parseInitialLoadedLabwareByModuleId, parseInitialLoadedLabwareByAdapter, - parseInitialLoadedFixturesByCutout, } from '@opentrons/api-client' -import { - WASTE_CHUTE_LOAD_NAME, - getGripperDisplayName, -} from '@opentrons/shared-data' +import { getGripperDisplayName } from '@opentrons/shared-data' import { Portal } from '../../App/portal' import { Divider } from '../../atoms/structure' @@ -58,6 +54,7 @@ import { analyzeProtocol, } from '../../redux/protocol-storage' import { useFeatureFlag } from '../../redux/config' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { ChooseRobotToRunProtocolSlideout } from '../ChooseRobotToRunProtocolSlideout' import { SendProtocolToOT3Slideout } from '../SendProtocolToOT3Slideout' import { ProtocolAnalysisFailure } from '../ProtocolAnalysisFailure' @@ -72,11 +69,7 @@ import { ProtocolLabwareDetails } from './ProtocolLabwareDetails' import { ProtocolLiquidsDetails } from './ProtocolLiquidsDetails' import { RobotConfigurationDetails } from './RobotConfigurationDetails' -import type { - JsonConfig, - LoadFixtureRunTimeCommand, - PythonConfig, -} from '@opentrons/shared-data' +import type { JsonConfig, PythonConfig } from '@opentrons/shared-data' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State, Dispatch } from '../../redux/types' @@ -237,29 +230,9 @@ export function ProtocolDetails( ? map(parseInitialLoadedModulesBySlot(mostRecentAnalysis.commands)) : [] - // TODO: IMMEDIATELY remove stubbed fixture as soon as PE supports loadFixture - const STUBBED_LOAD_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - } - const requiredFixtureDetails = - mostRecentAnalysis?.commands != null - ? [ - ...map( - parseInitialLoadedFixturesByCutout(mostRecentAnalysis.commands) - ), - STUBBED_LOAD_FIXTURE, - ] - : [] + const requiredFixtureDetails = getSimplestDeckConfigForProtocolCommands( + mostRecentAnalysis?.commands ?? [] + ) const requiredLabwareDetails = mostRecentAnalysis != null diff --git a/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx b/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx index d453c00ded7..b8382ac1ef2 100644 --- a/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx +++ b/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx @@ -1,23 +1,19 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' -import { renderWithProviders, DeckConfigurator } from '@opentrons/components' -import { - useUpdateDeckConfigurationMutation, - useCreateDeckConfigurationMutation, -} from '@opentrons/react-api-client' +import { renderWithProviders, BaseDeck } from '@opentrons/components' +import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { ProtocolSetupDeckConfiguration } from '..' -jest.mock('@opentrons/components/src/hardware-sim/DeckConfigurator/index') +jest.mock('@opentrons/components/src/hardware-sim/BaseDeck/index') jest.mock('@opentrons/react-api-client') jest.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') const mockSetSetupScreen = jest.fn() const mockUpdateDeckConfiguration = jest.fn() -const mockCreateDeckConfiguration = jest.fn() const PROTOCOL_DETAILS = { displayName: 'fake protocol', protocolData: [], @@ -25,18 +21,13 @@ const PROTOCOL_DETAILS = { robotType: 'OT-3 Standard' as const, } -const mockDeckConfigurator = DeckConfigurator as jest.MockedFunction< - typeof DeckConfigurator -> const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction< typeof useMostRecentCompletedAnalysis > const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction< typeof useUpdateDeckConfigurationMutation > -const mockUseCreateDeckConfigurationMutation = useCreateDeckConfigurationMutation as jest.MockedFunction< - typeof useCreateDeckConfigurationMutation -> +const mockBaseDeck = BaseDeck as jest.MockedFunction const render = ( props: React.ComponentProps @@ -51,21 +42,18 @@ describe('ProtocolSetupDeckConfiguration', () => { beforeEach(() => { props = { - fixtureLocation: 'cutoutD3', + cutoutId: 'cutoutD3', runId: 'mockRunId', setSetupScreen: mockSetSetupScreen, providedFixtureOptions: [], } - mockDeckConfigurator.mockReturnValue(
    mock DeckConfigurator
    ) + mockBaseDeck.mockReturnValue(
    mock BaseDeck
    ) when(mockUseMostRecentCompletedAnalysis) .calledWith('mockRunId') .mockReturnValue(PROTOCOL_DETAILS.protocolData as any) mockUseUpdateDeckConfigurationMutation.mockReturnValue({ updateDeckConfiguration: mockUpdateDeckConfiguration, } as any) - mockUseCreateDeckConfigurationMutation.mockReturnValue({ - createDeckConfiguration: mockCreateDeckConfiguration, - } as any) }) afterEach(() => { @@ -75,7 +63,7 @@ describe('ProtocolSetupDeckConfiguration', () => { it('should render text, button, and DeckConfigurator', () => { const [{ getByText }] = render(props) getByText('Deck configuration') - getByText('mock DeckConfigurator') + getByText('mock BaseDeck') getByText('Confirm') }) @@ -88,6 +76,6 @@ describe('ProtocolSetupDeckConfiguration', () => { it('should call a mock function when tapping confirm button', () => { const [{ getByText }] = render(props) getByText('Confirm').click() - expect(mockCreateDeckConfiguration).toHaveBeenCalled() + expect(mockUpdateDeckConfiguration).toHaveBeenCalled() }) }) diff --git a/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx b/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx index bb5638ac06a..435498c53d9 100644 --- a/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx +++ b/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx @@ -2,39 +2,38 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { - DeckConfigurator, + BaseDeck, DIRECTION_COLUMN, Flex, JUSTIFY_CENTER, SPACING, } from '@opentrons/components' -import { useCreateDeckConfigurationMutation } from '@opentrons/react-api-client' -import { WASTE_CHUTE_LOAD_NAME } from '@opentrons/shared-data' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' import { ChildNavigation } from '../ChildNavigation' import { AddFixtureModal } from '../DeviceDetailsDeckConfiguration/AddFixtureModal' import { DeckConfigurationDiscardChangesModal } from '../DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { Portal } from '../../App/portal' import type { - Cutout, + CutoutFixtureId, + CutoutId, DeckConfiguration, - Fixture, - FixtureLoadName, - LoadFixtureRunTimeCommand, } from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/OnDeviceDisplay/ProtocolSetup' interface ProtocolSetupDeckConfigurationProps { - fixtureLocation: Cutout + cutoutId: CutoutId | null runId: string setSetupScreen: React.Dispatch> - providedFixtureOptions: FixtureLoadName[] + providedFixtureOptions: CutoutFixtureId[] } export function ProtocolSetupDeckConfiguration({ - fixtureLocation, + cutoutId, runId, setSetupScreen, providedFixtureOptions, @@ -51,46 +50,19 @@ export function ProtocolSetupDeckConfiguration({ ] = React.useState(false) const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const STUBBED_LOAD_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - } - - const requiredFixtureDetails = - mostRecentAnalysis?.commands != null - ? [ - // parseInitialLoadedFixturesByCutout(mostRecentAnalysis.commands), - STUBBED_LOAD_FIXTURE, - ] - : [] - const deckConfig = - (requiredFixtureDetails.map( - (fixture): Fixture | false => - fixture.params.fixtureId != null && { - fixtureId: fixture.params.fixtureId, - fixtureLocation: fixture.params.location.cutout, - loadName: fixture.params.loadName, - } - ) as DeckConfiguration) ?? [] + const simplestDeckConfig = getSimplestDeckConfigForProtocolCommands( + mostRecentAnalysis?.commands ?? [] + ).map(({ cutoutId, cutoutFixtureId }) => ({ cutoutId, cutoutFixtureId })) const [ currentDeckConfig, setCurrentDeckConfig, - ] = React.useState(deckConfig) + ] = React.useState(simplestDeckConfig) - const { createDeckConfiguration } = useCreateDeckConfigurationMutation() + const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const handleClickConfirm = (): void => { - createDeckConfiguration(currentDeckConfig) + updateDeckConfiguration(currentDeckConfig) setSetupScreen('modules') } @@ -102,9 +74,9 @@ export function ProtocolSetupDeckConfiguration({ setShowConfirmationModal={setShowDiscardChangeModal} /> ) : null} - {showConfigurationModal && fixtureLocation != null ? ( + {showConfigurationModal && cutoutId != null ? ( - {/* DeckConfigurator will be replaced by BaseDeck when RAUT-793 is ready */} - {}} - handleClickRemove={() => {}} + diff --git a/app/src/organisms/ProtocolSetupLabware/LabwareMapViewModal.tsx b/app/src/organisms/ProtocolSetupLabware/LabwareMapViewModal.tsx index b56629a6876..13bfb49a3b2 100644 --- a/app/src/organisms/ProtocolSetupLabware/LabwareMapViewModal.tsx +++ b/app/src/organisms/ProtocolSetupLabware/LabwareMapViewModal.tsx @@ -5,7 +5,7 @@ import { BaseDeck } from '@opentrons/components' import { FLEX_ROBOT_TYPE, THERMOCYCLER_MODULE_V1 } from '@opentrons/shared-data' import { Modal } from '../../molecules/Modal' -import { getDeckConfigFromProtocolCommands } from '../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { getStandardDeckViewLayerBlockList } from '../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' import { getLabwareRenderInfo } from '../Devices/ProtocolRun/utils/getLabwareRenderInfo' import { AttachedProtocolModuleMatch } from '../ProtocolSetupModulesAndDeck/utils' @@ -42,7 +42,7 @@ export function LabwareMapViewModal( mostRecentAnalysis, } = props const { t } = useTranslation('protocol_setup') - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( mostRecentAnalysis?.commands ?? [] ) const labwareRenderInfo = diff --git a/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx b/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx index 5c9bf19d6ed..5b7d63a5277 100644 --- a/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx +++ b/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx @@ -10,7 +10,7 @@ import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import deckDefFixture from '@opentrons/shared-data/deck/fixtures/3/deckExample.json' import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' import { i18n } from '../../../i18n' -import { getDeckConfigFromProtocolCommands } from '../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../resources/deck_configuration/utils' import { getLabwareRenderInfo } from '../../Devices/ProtocolRun/utils/getLabwareRenderInfo' import { getStandardDeckViewLayerBlockList } from '../../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' import { mockProtocolModuleInfo } from '../__fixtures__' @@ -32,8 +32,8 @@ jest.mock('../../../redux/config') const mockGetLabwareRenderInfo = getLabwareRenderInfo as jest.MockedFunction< typeof getLabwareRenderInfo > -const mockGetDeckConfigFromProtocolCommands = getDeckConfigFromProtocolCommands as jest.MockedFunction< - typeof getDeckConfigFromProtocolCommands +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands > const mockBaseDeck = BaseDeck as jest.MockedFunction @@ -53,7 +53,7 @@ const render = (props: React.ComponentProps) => { describe('LabwareMapViewModal', () => { beforeEach(() => { mockGetLabwareRenderInfo.mockReturnValue({}) - mockGetDeckConfigFromProtocolCommands.mockReturnValue([]) + mockGetSimplestDeckConfigForProtocolCommands.mockReturnValue([]) }) afterEach(() => { diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx index ef77aae7f2a..26287055b76 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx @@ -14,73 +14,68 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS, getCutoutDisplayName, getFixtureDisplayName, - WASTE_CHUTE_LOAD_NAME, + SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' -// import { parseInitialLoadedFixturesByCutout } from '@opentrons/api-client' -import { - CONFIGURED, - CONFLICTING, - NOT_CONFIGURED, - useLoadedFixturesConfigStatus, -} from '../../resources/deck_configuration/hooks' +import { useDeckConfigurationCompatibility } from '../../resources/deck_configuration/hooks' import { LocationConflictModal } from '../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' import { StyledText } from '../../atoms/text' import { Chip } from '../../atoms/Chip' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import type { CompletedProtocolAnalysis, - Cutout, - FixtureLoadName, - LoadFixtureRunTimeCommand, + CutoutFixtureId, + CutoutId, + RobotType, } from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/OnDeviceDisplay/ProtocolSetup' interface FixtureTableProps { + robotType: RobotType mostRecentAnalysis: CompletedProtocolAnalysis | null setSetupScreen: React.Dispatch> - setFixtureLocation: (fixtureLocation: Cutout) => void - setProvidedFixtureOptions: (providedFixtureOptions: FixtureLoadName[]) => void + setCutoutId: (cutoutId: CutoutId) => void + setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void } export function FixtureTable({ + robotType, mostRecentAnalysis, setSetupScreen, - setFixtureLocation, + setCutoutId, setProvidedFixtureOptions, -}: FixtureTableProps): JSX.Element { +}: FixtureTableProps): JSX.Element | null { const { t, i18n } = useTranslation('protocol_setup') - const STUBBED_LOAD_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - } const [ showLocationConflictModal, setShowLocationConflictModal, ] = React.useState(false) - const requiredFixtureDetails = - mostRecentAnalysis?.commands != null - ? [ - // parseInitialLoadedFixturesByCutout(mostRecentAnalysis.commands), - STUBBED_LOAD_FIXTURE, - ] - : [] + const requiredFixtureDetails = getSimplestDeckConfigForProtocolCommands( + mostRecentAnalysis?.commands ?? [] + ) + const deckConfigCompatibility = useDeckConfigurationCompatibility( + robotType, + mostRecentAnalysis?.commands ?? [] + ) - const configurations = useLoadedFixturesConfigStatus(requiredFixtureDetails) + const nonSingleSlotDeckConfigCompatibility = deckConfigCompatibility.filter( + ({ requiredAddressableAreas }) => + // required AA list includes a non-single-slot AA + !requiredAddressableAreas.every(aa => + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(aa) + ) + ) + // fixture includes at least 1 required AA + const requiredDeckConfigCompatibility = nonSingleSlotDeckConfigCompatibility.filter( + fixture => fixture.requiredAddressableAreas.length > 0 + ) - return ( + return requiredDeckConfigCompatibility.length > 0 ? ( {t('location')} {t('status')} - {requiredFixtureDetails.map((fixture, index) => { - const configurationStatus = configurations.find( - configuration => configuration.id === fixture.id - )?.configurationStatus - - const statusNotReady = - configurationStatus === CONFLICTING || - configurationStatus === NOT_CONFIGURED + {requiredDeckConfigCompatibility.map( + ({ cutoutId, cutoutFixtureId, compatibleCutoutFixtureIds }, index) => { + const isCurrentFixtureCompatible = + cutoutFixtureId != null && + compatibleCutoutFixtureIds.includes(cutoutFixtureId) - let chipLabel: JSX.Element - let handleClick - if (statusNotReady) { - chipLabel = ( - <> - - - - ) - handleClick = - configurationStatus === CONFLICTING + let chipLabel: JSX.Element + let handleClick + if (!isCurrentFixtureCompatible) { + const isConflictingFixtureConfigured = + cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + chipLabel = ( + <> + + + + ) + handleClick = isConflictingFixtureConfigured ? () => setShowLocationConflictModal(true) : () => { - setFixtureLocation(fixture.params.location.cutout) - setProvidedFixtureOptions([fixture.params.loadName]) + setCutoutId(cutoutId) + setProvidedFixtureOptions(compatibleCutoutFixtureIds) setSetupScreen('deck configuration') } - } else if (configurationStatus === CONFIGURED) { - chipLabel = ( - - ) - // TODO(jr, 10/17/23): wire this up - // handleClick = () => setShowNotConfiguredModal(true) - - // shouldn't run into this case - } else { - chipLabel =
    status label unknown
    - } - - return ( - - {showLocationConflictModal ? ( - setShowLocationConflictModal(false)} - cutout={fixture.params.location.cutout} - requiredFixture={fixture.params.loadName} - isOnDevice={true} + } else { + chipLabel = ( + - ) : null} - - - - {getFixtureDisplayName(fixture.params.loadName)} - - - - + {showLocationConflictModal ? ( + setShowLocationConflictModal(false)} + cutoutId={cutoutId} + requiredFixtureId={compatibleCutoutFixtureIds[0]} + isOnDevice={true} /> - + ) : null} - {chipLabel} + + + {cutoutFixtureId != null + ? getFixtureDisplayName(cutoutFixtureId) + : null} + + + + + + + {chipLabel} + - - - ) - })} + + ) + } + )}
    - ) + ) : null } diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapViewModal.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapViewModal.tsx index 5045795d771..d432f9d8bba 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapViewModal.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapViewModal.tsx @@ -6,7 +6,7 @@ import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { Modal } from '../../molecules/Modal' import { ModuleInfo } from '../Devices/ModuleInfo' -import { getDeckConfigFromProtocolCommands } from '../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { getStandardDeckViewLayerBlockList } from '../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' @@ -36,7 +36,7 @@ export function ModulesAndDeckMapViewModal({ if (protocolAnalysis == null) return null - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx index 4d7e18cbcea..c24bbbe2537 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx @@ -1,55 +1,27 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' import { - STAGING_AREA_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + FLEX_ROBOT_TYPE, + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, } from '@opentrons/shared-data' import { i18n } from '../../../i18n' -import { useLoadedFixturesConfigStatus } from '../../../resources/deck_configuration/hooks' +import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks' import { LocationConflictModal } from '../../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' import { FixtureTable } from '../FixtureTable' -import type { LoadFixtureRunTimeCommand } from '@opentrons/shared-data' jest.mock('../../../resources/deck_configuration/hooks') jest.mock('../../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal') -const mockUseLoadedFixturesConfigStatus = useLoadedFixturesConfigStatus as jest.MockedFunction< - typeof useLoadedFixturesConfigStatus -> const mockLocationConflictModal = LocationConflictModal as jest.MockedFunction< typeof LocationConflictModal > -const mockLoadedFixture = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} as LoadFixtureRunTimeCommand - -const mockLoadedStagingAreaFixture = { - id: 'stubbed_load_fixture_2', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: STAGING_AREA_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} as LoadFixtureRunTimeCommand +const mockUseDeckConfigurationCompatibility = useDeckConfigurationCompatibility as jest.MockedFunction< + typeof useDeckConfigurationCompatibility +> const mockSetSetupScreen = jest.fn() -const mockSetFixtureLocation = jest.fn() +const mockSetCutoutId = jest.fn() const mockSetProvidedFixtureOptions = jest.fn() const render = (props: React.ComponentProps) => { @@ -63,16 +35,24 @@ describe('FixtureTable', () => { beforeEach(() => { props = { mostRecentAnalysis: [] as any, + robotType: FLEX_ROBOT_TYPE, setSetupScreen: mockSetSetupScreen, - setFixtureLocation: mockSetFixtureLocation, + setCutoutId: mockSetCutoutId, setProvidedFixtureOptions: mockSetProvidedFixtureOptions, } - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { ...mockLoadedFixture, configurationStatus: 'configured' }, - ]) mockLocationConflictModal.mockReturnValue(
    mock location conflict modal
    ) + mockUseDeckConfigurationCompatibility.mockReturnValue([ + { + cutoutId: 'cutoutD3', + cutoutFixtureId: STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + requiredAddressableAreas: ['D4'], + compatibleCutoutFixtureIds: [ + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + ], + }, + ]) }) it('should render table header and contents', () => { @@ -84,46 +64,39 @@ describe('FixtureTable', () => { it('should render the current status - configured', () => { props = { ...props, - mostRecentAnalysis: { commands: [mockLoadedFixture] } as any, + // TODO(bh, 2023-11-13): mock load labware etc commands + mostRecentAnalysis: { commands: [] } as any, } const [{ getByText }] = render(props) getByText('Configured') }) - it('should render the current status - not configured', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { ...mockLoadedFixture, configurationStatus: 'not configured' }, - ]) - props = { - ...props, - mostRecentAnalysis: { commands: [mockLoadedStagingAreaFixture] } as any, - } - const [{ getByText }] = render(props) - getByText('Not configured') - }) - it('should render the current status - conflicting', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { ...mockLoadedFixture, configurationStatus: 'conflicting' }, - ]) - props = { - ...props, - mostRecentAnalysis: { commands: [mockLoadedStagingAreaFixture] } as any, - } - const [{ getByText, getAllByText }] = render(props) - getByText('Location conflict').click() - getAllByText('mock location conflict modal') - }) - it('should call a mock function when tapping not configured row', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { ...mockLoadedFixture, configurationStatus: 'not configured' }, - ]) - props = { - ...props, - mostRecentAnalysis: { commands: [mockLoadedStagingAreaFixture] } as any, - } - const [{ getByText }] = render(props) - getByText('Not configured').click() - expect(mockSetFixtureLocation).toHaveBeenCalledWith('cutoutD3') - expect(mockSetSetupScreen).toHaveBeenCalledWith('deck configuration') - expect(mockSetProvidedFixtureOptions).toHaveBeenCalledWith(['wasteChute']) - }) + // TODO(bh, 2023-11-14): implement test cases when example JSON protocol fixtures exist + // it('should render the current status - not configured', () => { + // props = { + // ...props, + // mostRecentAnalysis: { commands: [] } as any, + // } + // const [{ getByText }] = render(props) + // getByText('Not configured') + // }) + // it('should render the current status - conflicting', () => { + // props = { + // ...props, + // mostRecentAnalysis: { commands: [] } as any, + // } + // const [{ getByText, getAllByText }] = render(props) + // getByText('Location conflict').click() + // getAllByText('mock location conflict modal') + // }) + // it('should call a mock function when tapping not configured row', () => { + // props = { + // ...props, + // mostRecentAnalysis: { commands: [] } as any, + // } + // const [{ getByText }] = render(props) + // getByText('Not configured').click() + // expect(mockSetCutoutId).toHaveBeenCalledWith('cutoutD3') + // expect(mockSetSetupScreen).toHaveBeenCalledWith('deck configuration') + // expect(mockSetProvidedFixtureOptions).toHaveBeenCalledWith(['wasteChute']) + // }) }) diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapViewModal.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapViewModal.test.tsx index 1882f9947cc..343a692a4f4 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapViewModal.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapViewModal.test.tsx @@ -1,14 +1,10 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' -import { - renderWithProviders, - BaseDeck, - EXTENDED_DECK_CONFIG_FIXTURE, -} from '@opentrons/components' +import { renderWithProviders, BaseDeck } from '@opentrons/components' import { i18n } from '../../../i18n' -import { getDeckConfigFromProtocolCommands } from '../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../resources/deck_configuration/utils' import { ModulesAndDeckMapViewModal } from '../ModulesAndDeckMapViewModal' jest.mock('@opentrons/components/src/hardware-sim/BaseDeck') @@ -92,8 +88,8 @@ const render = ( } const mockBaseDeck = BaseDeck as jest.MockedFunction -const mockGetDeckConfigFromProtocolCommands = getDeckConfigFromProtocolCommands as jest.MockedFunction< - typeof getDeckConfigFromProtocolCommands +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands > describe('ModulesAndDeckMapViewModal', () => { @@ -106,8 +102,9 @@ describe('ModulesAndDeckMapViewModal', () => { runId: mockRunId, protocolAnalysis: PROTOCOL_ANALYSIS, } - when(mockGetDeckConfigFromProtocolCommands).mockReturnValue( - EXTENDED_DECK_CONFIG_FIXTURE + when(mockGetSimplestDeckConfigForProtocolCommands).mockReturnValue( + // TODO(bh, 2023-11-13): mock cutout config protocol spec + [] ) mockBaseDeck.mockReturnValue(
    mock BaseDeck
    ) }) diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx index d5dd9c2d787..03bc2b1a49a 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx @@ -7,12 +7,10 @@ import { MemoryRouter } from 'react-router-dom' import { renderWithProviders } from '@opentrons/components' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' import { - DeckConfiguration, - Fixture, getDeckDefFromRobotType, - STAGING_AREA_LOAD_NAME, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, } from '@opentrons/shared-data' -import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json' +import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot3_standard.json' import { i18n } from '../../../i18n' import { useChainLiveCommands } from '../../../resources/runs/hooks' @@ -38,6 +36,8 @@ import { FixtureTable } from '../FixtureTable' import { ModulesAndDeckMapViewModal } from '../ModulesAndDeckMapViewModal' import { ProtocolSetupModulesAndDeck } from '..' +import type { CutoutConfig, DeckConfiguration } from '@opentrons/shared-data' + jest.mock('@opentrons/react-api-client') jest.mock('../../../resources/runs/hooks') jest.mock('@opentrons/shared-data/js/helpers') @@ -103,7 +103,7 @@ const mockModulesAndDeckMapViewModal = ModulesAndDeckMapViewModal as jest.Mocked const ROBOT_NAME = 'otie' const RUN_ID = '1' const mockSetSetupScreen = jest.fn() -const mockSetFixtureLocation = jest.fn() +const mockSetCutoutId = jest.fn() const mockSetProvidedFixtureOptions = jest.fn() const calibratedMockApiHeaterShaker = { @@ -118,11 +118,10 @@ const calibratedMockApiHeaterShaker = { last_modified: '2023-06-01T14:42:20.131798+00:00', }, } -const mockFixture = { - fixtureId: 'mockId', - fixtureLocation: '10' as any, - loadName: STAGING_AREA_LOAD_NAME, -} as Fixture +const mockFixture: CutoutConfig = { + cutoutId: 'cutoutD3', + cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, +} const render = () => { return renderWithProviders( @@ -130,7 +129,7 @@ const render = () => { , @@ -363,6 +362,7 @@ describe('ProtocolSetupModulesAndDeck', () => { { ...mockProtocolModuleInfo[0], attachedModuleMatch: calibratedMockApiHeaterShaker, + slotName: 'D3', }, ]) const [{ getByText }] = render() diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx index 5a58f031275..394f3fb68f8 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx @@ -16,11 +16,12 @@ import { } from '@opentrons/components' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' import { + FLEX_ROBOT_TYPE, getDeckDefFromRobotType, getModuleDisplayName, getModuleType, NON_CONNECTING_MODULE_TYPES, - STANDARD_SLOT_LOAD_NAME, + SINGLE_SLOT_FIXTURES, TC_MODULE_LOCATION_OT3, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' @@ -38,7 +39,8 @@ import { import { MultipleModulesModal } from '../Devices/ProtocolRun/SetupModuleAndDeck/MultipleModulesModal' import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { ROBOT_MODEL_OT3, getLocalRobot } from '../../redux/discovery' +import { getLocalRobot } from '../../redux/discovery' +import { getCutoutIdForSlotName } from '../../resources/deck_configuration/utils' import { useChainLiveCommands } from '../../resources/runs/hooks' import { getModulePrepCommands, @@ -57,7 +59,11 @@ import { FixtureTable } from './FixtureTable' import { ModulesAndDeckMapViewModal } from './ModulesAndDeckMapViewModal' import type { CommandData } from '@opentrons/api-client' -import type { Cutout, Fixture, FixtureLoadName } from '@opentrons/shared-data' +import type { + CutoutConfig, + CutoutId, + CutoutFixtureId, +} from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/OnDeviceDisplay/ProtocolSetup' import type { ProtocolCalibrationStatus } from '../../organisms/Devices/hooks' import type { AttachedProtocolModuleMatch } from './utils' @@ -75,7 +81,7 @@ interface RenderModuleStatusProps { commands: ModulePrepCommandsType[], continuePastCommandFailure: boolean ) => Promise - conflictedFixture?: Fixture + conflictedFixture?: CutoutConfig } function RenderModuleStatus({ @@ -186,7 +192,7 @@ interface RowModuleProps { ) => Promise prepCommandErrorMessage: string setPrepCommandErrorMessage: React.Dispatch> - conflictedFixture?: Fixture + conflictedFixture?: CutoutConfig } function RowModule({ @@ -229,7 +235,7 @@ function RowModule({ {showLocationConflictModal && conflictedFixture != null ? ( setShowLocationConflictModal(false)} - cutout={conflictedFixture.fixtureLocation} + cutoutId={conflictedFixture.cutoutId} requiredModule={module.moduleDef.model} isOnDevice={true} /> @@ -302,8 +308,8 @@ function RowModule({ interface ProtocolSetupModulesAndDeckProps { runId: string setSetupScreen: React.Dispatch> - setFixtureLocation: (fixtureLocation: Cutout) => void - setProvidedFixtureOptions: (providedFixtureOptions: FixtureLoadName[]) => void + setCutoutId: (cutoutId: CutoutId) => void + setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void } /** @@ -312,7 +318,7 @@ interface ProtocolSetupModulesAndDeckProps { export function ProtocolSetupModulesAndDeck({ runId, setSetupScreen, - setFixtureLocation, + setCutoutId, setProvidedFixtureOptions, }: ProtocolSetupModulesAndDeckProps): JSX.Element { const { i18n, t } = useTranslation('protocol_setup') @@ -337,7 +343,7 @@ export function ProtocolSetupModulesAndDeck({ const { data: deckConfig } = useDeckConfigurationQuery() const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const deckDef = getDeckDefFromRobotType(ROBOT_MODEL_OT3) + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) const attachedModules = useAttachedModules({ @@ -401,6 +407,7 @@ export function ProtocolSetupModulesAndDeck({ flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing24} marginTop="7.75rem" + marginBottom={SPACING.spacing80} > {isModuleMismatch && !clearModuleMismatchBanner ? ( otherModule.moduleDef.model === module.moduleDef.model ) + + const cutoutIdForSlotName = getCutoutIdForSlotName( + module.slotName, + deckDef + ) + return ( - fixture.fixtureLocation === module.slotName && - fixture.loadName !== STANDARD_SLOT_LOAD_NAME + fixture.cutoutId === cutoutIdForSlotName && + fixture.cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) )} /> ) })} diff --git a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx b/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx index d9e7123ab68..7f24c1e91fc 100644 --- a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx +++ b/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx @@ -5,9 +5,9 @@ import { when, resetAllWhenMocks } from 'jest-when' import { DeckConfigurator, renderWithProviders } from '@opentrons/components' import { useDeckConfigurationQuery, - useCreateDeckConfigurationMutation, + useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' -import { TRASH_BIN_LOAD_NAME } from '@opentrons/shared-data' +import { TRASH_BIN_ADAPTER_FIXTURE } from '@opentrons/shared-data' import { i18n } from '../../../i18n' import { DeckFixtureSetupInstructionsModal } from '../../../organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' @@ -17,7 +17,7 @@ import { DeckConfigurationEditor } from '..' import type { UseQueryResult } from 'react-query' import type { DeckConfiguration } from '@opentrons/shared-data' -const mockCreateDeckConfiguration = jest.fn() +const mockUpdateDeckConfiguration = jest.fn() const mockGoBack = jest.fn() jest.mock('react-router-dom', () => { const reactRouterDom = jest.requireActual('react-router-dom') @@ -29,9 +29,8 @@ jest.mock('react-router-dom', () => { const mockDeckConfig = [ { - fixtureId: 'mockFixtureIdC3', - fixtureLocation: 'cutoutC3', - loadName: TRASH_BIN_LOAD_NAME, + cutoutId: 'cutoutC3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, }, ] @@ -56,8 +55,8 @@ const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFu const mockDeckConfigurationDiscardChangesModal = DeckConfigurationDiscardChangesModal as jest.MockedFunction< typeof DeckConfigurationDiscardChangesModal > -const mockUseCreateDeckConfigurationMutation = useCreateDeckConfigurationMutation as jest.MockedFunction< - typeof useCreateDeckConfigurationMutation +const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction< + typeof useUpdateDeckConfigurationMutation > const render = () => { @@ -83,8 +82,8 @@ describe('DeckConfigurationEditor', () => { mockDeckConfigurationDiscardChangesModal.mockReturnValue(
    mock DeckConfigurationDiscardChangesModal
    ) - when(mockUseCreateDeckConfigurationMutation).mockReturnValue({ - createDeckConfiguration: mockCreateDeckConfiguration, + when(mockUseUpdateDeckConfigurationMutation).mockReturnValue({ + updateDeckConfiguration: mockUpdateDeckConfiguration, } as any) }) diff --git a/app/src/pages/DeckConfiguration/index.tsx b/app/src/pages/DeckConfiguration/index.tsx index 8f1e84f8068..6ea37eb40d6 100644 --- a/app/src/pages/DeckConfiguration/index.tsx +++ b/app/src/pages/DeckConfiguration/index.tsx @@ -12,9 +12,13 @@ import { } from '@opentrons/components' import { useDeckConfigurationQuery, - useCreateDeckConfigurationMutation, + useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' -import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data' +import { + SINGLE_RIGHT_CUTOUTS, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, +} from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' import { ChildNavigation } from '../../organisms/ChildNavigation' @@ -23,7 +27,7 @@ import { DeckFixtureSetupInstructionsModal } from '../../organisms/DeviceDetails import { DeckConfigurationDiscardChangesModal } from '../../organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' import { Portal } from '../../App/portal' -import type { Cutout, DeckConfiguration } from '@opentrons/shared-data' +import type { CutoutId, DeckConfiguration } from '@opentrons/shared-data' export function DeckConfigurationEditor(): JSX.Element { const { t, i18n } = useTranslation([ @@ -40,42 +44,45 @@ export function DeckConfigurationEditor(): JSX.Element { showConfigurationModal, setShowConfigurationModal, ] = React.useState(false) - const [ - targetFixtureLocation, - setTargetFixtureLocation, - ] = React.useState(null) + const [targetCutoutId, setTargetCutoutId] = React.useState( + null + ) const [ showDiscardChangeModal, setShowDiscardChangeModal, ] = React.useState(false) const deckConfig = useDeckConfigurationQuery().data ?? [] - const { createDeckConfiguration } = useCreateDeckConfigurationMutation() + const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const [ currentDeckConfig, setCurrentDeckConfig, ] = React.useState(deckConfig) - const handleClickAdd = (fixtureLocation: Cutout): void => { - setTargetFixtureLocation(fixtureLocation) + const handleClickAdd = (cutoutId: CutoutId): void => { + setTargetCutoutId(cutoutId) setShowConfigurationModal(true) } - const handleClickRemove = (fixtureLocation: Cutout): void => { + const handleClickRemove = (cutoutId: CutoutId): void => { setCurrentDeckConfig(prevDeckConfig => prevDeckConfig.map(fixture => - fixture.fixtureLocation === fixtureLocation - ? { ...fixture, loadName: STANDARD_SLOT_LOAD_NAME } + fixture.cutoutId === cutoutId + ? { + ...fixture, + cutoutFixtureId: SINGLE_RIGHT_CUTOUTS.includes(cutoutId) + ? SINGLE_RIGHT_SLOT_FIXTURE + : SINGLE_LEFT_SLOT_FIXTURE, + } : fixture ) ) - createDeckConfiguration(currentDeckConfig) } const handleClickConfirm = (): void => { if (!isEqual(deckConfig, currentDeckConfig)) { - createDeckConfiguration(currentDeckConfig) + updateDeckConfiguration(currentDeckConfig) } history.goBack() } @@ -114,9 +121,9 @@ export function DeckConfigurationEditor(): JSX.Element { isOnDevice /> ) : null} - {showConfigurationModal && targetFixtureLocation != null ? ( + {showConfigurationModal && targetCutoutId != null ? ( { } else if (protocolHardware.hardwareType === 'module') { return getModuleDisplayName(protocolHardware.moduleModel) } else { - return getFixtureDisplayName(protocolHardware.fixtureName) + return getFixtureDisplayName(protocolHardware.cutoutFixtureId) } } diff --git a/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx b/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx index c2f3fab61c5..e70ebc86dde 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' import { - STAGING_AREA_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { renderWithProviders } from '@opentrons/components' @@ -62,13 +62,13 @@ describe('Hardware', () => { }, { hardwareType: 'fixture', - fixtureName: WASTE_CHUTE_LOAD_NAME, + cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, location: { cutout: WASTE_CHUTE_CUTOUT }, hasSlotConflict: false, }, { hardwareType: 'fixture', - fixtureName: STAGING_AREA_LOAD_NAME, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, location: { cutout: 'cutoutB3' }, hasSlotConflict: false, }, @@ -93,7 +93,7 @@ describe('Hardware', () => { }) getByRole('row', { name: '1 Heater-Shaker Module GEN1' }) getByRole('row', { name: '3 Temperature Module GEN2' }) - getByRole('row', { name: 'D3 Waste Chute' }) - getByRole('row', { name: 'B3 Staging Area Slot' }) + getByRole('row', { name: 'D3 Waste chute only' }) + getByRole('row', { name: 'B3 Staging area slot' }) }) }) diff --git a/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index 32358722ecc..eff166f2617 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -18,7 +18,7 @@ import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - STAGING_AREA_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json' @@ -46,6 +46,7 @@ import { useRunStatus, } from '../../../../organisms/RunTimeControl/hooks' import { useIsHeaterShakerInProtocol } from '../../../../organisms/ModuleCard/hooks' +import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks' import { ConfirmAttachedModal } from '../ConfirmAttachedModal' import { ProtocolSetup } from '..' @@ -53,7 +54,6 @@ import type { UseQueryResult } from 'react-query' import type { DeckConfiguration, CompletedProtocolAnalysis, - Fixture, } from '@opentrons/shared-data' // Mock IntersectionObserver @@ -88,6 +88,7 @@ jest.mock('../../../../organisms/ModuleCard/hooks') jest.mock('../../../../redux/discovery/selectors') jest.mock('../ConfirmAttachedModal') jest.mock('../../../../organisms/ToasterOven') +jest.mock('../../../../resources/deck_configuration/hooks') const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction< typeof getDeckDefFromRobotType @@ -163,6 +164,9 @@ const mockUseModuleCalibrationStatus = useModuleCalibrationStatus as jest.Mocked const mockGetLocalRobot = getLocalRobot as jest.MockedFunction< typeof getLocalRobot > +const mockUseDeckConfigurationCompatibility = useDeckConfigurationCompatibility as jest.MockedFunction< + typeof useDeckConfigurationCompatibility +> const render = (path = '/') => { return renderWithProviders( @@ -230,10 +234,9 @@ const mockDoorStatus = { }, } const mockFixture = { - fixtureId: 'mockId', - fixtureLocation: 'cutoutD1', - loadName: STAGING_AREA_LOAD_NAME, -} as Fixture + cutoutId: 'cutoutD1', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, +} const MOCK_MAKE_SNACKBAR = jest.fn() @@ -332,6 +335,7 @@ describe('ProtocolSetup', () => { .mockReturnValue(({ makeSnackbar: MOCK_MAKE_SNACKBAR, } as unknown) as any) + when(mockUseDeckConfigurationCompatibility).mockReturnValue([]) }) afterEach(() => { diff --git a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx index d8732a22457..ea3210279e6 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx @@ -81,7 +81,7 @@ import { ConfirmAttachedModal } from './ConfirmAttachedModal' import { getLatestCurrentOffsets } from '../../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils' import { CloseButton, PlayButton } from './Buttons' -import type { Cutout, FixtureLoadName } from '@opentrons/shared-data' +import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' import type { OnDeviceRouteParams } from '../../../App/types' import type { ProtocolHardware, ProtocolFixture } from '../../Protocols/hooks' import type { ProtocolModuleInfo } from '../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' @@ -463,7 +463,7 @@ function PrepareToRun({ const missingFixturesText = missingFixtures.length === 1 ? `${t('missing')} ${getFixtureDisplayName( - missingFixtures[0].fixtureName + missingFixtures[0].cutoutFixtureId )}` : t('multiple_fixtures_missing', { count: missingFixtures.length }) @@ -691,11 +691,9 @@ export function ProtocolSetup(): JSX.Element { handleProceedToRunClick, !configBypassHeaterShakerAttachmentConfirmation ) - const [fixtureLocation, setFixtureLocation] = React.useState( - '' as Cutout - ) + const [cutoutId, setCutoutId] = React.useState(null) const [providedFixtureOptions, setProvidedFixtureOptions] = React.useState< - FixtureLoadName[] + CutoutFixtureId[] >([]) // orchestrate setup subpages/components @@ -719,7 +717,7 @@ export function ProtocolSetup(): JSX.Element { ), @@ -731,7 +729,7 @@ export function ProtocolSetup(): JSX.Element { ), 'deck configuration': ( - fixture.fixtureLocation === location.slotName && - fixture.loadName !== STANDARD_SLOT_LOAD_NAME - ), + hasSlotConflict: + deckConfig?.find( + ({ cutoutId, cutoutFixtureId }) => + cutoutId === location.slotName && + cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + ) != null, } } ) @@ -128,48 +139,37 @@ export const useRequiredProtocolHardwareFromAnalysis = ( }) ) - // TODO(jr, 10/2/23): IMMEDIATELY delete the stubs when api supports - // loadFixture - // const requiredFixture: ProtocolFixture[] = analysis.commands - // .filter( - // (command): command is LoadFixtureRunTimeCommand => - // command.commandType === 'loadFixture' - // ) - // .map(({ params }) => { - // return { - // hardwareType: 'fixture', - // fixtureName: params.loadName, - // location: params.location, - // } - // }) - const STUBBED_FIXTURES: ProtocolFixture[] = [ - { - hardwareType: 'fixture', - fixtureName: 'wasteChute', - location: { cutout: 'cutoutD3' }, - hasSlotConflict: false, - }, - { - hardwareType: 'fixture', - fixtureName: 'standardSlot', - location: { cutout: 'cutoutC3' }, - hasSlotConflict: false, - }, - { - hardwareType: 'fixture', - fixtureName: 'stagingArea', - location: { cutout: 'cutoutB3' }, - hasSlotConflict: false, - }, - ] + const nonSingleSlotDeckConfigCompatibility = deckConfigCompatibility.filter( + ({ requiredAddressableAreas }) => + // required AA list includes a non-single-slot AA + !requiredAddressableAreas.every(aa => + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(aa) + ) + ) + // fixture includes at least 1 required AA + const requiredDeckConfigCompatibility = nonSingleSlotDeckConfigCompatibility.filter( + fixture => fixture.requiredAddressableAreas.length > 0 + ) + + const requiredFixtures = requiredDeckConfigCompatibility.map( + ({ cutoutFixtureId, cutoutId, compatibleCutoutFixtureIds }) => ({ + hardwareType: 'fixture' as const, + cutoutFixtureId, + location: { cutout: cutoutId }, + hasSlotConflict: + cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + ? compatibleCutoutFixtureIds.includes(cutoutFixtureId) + : false, + }) + ) return { requiredProtocolHardware: [ ...requiredPipettes, ...requiredModules, ...requiredGripper, - // ...requiredFixture, - ...STUBBED_FIXTURES, + ...requiredFixtures, ], isLoading: isLoadingInstruments || isLoadingModules, } @@ -245,10 +245,10 @@ const useMissingProtocolHardwareFromRequiredProtocolHardware = ( return !hardware.connected } else { // fixtures - return !deckConfig?.find( - fixture => - hardware.location.cutout === fixture.fixtureLocation && - hardware.fixtureName === fixture.loadName + return !deckConfig?.some( + ({ cutoutId, cutoutFixtureId }) => + hardware.location.cutout === cutoutId && + hardware.cutoutFixtureId === cutoutFixtureId ) } }), diff --git a/app/src/resources/deck_configuration/__tests__/hooks.test.ts b/app/src/resources/deck_configuration/__tests__/hooks.test.ts index 2ee9eb57add..5a37005074e 100644 --- a/app/src/resources/deck_configuration/__tests__/hooks.test.ts +++ b/app/src/resources/deck_configuration/__tests__/hooks.test.ts @@ -1,26 +1,16 @@ import { when, resetAllWhenMocks } from 'jest-when' -import { v4 as uuidv4 } from 'uuid' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' import { - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, } from '@opentrons/shared-data' -import { - CONFIGURED, - CONFLICTING, - NOT_CONFIGURED, - useLoadedFixturesConfigStatus, -} from '../hooks' - import type { UseQueryResult } from 'react-query' -import type { - DeckConfiguration, - LoadFixtureRunTimeCommand, -} from '@opentrons/shared-data' +import type { DeckConfiguration } from '@opentrons/shared-data' jest.mock('@opentrons/react-api-client') @@ -30,76 +20,40 @@ const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFu const MOCK_DECK_CONFIG: DeckConfiguration = [ { - fixtureLocation: 'cutoutA1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutA1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutB1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutB1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutC1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutC1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutD1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutD1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutA3', - loadName: TRASH_BIN_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutA3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, }, { - fixtureLocation: 'cutoutB3', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutB3', + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutC3', - loadName: STAGING_AREA_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutC3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutD3', - loadName: WASTE_CHUTE_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutD3', + cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, }, ] -const WASTE_CHUTE_LOADED_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} - -const STAGING_AREA_LOADED_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: STAGING_AREA_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} - -describe('useLoadedFixturesConfigStatus', () => { +describe('useDeckConfigurationCompatibility', () => { beforeEach(() => { when(mockUseDeckConfigurationQuery) .calledWith() @@ -109,34 +63,5 @@ describe('useLoadedFixturesConfigStatus', () => { }) afterEach(() => resetAllWhenMocks()) - it('returns configured status if fixture is configured at location', () => { - const loadedFixturesConfigStatus = useLoadedFixturesConfigStatus([ - WASTE_CHUTE_LOADED_FIXTURE, - ]) - expect(loadedFixturesConfigStatus).toEqual([ - { ...WASTE_CHUTE_LOADED_FIXTURE, configurationStatus: CONFIGURED }, - ]) - }) - it('returns conflicted status if fixture is conflicted at location', () => { - const loadedFixturesConfigStatus = useLoadedFixturesConfigStatus([ - STAGING_AREA_LOADED_FIXTURE, - ]) - expect(loadedFixturesConfigStatus).toEqual([ - { ...STAGING_AREA_LOADED_FIXTURE, configurationStatus: CONFLICTING }, - ]) - }) - it('returns not configured status if fixture is not configured at location', () => { - when(mockUseDeckConfigurationQuery) - .calledWith() - .mockReturnValue({ - data: MOCK_DECK_CONFIG.slice(0, -1), - } as UseQueryResult) - - const loadedFixturesConfigStatus = useLoadedFixturesConfigStatus([ - WASTE_CHUTE_LOADED_FIXTURE, - ]) - expect(loadedFixturesConfigStatus).toEqual([ - { ...WASTE_CHUTE_LOADED_FIXTURE, configurationStatus: NOT_CONFIGURED }, - ]) - }) + it('returns configured status if fixture is configured at location', () => {}) }) diff --git a/app/src/resources/deck_configuration/hooks.ts b/app/src/resources/deck_configuration/hooks.ts index 967d46b5119..9d6dc3d8793 100644 --- a/app/src/resources/deck_configuration/hooks.ts +++ b/app/src/resources/deck_configuration/hooks.ts @@ -1,48 +1,60 @@ +import { parseAllAddressableAreas } from '@opentrons/api-client' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' -import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data' - -import type { Fixture, LoadFixtureRunTimeCommand } from '@opentrons/shared-data' - -export const CONFIGURED = 'configured' -export const CONFLICTING = 'conflicting' -export const NOT_CONFIGURED = 'not configured' - -type LoadedFixtureConfigurationStatus = - | typeof CONFIGURED - | typeof CONFLICTING - | typeof NOT_CONFIGURED - -type LoadedFixtureConfiguration = LoadFixtureRunTimeCommand & { - configurationStatus: LoadedFixtureConfigurationStatus +import { + FLEX_ROBOT_TYPE, + getDeckDefFromRobotTypeV4, +} from '@opentrons/shared-data' + +import { + getCutoutFixturesForCutoutId, + getCutoutIdForAddressableArea, +} from './utils' + +import type { + CutoutFixtureId, + RobotType, + RunTimeCommand, +} from '@opentrons/shared-data' +import type { CutoutConfigProtocolSpec } from './utils' + +export interface CutoutConfigAndCompatibility extends CutoutConfigProtocolSpec { + compatibleCutoutFixtureIds: CutoutFixtureId[] } - -export function useLoadedFixturesConfigStatus( - loadedFixtures: LoadFixtureRunTimeCommand[] -): LoadedFixtureConfiguration[] { +export function useDeckConfigurationCompatibility( + robotType: RobotType, + protocolCommands: RunTimeCommand[] +): CutoutConfigAndCompatibility[] { const deckConfig = useDeckConfigurationQuery().data ?? [] - - return loadedFixtures.map(loadedFixture => { - const deckConfigurationAtLocation = deckConfig.find( - (deckFixture: Fixture) => - deckFixture.fixtureLocation === loadedFixture.params.location.cutout - ) - - let configurationStatus: LoadedFixtureConfigurationStatus = NOT_CONFIGURED - if ( - deckConfigurationAtLocation != null && - deckConfigurationAtLocation.loadName === loadedFixture.params.loadName - ) { - configurationStatus = CONFIGURED - // special casing this for now until we know what the backend will give us. It is only - // conflicting if the current deck configuration fixture is not the desired or standard slot - } else if ( - deckConfigurationAtLocation != null && - deckConfigurationAtLocation.loadName !== loadedFixture.params.loadName && - deckConfigurationAtLocation.loadName !== STANDARD_SLOT_LOAD_NAME - ) { - configurationStatus = CONFLICTING - } - - return { ...loadedFixture, configurationStatus } - }) + if (robotType !== FLEX_ROBOT_TYPE) return [] + const deckDef = getDeckDefFromRobotTypeV4(robotType) + const allAddressableAreas = parseAllAddressableAreas(protocolCommands) + return deckConfig.reduce( + (acc, { cutoutId, cutoutFixtureId }) => { + const fixturesThatMountToCutoutId = getCutoutFixturesForCutoutId( + cutoutId, + deckDef.cutoutFixtures + ) + const requiredAddressableAreasForCutoutId = allAddressableAreas.filter( + aa => + getCutoutIdForAddressableArea(aa, fixturesThatMountToCutoutId) === + cutoutId + ) + return [ + ...acc, + { + cutoutId, + cutoutFixtureId: cutoutFixtureId, + requiredAddressableAreas: requiredAddressableAreasForCutoutId, + compatibleCutoutFixtureIds: fixturesThatMountToCutoutId + .filter(cf => + requiredAddressableAreasForCutoutId.every(aa => + cf.providesAddressableAreas[cutoutId].includes(aa) + ) + ) + .map(cf => cf.id), + }, + ] + }, + [] + ) } diff --git a/app/src/resources/deck_configuration/types.ts b/app/src/resources/deck_configuration/types.ts new file mode 100644 index 00000000000..2929de72deb --- /dev/null +++ b/app/src/resources/deck_configuration/types.ts @@ -0,0 +1,11 @@ +import type { + CutoutId, + CutoutFixtureId, + AddressableAreaName, +} from '@opentrons/shared-data' + +export interface CutoutConfig { + cutoutId: CutoutId + cutoutFixtureId: CutoutFixtureId + requiredAddressableAreas: AddressableAreaName[] +} diff --git a/app/src/resources/deck_configuration/utils.ts b/app/src/resources/deck_configuration/utils.ts index 36150586b09..2075306b09e 100644 --- a/app/src/resources/deck_configuration/utils.ts +++ b/app/src/resources/deck_configuration/utils.ts @@ -1,27 +1,26 @@ import { parseAllAddressableAreas } from '@opentrons/api-client' import { FLEX_ROBOT_TYPE, + getAddressableAreaFromSlotId, getDeckDefFromRobotTypeV4, } from '@opentrons/shared-data' import type { CutoutId, - DeckConfiguration, RunTimeCommand, - Cutout, CutoutFixtureId, CutoutFixture, AddressableAreaName, - FixtureLoadName, + DeckDefinition, } from '@opentrons/shared-data' -interface CutoutConfig { +export interface CutoutConfigProtocolSpec { cutoutId: CutoutId - cutoutFixtureId: CutoutFixtureId + cutoutFixtureId: CutoutFixtureId | null requiredAddressableAreas: AddressableAreaName[] } -export const FLEX_SIMPLEST_DECK_CONFIG: CutoutConfig[] = [ +export const FLEX_SIMPLEST_DECK_CONFIG: CutoutConfigProtocolSpec[] = [ { cutoutId: 'cutoutA1', cutoutFixtureId: 'singleLeftSlot', @@ -86,95 +85,81 @@ export const FLEX_SIMPLEST_DECK_CONFIG: CutoutConfig[] = [ export function getSimplestDeckConfigForProtocolCommands( protocolAnalysisCommands: RunTimeCommand[] -): CutoutConfig[] { +): CutoutConfigProtocolSpec[] { // TODO(BC, 2023-11-06): abstract out the robot type const deckDef = getDeckDefFromRobotTypeV4(FLEX_ROBOT_TYPE) const addressableAreas = parseAllAddressableAreas(protocolAnalysisCommands) - const simplestDeckConfig = addressableAreas.reduce( - (acc, addressableArea) => { - const cutoutFixturesForAddressableArea = getCutoutFixturesForAddressableAreas( - [addressableArea], - deckDef.cutoutFixtures + const simplestDeckConfig = addressableAreas.reduce< + CutoutConfigProtocolSpec[] + >((acc, addressableArea) => { + const cutoutFixturesForAddressableArea = getCutoutFixturesForAddressableAreas( + [addressableArea], + deckDef.cutoutFixtures + ) + const cutoutIdForAddressableArea = getCutoutIdForAddressableArea( + addressableArea, + cutoutFixturesForAddressableArea + ) + const cutoutFixturesForCutoutId = + cutoutIdForAddressableArea != null + ? getCutoutFixturesForCutoutId( + cutoutIdForAddressableArea, + deckDef.cutoutFixtures + ) + : null + + const existingCutoutConfig = acc.find( + cutoutConfig => cutoutConfig.cutoutId === cutoutIdForAddressableArea + ) + + if ( + existingCutoutConfig != null && + cutoutFixturesForCutoutId != null && + cutoutIdForAddressableArea != null + ) { + const indexOfExistingFixture = cutoutFixturesForCutoutId.findIndex( + ({ id }) => id === existingCutoutConfig.cutoutFixtureId ) - const cutoutIdForAddressableArea = getCutoutIdForAddressableArea( - addressableArea, - cutoutFixturesForAddressableArea + const accIndex = acc.findIndex( + ({ cutoutId }) => cutoutId === cutoutIdForAddressableArea ) - const cutoutFixturesForCutoutId = - cutoutIdForAddressableArea != null - ? getCutoutFixturesForCutoutId( - cutoutIdForAddressableArea, - deckDef.cutoutFixtures - ) - : null - - const existingCutoutConfig = acc.find( - cutoutConfig => cutoutConfig.cutoutId === cutoutIdForAddressableArea + const previousRequiredAAs = acc[accIndex]?.requiredAddressableAreas + const allNextRequiredAddressableAreas = previousRequiredAAs.includes( + addressableArea + ) + ? previousRequiredAAs + : [...previousRequiredAAs, addressableArea] + const nextCompatibleCutoutFixture = getSimplestFixtureForAddressableAreas( + cutoutIdForAddressableArea, + allNextRequiredAddressableAreas, + cutoutFixturesForCutoutId + ) + const indexOfCurrentFixture = cutoutFixturesForCutoutId.findIndex( + ({ id }) => id === nextCompatibleCutoutFixture?.id ) if ( - existingCutoutConfig != null && - cutoutFixturesForCutoutId != null && - cutoutIdForAddressableArea != null + nextCompatibleCutoutFixture != null && + indexOfCurrentFixture > indexOfExistingFixture ) { - const indexOfExistingFixture = cutoutFixturesForCutoutId.findIndex( - ({ id }) => id === existingCutoutConfig.cutoutFixtureId - ) - const accIndex = acc.findIndex( - ({ cutoutId }) => cutoutId === cutoutIdForAddressableArea - ) - const previousRequiredAAs = acc[accIndex]?.requiredAddressableAreas - const allNextRequiredAddressableAreas = previousRequiredAAs.includes( - addressableArea - ) - ? previousRequiredAAs - : [...previousRequiredAAs, addressableArea] - const nextCompatibleCutoutFixture = getSimplestFixtureForAddressableAreas( - cutoutIdForAddressableArea, - allNextRequiredAddressableAreas, - cutoutFixturesForCutoutId - ) - const indexOfCurrentFixture = cutoutFixturesForCutoutId.findIndex( - ({ id }) => id === nextCompatibleCutoutFixture?.id - ) - - if ( - nextCompatibleCutoutFixture != null && - indexOfCurrentFixture > indexOfExistingFixture - ) { - return [ - ...acc.slice(0, accIndex), - { - cutoutId: cutoutIdForAddressableArea, - cutoutFixtureId: nextCompatibleCutoutFixture.id, - requiredAddressableAreas: allNextRequiredAddressableAreas, - }, - ...acc.slice(accIndex + 1), - ] - } + return [ + ...acc.slice(0, accIndex), + { + cutoutId: cutoutIdForAddressableArea, + cutoutFixtureId: nextCompatibleCutoutFixture.id, + requiredAddressableAreas: allNextRequiredAddressableAreas, + }, + ...acc.slice(accIndex + 1), + ] } - return acc - }, - FLEX_SIMPLEST_DECK_CONFIG - ) + } + return acc + }, FLEX_SIMPLEST_DECK_CONFIG) return simplestDeckConfig } -// TODO(BC, 11/7/23): remove this function in favor of getSimplestDeckConfigForProtocolCommands -export function getDeckConfigFromProtocolCommands( - commands: RunTimeCommand[] -): DeckConfiguration { - return getSimplestDeckConfigForProtocolCommands(commands).map( - ({ cutoutId, cutoutFixtureId }) => ({ - fixtureId: cutoutFixtureId, - fixtureLocation: cutoutId as Cutout, - loadName: cutoutFixtureId as FixtureLoadName, - }) - ) -} - export function getCutoutFixturesForAddressableAreas( addressableAreas: AddressableAreaName[], cutoutFixtures: CutoutFixture[] @@ -195,6 +180,22 @@ export function getCutoutFixturesForCutoutId( ) } +export function getCutoutIdForSlotName( + slotName: string, + deckDef: DeckDefinition +): CutoutId | null { + const addressableArea = getAddressableAreaFromSlotId(slotName, deckDef) + const cutoutIdForSlotName = + addressableArea != null + ? getCutoutIdForAddressableArea( + addressableArea.id, + deckDef.cutoutFixtures + ) + : null + + return cutoutIdForSlotName +} + export function getCutoutIdForAddressableArea( addressableArea: AddressableAreaName, cutoutFixtures: CutoutFixture[] diff --git a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx index 28d66051393..0760fc10fa8 100644 --- a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx +++ b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx @@ -46,7 +46,7 @@ import type { WellFill } from '../Labware' interface BaseDeckProps { robotType: RobotType - labwareLocations: Array<{ + labwareLocations?: Array<{ labwareLocation: LabwareLocation definition: LabwareDefinition2 wellFill?: WellFill @@ -54,7 +54,7 @@ interface BaseDeckProps { labwareChildren?: React.ReactNode onLabwareClick?: () => void }> - moduleLocations: Array<{ + moduleLocations?: Array<{ moduleModel: ModuleModel moduleLocation: ModuleLocation nestedLabwareDef?: LabwareDefinition2 | null @@ -76,8 +76,8 @@ interface BaseDeckProps { export function BaseDeck(props: BaseDeckProps): JSX.Element { const { robotType, - moduleLocations, - labwareLocations, + moduleLocations = [], + labwareLocations = [], lightFill = COLORS.light1, darkFill = COLORS.darkGreyEnabled, deckLayerBlocklist = [], @@ -92,20 +92,20 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { const singleSlotFixtures = deckConfig.filter(fixture => SINGLE_SLOT_FIXTURES.includes( - fixture.fixtureId as SingleSlotCutoutFixtureId + fixture.cutoutFixtureId as SingleSlotCutoutFixtureId ) ) const stagingAreaFixtures = deckConfig.filter( - fixture => fixture.fixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE + fixture => fixture.cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE ) const trashBinFixtures = deckConfig.filter( - fixture => fixture.fixtureId === TRASH_BIN_ADAPTER_FIXTURE + fixture => fixture.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE ) const wasteChuteFixtures = deckConfig.filter( fixture => WASTE_CHUTE_FIXTURES.includes( - fixture.fixtureId as WasteChuteCutoutFixtureId - ) && fixture.fixtureLocation === WASTE_CHUTE_CUTOUT + fixture.cutoutFixtureId as WasteChuteCutoutFixtureId + ) && fixture.cutoutId === WASTE_CHUTE_CUTOUT ) return ( @@ -121,8 +121,8 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { <> {singleSlotFixtures.map(fixture => ( ( ))} {trashBinFixtures.map(fixture => ( - + ))} {wasteChuteFixtures.map(fixture => ( void - handleClickRemove: (fixtureLocation: Cutout) => void + handleClickAdd: (cutoutId: CutoutId) => void + handleClickRemove: (cutoutId: CutoutId) => void lightFill?: string darkFill?: string readOnly?: boolean @@ -45,7 +45,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) // restrict configuration to certain locations - const configurableFixtureLocations: Cutout[] = [ + const configurableFixtureLocations: CutoutId[] = [ 'cutoutA1', 'cutoutB1', 'cutoutC1', @@ -55,23 +55,26 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { 'cutoutC3', 'cutoutD3', ] - const configurableDeckConfig = deckConfig.filter(fixture => - configurableFixtureLocations.includes(fixture.fixtureLocation) + const configurableDeckConfig = deckConfig.filter(({ cutoutId }) => + configurableFixtureLocations.includes(cutoutId) ) const stagingAreaFixtures = configurableDeckConfig.filter( - fixture => fixture.loadName === STAGING_AREA_LOAD_NAME + ({ cutoutFixtureId }) => cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE ) const wasteChuteFixtures = configurableDeckConfig.filter( - fixture => fixture.loadName === WASTE_CHUTE_LOAD_NAME + ({ cutoutFixtureId }) => + cutoutFixtureId != null && WASTE_CHUTE_FIXTURES.includes(cutoutFixtureId) ) const emptyFixtures = readOnly ? [] : configurableDeckConfig.filter( - fixture => fixture.loadName === STANDARD_SLOT_LOAD_NAME + ({ cutoutFixtureId }) => + cutoutFixtureId != null && + SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) ) const trashBinFixtures = configurableDeckConfig.filter( - fixture => fixture.loadName === TRASH_BIN_LOAD_NAME + ({ cutoutFixtureId }) => cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE ) return ( @@ -80,47 +83,46 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions viewBox={`${deckDef.cornerOffsetFromOrigin[0]} ${deckDef.cornerOffsetFromOrigin[1]} ${deckDef.dimensions[0]} ${deckDef.dimensions[1]}`} > - {/* TODO(bh, 2023-10-18): migrate to v4 deck def cutouts */} - {deckDef.locations.cutouts.map(slotDef => ( + {deckDef.locations.cutouts.map(cutout => ( ))} - {stagingAreaFixtures.map(fixture => ( + {stagingAreaFixtures.map(({ cutoutId }) => ( ))} - {emptyFixtures.map(fixture => ( + {emptyFixtures.map(({ cutoutId }) => ( ))} - {wasteChuteFixtures.map(fixture => ( + {wasteChuteFixtures.map(({ cutoutId }) => ( ))} - {trashBinFixtures.map(fixture => ( + {trashBinFixtures.map(({ cutoutId }) => ( ))} diff --git a/protocol-designer/src/components/DeckSetup/constants.ts b/protocol-designer/src/components/DeckSetup/constants.ts index 8d2a436291c..2037126b253 100644 --- a/protocol-designer/src/components/DeckSetup/constants.ts +++ b/protocol-designer/src/components/DeckSetup/constants.ts @@ -1,26 +1,3 @@ -import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data' - -const cutouts = [ - 'A1', - 'A2', - 'A3', - 'B1', - 'B2', - 'B3', - 'C1', - 'C2', - 'C3', - 'D1', - 'D2', - 'D3', -] - -export const DEFAULT_SLOTS = cutouts.map((cutout, index) => ({ - fixtureId: (index + 1).toString(), - fixtureLocation: cutout, - loadName: STANDARD_SLOT_LOAD_NAME, -})) - export const VIEWBOX_MIN_X = -64 export const VIEWBOX_MIN_Y = -10 export const VIEWBOX_WIDTH = 520 diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx index ed443435e00..3db220a1858 100644 --- a/protocol-designer/src/components/DeckSetup/index.tsx +++ b/protocol-designer/src/components/DeckSetup/index.tsx @@ -23,6 +23,7 @@ import { } from '@opentrons/step-generation' import { FLEX_ROBOT_TYPE, + FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, getAddressableAreaFromSlotId, getDeckDefFromRobotType, getLabwareHasQuirk, @@ -33,11 +34,10 @@ import { inferModuleOrientationFromXCoordinate, isAddressableAreaStandardSlot, OT2_ROBOT_TYPE, - STAGING_AREA_LOAD_NAME, THERMOCYCLER_MODULE_TYPE, - TRASH_BIN_LOAD_NAME, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_ADDRESSABLE_AREAS, WASTE_CHUTE_CUTOUT, - WASTE_CHUTE_LOAD_NAME, } from '@opentrons/shared-data' import { FLEX_TRASH_DEF_URI, OT_2_TRASH_DEF_URI } from '../../constants' import { selectors as labwareDefSelectors } from '../../labware-defs' @@ -528,23 +528,28 @@ export const DeckSetup = (): JSX.Element => { const trashBinFixtures = [ { - fixtureId: trash?.id, - fixtureLocation: + cutoutId: trash?.slot != null ? getCutoutIdForAddressableArea( trash?.slot as AddressableAreaName, deckDef.cutoutFixtures ) : null, - loadName: TRASH_BIN_LOAD_NAME, + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, }, ] const wasteChuteFixtures = Object.values( activeDeckSetup.additionalEquipmentOnDeck - ).filter(aE => aE.name === WASTE_CHUTE_LOAD_NAME) + ).filter(aE => + WASTE_CHUTE_ADDRESSABLE_AREAS.includes(aE.name as AddressableAreaName) + ) const stagingAreaFixtures: AdditionalEquipmentEntity[] = Object.values( activeDeckSetup.additionalEquipmentOnDeck - ).filter(aE => aE.name === STAGING_AREA_LOAD_NAME) + ).filter(aE => + FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes( + aE.name as AddressableAreaName + ) + ) const filteredAddressableAreas = deckDef.locations.addressableAreas.filter( aa => isAddressableAreaStandardSlot(aa.id, deckDef) @@ -593,11 +598,11 @@ export const DeckSetup = (): JSX.Element => { /> ))} {trash != null - ? trashBinFixtures.map(fixture => - fixture.fixtureLocation != null ? ( - + ? trashBinFixtures.map(({ cutoutId }) => + cutoutId != null ? ( + { diff --git a/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx index dc552a72d63..423b0c93689 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx @@ -13,16 +13,16 @@ import { } from '@opentrons/components' import { OT2_ROBOT_TYPE, + SINGLE_RIGHT_SLOT_FIXTURE, STAGING_AREA_CUTOUTS, - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { i18n } from '../../../localization' import { getEnableDeckModification } from '../../../feature-flags/selectors' import { GoBack } from './GoBack' import { HandleEnter } from './HandleEnter' -import type { DeckConfiguration } from '@opentrons/shared-data' +import type { DeckConfiguration, CutoutId } from '@opentrons/shared-data' import type { WizardTileProps } from './types' export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { @@ -30,36 +30,36 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { const isOt2 = values.fields.robotType === OT2_ROBOT_TYPE const deckConfigurationFF = useSelector(getEnableDeckModification) const stagingAreaItems = values.additionalEquipment.filter(equipment => - equipment.includes(STAGING_AREA_LOAD_NAME) + // TODO(bc, 11/14/2023): refactor the additional items field to include a cutoutId + // and a cutoutFixtureId so that we don't have to string parse here to generate them + equipment.includes('stagingArea') ) - const savedStagingAreaSlots = stagingAreaItems.flatMap(item => { - const [loadName, fixtureLocation] = item.split('_') - const fixtureId = `id_${fixtureLocation}` - return [ - { - fixtureId, - fixtureLocation, - loadName, - }, - ] as DeckConfiguration - }) + const savedStagingAreaSlots: DeckConfiguration = stagingAreaItems.flatMap( + item => { + // TODO(bc, 11/14/2023): refactor the additional items field to include a cutoutId + // and a cutoutFixtureId so that we don't have to string parse here to generate them + const cutoutId = item.split('_')[1] as CutoutId + return [ + { + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + cutoutId, + }, + ] + } + ) - // NOTE: fixtureId doesn't matter since we don't create - // the entity until you complete the create file wizard via createDeckFixture action - // fixtureId here is only needed to visually add to the deck configurator const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_CUTOUTS.map( - fixtureLocation => ({ - fixtureId: `id_${fixtureLocation}`, - fixtureLocation, - loadName: STANDARD_SLOT_LOAD_NAME, + cutoutId => ({ + cutoutId, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, }) ) STANDARD_EMPTY_SLOTS.forEach(emptySlot => { if ( !savedStagingAreaSlots.some( - slot => slot.fixtureLocation === emptySlot.fixtureLocation + ({ cutoutId }) => cutoutId === emptySlot.cutoutId ) ) { savedStagingAreaSlots.push(emptySlot) @@ -78,12 +78,12 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { return null } - const handleClickAdd = (fixtureLocation: string): void => { + const handleClickAdd = (cutoutId: string): void => { const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { - if (slot.fixtureLocation === fixtureLocation) { + if (slot.cutoutId === cutoutId) { return { ...slot, - loadName: STAGING_AREA_LOAD_NAME, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, } } return slot @@ -91,16 +91,16 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { setUpdatedSlots(modifiedSlots) setFieldValue('additionalEquipment', [ ...values.additionalEquipment, - `${STAGING_AREA_LOAD_NAME}_${fixtureLocation}`, + `stagingArea_${cutoutId}`, ]) } - const handleClickRemove = (fixtureLocation: string): void => { + const handleClickRemove = (cutoutId: string): void => { const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { - if (slot.fixtureLocation === fixtureLocation) { + if (slot.cutoutId === cutoutId) { return { ...slot, - loadName: STANDARD_SLOT_LOAD_NAME, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, } } return slot @@ -108,10 +108,7 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { setUpdatedSlots(modifiedSlots) setFieldValue( 'additionalEquipment', - without( - values.additionalEquipment, - `${STAGING_AREA_LOAD_NAME}_${fixtureLocation}` - ) + without(values.additionalEquipment, `stagingArea_${cutoutId}`) ) } diff --git a/protocol-designer/src/components/modules/StagingAreasModal.tsx b/protocol-designer/src/components/modules/StagingAreasModal.tsx index 79725aa5147..a5fd5a258ec 100644 --- a/protocol-designer/src/components/modules/StagingAreasModal.tsx +++ b/protocol-designer/src/components/modules/StagingAreasModal.tsx @@ -17,11 +17,11 @@ import { DeckConfigurator, } from '@opentrons/components' import { - Cutout, + CutoutId, DeckConfiguration, - STAGING_AREA_LOAD_NAME, + SINGLE_RIGHT_SLOT_FIXTURE, STAGING_AREA_CUTOUTS, - STANDARD_SLOT_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { i18n } from '../../localization' import { @@ -56,27 +56,27 @@ const StagingAreasModalComponent = ( ? false : areSlotsEmpty.includes(false) - const mappedStagingAreas = stagingAreas.flatMap(area => { - return [ - { - fixtureId: area.id, - fixtureLocation: area.location ?? '', - loadName: STAGING_AREA_LOAD_NAME, - }, - ] as DeckConfiguration + const mappedStagingAreas: DeckConfiguration = stagingAreas.flatMap(area => { + return area.location != null + ? [ + { + cutoutId: area.location as CutoutId, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + }, + ] + : [] }) const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_CUTOUTS.map( - fixtureLocation => ({ - fixtureId: `id_${fixtureLocation}`, - fixtureLocation: fixtureLocation as Cutout, - loadName: STANDARD_SLOT_LOAD_NAME, + cutoutId => ({ + cutoutId, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, }) ) STANDARD_EMPTY_SLOTS.forEach(emptySlot => { if ( !mappedStagingAreas.some( - slot => slot.fixtureLocation === emptySlot.fixtureLocation + ({ cutoutId }) => cutoutId === emptySlot.cutoutId ) ) { mappedStagingAreas.push(emptySlot) @@ -89,34 +89,31 @@ const StagingAreasModalComponent = ( selectableSlots ) - const handleClickAdd = (fixtureLocation: string): void => { + const handleClickAdd = (cutoutId: string): void => { const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { - if (slot.fixtureLocation === fixtureLocation) { + if (slot.cutoutId === cutoutId) { return { ...slot, - loadName: STAGING_AREA_LOAD_NAME, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, } } return slot }) setUpdatedSlots(modifiedSlots) - const updatedSelectedSlots = [...values.selectedSlots, fixtureLocation] + const updatedSelectedSlots = [...values.selectedSlots, cutoutId] setFieldValue('selectedSlots', updatedSelectedSlots) } - const handleClickRemove = (fixtureLocation: string): void => { + const handleClickRemove = (cutoutId: string): void => { const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { - if (slot.fixtureLocation === fixtureLocation) { - return { - ...slot, - loadName: STANDARD_SLOT_LOAD_NAME, - } + if (slot.cutoutId === cutoutId) { + return { ...slot, loadName: SINGLE_RIGHT_SLOT_FIXTURE } } return slot }) setUpdatedSlots(modifiedSlots) const updatedSelectedSlots = values.selectedSlots.filter( - item => item !== fixtureLocation + item => item !== cutoutId ) setFieldValue('selectedSlots', updatedSelectedSlots) } diff --git a/react-api-client/src/deck_configuration/__tests__/useCreateDeckConfigurationMutation.test.tsx b/react-api-client/src/deck_configuration/__tests__/useCreateDeckConfigurationMutation.test.tsx deleted file mode 100644 index 200b05f9208..00000000000 --- a/react-api-client/src/deck_configuration/__tests__/useCreateDeckConfigurationMutation.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import * as React from 'react' -import { when, resetAllWhenMocks } from 'jest-when' -import { QueryClient, QueryClientProvider } from 'react-query' -import { renderHook } from '@testing-library/react-hooks' - -import { createDeckConfiguration } from '@opentrons/api-client' -// import { -// TRASH_BIN_LOAD_NAME, -// WASTE_CHUTE_LOAD_NAME, -// WASTE_CHUTE_CUTOUT, -// } from '@opentrons/shared-data' - -import { useHost } from '../../api' -import { useCreateDeckConfigurationMutation } from '..' - -import type { HostConfig } from '@opentrons/api-client' -// import type { DeckConfiguration } from '@opentrons/shared-data' - -jest.mock('@opentrons/api-client') -jest.mock('../../api/useHost') - -const mockCreateDeckConfiguration = createDeckConfiguration as jest.MockedFunction< - typeof createDeckConfiguration -> -const mockUseHost = useHost as jest.MockedFunction - -// const mockDeckConfiguration = [ -// { -// fixtureId: 'mockFixtureWasteChuteId', -// fixtureLocation: 'cutoutD3', -// loadName: WASTE_CHUTE_LOAD_NAME, -// }, -// ] as DeckConfiguration - -const HOST_CONFIG: HostConfig = { hostname: 'localhost' } - -describe('useCreateDeckConfigurationMutation hook', () => { - let wrapper: React.FunctionComponent<{}> - - beforeEach(() => { - const queryClient = new QueryClient() - const clientProvider: React.FunctionComponent<{}> = ({ children }) => ( - {children} - ) - - wrapper = clientProvider - }) - - afterEach(() => { - resetAllWhenMocks() - }) - - it('should return no data when calling createDeckConfiguration if the request fails', async () => { - when(mockUseHost).calledWith().mockReturnValue(HOST_CONFIG) - when(mockCreateDeckConfiguration) - .calledWith(HOST_CONFIG, []) - .mockRejectedValue('oh no') - - const { result, waitFor } = renderHook( - () => useCreateDeckConfigurationMutation(), - { - wrapper, - } - ) - expect(result.current.data).toBeUndefined() - result.current.createDeckConfiguration([]) - await waitFor(() => { - return result.current.status !== 'loading' - }) - expect(result.current.data).toBeUndefined() - }) - // ToDo (kk:10/25/2023) this part will be update when backend is ready - // it('should create a run when calling createDeckConfiguration callback with DeckConfiguration', async () => { - // when(useHost).calledWith().mockReturnValue(HOST_CONFIG) - // when(mockCreateDeckConfiguration) - // .calledWith(HOST_CONFIG, mockDeckConfiguration) - // .mockResolvedValue({ - // data: mockCreateDeckConfiguration, - // } as Response) - - // const { result, waitFor } = renderHook(useCreateDeckConfigurationMutation, { - // wrapper, - // }) - // act(() => result.current.createDeckConfiguration(mockDeckConfiguration)) - - // await waitFor(() => result.current.data != null) - // expect(result.current.data).toEqual(mockDeckConfiguration) - // }) -}) diff --git a/react-api-client/src/deck_configuration/index.ts b/react-api-client/src/deck_configuration/index.ts index b6237d14c30..063a5b0fe82 100644 --- a/react-api-client/src/deck_configuration/index.ts +++ b/react-api-client/src/deck_configuration/index.ts @@ -1,3 +1,2 @@ -export { useCreateDeckConfigurationMutation } from './useCreateDeckConfigurationMutation' export { useDeckConfigurationQuery } from './useDeckConfigurationQuery' export { useUpdateDeckConfigurationMutation } from './useUpdateDeckConfigurationMutation' diff --git a/react-api-client/src/deck_configuration/useCreateDeckConfigurationMutation.ts b/react-api-client/src/deck_configuration/useCreateDeckConfigurationMutation.ts deleted file mode 100644 index ad898e8c13b..00000000000 --- a/react-api-client/src/deck_configuration/useCreateDeckConfigurationMutation.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useMutation, useQueryClient } from 'react-query' -import { createDeckConfiguration } from '@opentrons/api-client' -import { useHost } from '../api' - -import type { - UseMutateFunction, - UseMutationOptions, - UseMutationResult, -} from 'react-query' -import type { AxiosError } from 'axios' -import type { DeckConfiguration } from '@opentrons/shared-data' -import type { ErrorResponse, HostConfig } from '@opentrons/api-client' - -const DECK_CONFIGURATION = 'deck_configuration' - -export type UseCreateDeckConfigurationMutationResult = UseMutationResult< - DeckConfiguration, - AxiosError, - DeckConfiguration -> & { - createDeckConfiguration: UseMutateFunction< - DeckConfiguration, - AxiosError, - DeckConfiguration - > -} - -export type UseCreateDeckConfigurationMutationOptions = UseMutationOptions< - DeckConfiguration, - AxiosError, - DeckConfiguration -> - -export function useCreateDeckConfigurationMutation( - options: UseCreateDeckConfigurationMutationOptions = {} -): UseCreateDeckConfigurationMutationResult { - const host = useHost() - const queryClient = useQueryClient() - - const mutation = useMutation< - DeckConfiguration, - AxiosError, - DeckConfiguration - >( - [host, DECK_CONFIGURATION], - (deckConfiguration: DeckConfiguration) => - createDeckConfiguration(host as HostConfig, deckConfiguration).then( - response => { - queryClient - .invalidateQueries([host, DECK_CONFIGURATION]) - .catch((error: Error) => { - throw error - }) - return response.data - } - ), - options - ) - return { - ...mutation, - createDeckConfiguration: mutation.mutate, - } -} diff --git a/react-api-client/src/deck_configuration/useUpdateDeckConfigurationMutation.ts b/react-api-client/src/deck_configuration/useUpdateDeckConfigurationMutation.ts index 13dc7e0a155..f90bfdd2b49 100644 --- a/react-api-client/src/deck_configuration/useUpdateDeckConfigurationMutation.ts +++ b/react-api-client/src/deck_configuration/useUpdateDeckConfigurationMutation.ts @@ -12,24 +12,24 @@ import { useHost } from '../api' import type { AxiosError } from 'axios' import type { ErrorResponse, HostConfig } from '@opentrons/api-client' -import type { Fixture } from '@opentrons/shared-data' +import type { DeckConfiguration } from '@opentrons/shared-data' export type UseUpdateDeckConfigurationMutationResult = UseMutationResult< - Omit, + DeckConfiguration, AxiosError, - Omit + DeckConfiguration > & { updateDeckConfiguration: UseMutateFunction< - Omit, + DeckConfiguration, AxiosError, - Omit + DeckConfiguration > } export type UseUpdateDeckConfigurationMutationOptions = UseMutationOptions< - Omit, + DeckConfiguration, AxiosError, - Omit + DeckConfiguration > export function useUpdateDeckConfigurationMutation( @@ -39,12 +39,12 @@ export function useUpdateDeckConfigurationMutation( const queryClient = useQueryClient() const mutation = useMutation< - Omit, + DeckConfiguration, AxiosError, - Omit + DeckConfiguration >( [host, 'deck_configuration'], - (fixture: Omit) => + (fixture: DeckConfiguration) => updateDeckConfiguration(host as HostConfig, fixture).then(response => { queryClient .invalidateQueries([host, 'deck_configuration']) diff --git a/shared-data/command/types/setup.ts b/shared-data/command/types/setup.ts index f8fb78752e5..73156d26377 100644 --- a/shared-data/command/types/setup.ts +++ b/shared-data/command/types/setup.ts @@ -5,7 +5,6 @@ import type { LabwareOffset, PipetteName, ModuleModel, - FixtureLoadName, Cutout, } from '../../js' @@ -158,6 +157,6 @@ interface LoadLiquidResult { interface LoadFixtureParams { location: { cutout: Cutout } - loadName: FixtureLoadName + loadName: string fixtureId?: string } diff --git a/shared-data/deck/types/schemaV4.ts b/shared-data/deck/types/schemaV4.ts index ecf6bb51d26..9f4d6045fc4 100644 --- a/shared-data/deck/types/schemaV4.ts +++ b/shared-data/deck/types/schemaV4.ts @@ -64,6 +64,8 @@ export type SingleSlotCutoutFixtureId = | 'singleCenterSlot' | 'singleRightSlot' +export type StagingAreaRightSlotFixtureId = 'stagingAreaRightSlot' + export type TrashBinAdapterCutoutFixtureId = 'trashBinAdapter' export type WasteChuteCutoutFixtureId = @@ -74,6 +76,7 @@ export type WasteChuteCutoutFixtureId = export type CutoutFixtureId = | SingleSlotCutoutFixtureId + | StagingAreaRightSlotFixtureId | TrashBinAdapterCutoutFixtureId | WasteChuteCutoutFixtureId | 'stagingAreaRightSlot' diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index b31a67b958d..368f23c0a59 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -1,5 +1,5 @@ -import { AddressableAreaName } from '.' -import type { Cutout, ModuleType } from './types' +import type { CutoutFixtureId, CutoutId, AddressableAreaName } from '../deck' +import type { ModuleType } from './types' // constants for dealing with robot coordinate system (eg in labwareTools) export const SLOT_LENGTH_MM = 127.76 // along X axis in robot coordinate system @@ -184,18 +184,35 @@ export const TC_MODULE_LOCATION_OT3: 'A1+B1' = 'A1+B1' export const WEIGHT_OF_96_CHANNEL: '~10kg' = '~10kg' -export const STAGING_AREA_CUTOUTS: Cutout[] = [ +export const SINGLE_LEFT_CUTOUTS: CutoutId[] = [ + 'cutoutA1', + 'cutoutB1', + 'cutoutC1', + 'cutoutD1', +] + +export const SINGLE_CENTER_CUTOUTS: CutoutId[] = [ + 'cutoutA2', + 'cutoutB2', + 'cutoutC2', + 'cutoutD2', +] + +export const SINGLE_RIGHT_CUTOUTS: CutoutId[] = [ + 'cutoutA3', + 'cutoutB3', + 'cutoutC3', + 'cutoutD3', +] + +export const STAGING_AREA_CUTOUTS: CutoutId[] = [ 'cutoutA3', 'cutoutB3', 'cutoutC3', 'cutoutD3', ] -export const WASTE_CHUTE_CUTOUT: 'cutoutD3' = 'cutoutD3' -export const STAGING_AREA_LOAD_NAME = 'stagingArea' -export const STANDARD_SLOT_LOAD_NAME = 'standardSlot' -export const TRASH_BIN_LOAD_NAME = 'trashBin' -export const WASTE_CHUTE_LOAD_NAME = 'wasteChute' +export const WASTE_CHUTE_CUTOUT: 'cutoutD3' = 'cutoutD3' export const A1_ADDRESSABLE_AREA: 'A1' = 'A1' export const A2_ADDRESSABLE_AREA: 'A2' = 'A2' @@ -238,7 +255,7 @@ export const NINETY_SIX_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA: '96ChannelWasteChu export const GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA: 'gripperWasteChute' = 'gripperWasteChute' -export const FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS = [ +export const FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS: AddressableAreaName[] = [ A1_ADDRESSABLE_AREA, A2_ADDRESSABLE_AREA, A3_ADDRESSABLE_AREA, @@ -253,7 +270,7 @@ export const FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS = [ D3_ADDRESSABLE_AREA, ] -export const FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS = [ +export const FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS: AddressableAreaName[] = [ A4_ADDRESSABLE_AREA, B4_ADDRESSABLE_AREA, C4_ADDRESSABLE_AREA, @@ -295,13 +312,13 @@ export const STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE: ' export const STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE: 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' = 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' -export const SINGLE_SLOT_FIXTURES = [ +export const SINGLE_SLOT_FIXTURES: CutoutFixtureId[] = [ SINGLE_LEFT_SLOT_FIXTURE, SINGLE_CENTER_SLOT_FIXTURE, SINGLE_RIGHT_SLOT_FIXTURE, ] -export const WASTE_CHUTE_FIXTURES = [ +export const WASTE_CHUTE_FIXTURES: CutoutFixtureId[] = [ WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, diff --git a/shared-data/js/fixtures.ts b/shared-data/js/fixtures.ts index 39ae5b9fe8d..f5549408af7 100644 --- a/shared-data/js/fixtures.ts +++ b/shared-data/js/fixtures.ts @@ -1,15 +1,18 @@ import { FLEX_ROBOT_TYPE, - STAGING_AREA_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, } from './constants' +import type { CutoutFixtureId } from '../deck' import type { AddressableArea, CoordinateTuple, Cutout, DeckDefinition, - FixtureLoadName, OT2Cutout, } from './types' @@ -93,13 +96,27 @@ export function getAddressableAreaFromSlotId( ) } -export function getFixtureDisplayName(loadName: FixtureLoadName): string { - if (loadName === STAGING_AREA_LOAD_NAME) { - return 'Staging Area Slot' - } else if (loadName === TRASH_BIN_LOAD_NAME) { - return 'Trash Bin' - } else if (loadName === WASTE_CHUTE_LOAD_NAME) { - return 'Waste Chute' +export function getFixtureDisplayName( + cutoutFixtureId: CutoutFixtureId | null +): string { + if (cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE) { + return 'Staging area slot' + } else if (cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE) { + return 'Trash bin' + } else if (cutoutFixtureId === WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE) { + return 'Waste chute only' + } else if (cutoutFixtureId === WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE) { + return 'Waste chute only covered' + } else if ( + cutoutFixtureId === + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE + ) { + return 'Waste chute with staging area slot' + } else if ( + cutoutFixtureId === + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE + ) { + return 'Waste chute with staging area slot covered' } else { return 'Slot' } diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 79e84d2af33..c295bd8977a 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -24,10 +24,6 @@ import { GRIPPER_V1_2, EXTENSION, MAGNETIC_BLOCK_V1, - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, } from './constants' import type { INode } from 'svgson' import type { RunTimeCommand, LabwareLocation } from '../command/types' @@ -232,18 +228,6 @@ export type ModuleModelWithLegacy = | typeof MAGDECK | typeof TEMPDECK -export type FixtureLoadName = - | typeof STAGING_AREA_LOAD_NAME - | typeof STANDARD_SLOT_LOAD_NAME - | typeof TRASH_BIN_LOAD_NAME - | typeof WASTE_CHUTE_LOAD_NAME - -export interface DeckOffset { - x: number - y: number - z: number -} - export interface Dimensions { xDimension: number yDimension: number @@ -254,13 +238,6 @@ export interface DeckRobot { model: RobotType } -export interface DeckFixture { - id: string - slot: string - labware: string - displayName: string -} - export type CoordinateTuple = [number, number, number] export type UnitDirection = 1 | -1 @@ -307,29 +284,11 @@ export interface AddressableArea { matingSurfaceUnitVector?: UnitVectorTuple } -export interface DeckLocations { - orderedSlots: DeckSlot[] - calibrationPoints: DeckCalibrationPoint[] - fixtures: DeckFixture[] - addressableAreas: AddressableArea[] -} - export interface DeckMetadata { displayName: string tags: string[] } -export interface DeckDefinitionV3 { - otId: string - cornerOffsetFromOrigin: CoordinateTuple - dimensions: CoordinateTuple - robot: DeckRobot - cutoutFixtures: CutoutFixture[] - locations: DeckLocations - metadata: DeckMetadata - layers: INode[] -} - export interface DeckCutout { id: string position: CoordinateTuple @@ -338,13 +297,12 @@ export interface DeckCutout { export interface LegacyFixture { id: string - // TODO: is this cutout location? slot: string labware: string displayName: string } -export interface DeckLocationsV4 { +export interface DeckLocations { addressableAreas: AddressableArea[] calibrationPoints: DeckCalibrationPoint[] cutouts: DeckCutout[] @@ -356,7 +314,7 @@ export interface DeckDefinition { cornerOffsetFromOrigin: CoordinateTuple dimensions: CoordinateTuple robot: DeckRobot - locations: DeckLocationsV4 + locations: DeckLocations metadata: DeckMetadata cutoutFixtures: CutoutFixture[] } @@ -633,10 +591,9 @@ export type FlexSlot = | 'C4' | 'D4' -export interface Fixture { - fixtureId: string - fixtureLocation: Cutout - loadName: FixtureLoadName +export interface CutoutConfig { + cutoutId: CutoutId + cutoutFixtureId: CutoutFixtureId | null } -export type DeckConfiguration = Fixture[] +export type DeckConfiguration = CutoutConfig[] diff --git a/shared-data/protocol/types/schemaV7/command/setup.ts b/shared-data/protocol/types/schemaV7/command/setup.ts index f0d3ff0b0da..e6048ef58c9 100644 --- a/shared-data/protocol/types/schemaV7/command/setup.ts +++ b/shared-data/protocol/types/schemaV7/command/setup.ts @@ -5,7 +5,6 @@ import type { LabwareOffset, PipetteName, ModuleModel, - FixtureLoadName, Cutout, } from '../../../../js' @@ -154,6 +153,6 @@ interface LoadLiquidResult { interface LoadFixtureParams { location: { cutout: Cutout } - loadName: FixtureLoadName + loadName: string fixtureId?: string } From 1b3d68027a5a769d11689166fde06f16983bab24 Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Wed, 15 Nov 2023 10:59:48 -0500 Subject: [PATCH 12/46] feat(shared-data, app): add configureNozzleLayout command type in run log (#13961) * feat(shared-data, app): add configureNozzleLayout command type and run log support --- .../en/protocol_command_text.json | 3 +- .../CommandText/PipettingCommandText.tsx | 16 ++++++- .../__tests__/CommandText.test.tsx | 2 +- app/src/organisms/CommandText/index.tsx | 19 ++++++++ .../CommandText/utils/getWellRange.ts | 46 +++++++++++++++++++ app/src/organisms/CommandText/utils/index.ts | 1 + shared-data/command/types/setup.ts | 37 +++++++++++++++ 7 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 app/src/organisms/CommandText/utils/getWellRange.ts diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index 8d84b9e341f..fddc61a345e 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -6,6 +6,7 @@ "closing_tc_lid": "Closing Thermocycler lid", "comment": "Comment", "configure_for_volume": "Configure {{pipette}} to aspirate {{volume}} µL", + "configure_nozzle_layout": "Configure {{pipette}} to use {{amount}} nozzles", "confirm_and_resume": "Confirm and resume", "deactivate_hs_shake": "Deactivating shaker", "deactivate_temperature_module": "Deactivating Temperature Module", @@ -37,7 +38,7 @@ "opening_tc_lid": "Opening Thermocycler lid", "pause_on": "Pause on {{robot_name}}", "pause": "Pause", - "pickup_tip": "Picking up tip from {{well_name}} of {{labware}} in {{labware_location}}", + "pickup_tip": "Picking up tip(s) from {{well_range}} of {{labware}} in {{labware_location}}", "prepare_to_aspirate": "Preparing {{pipette}} to aspirate", "return_tip": "Returning tip to {{well_name}} of {{labware}} in {{labware_location}}", "save_position": "Saving position", diff --git a/app/src/organisms/CommandText/PipettingCommandText.tsx b/app/src/organisms/CommandText/PipettingCommandText.tsx index 8aff25b4da3..7285195c994 100644 --- a/app/src/organisms/CommandText/PipettingCommandText.tsx +++ b/app/src/organisms/CommandText/PipettingCommandText.tsx @@ -16,7 +16,9 @@ import { getLabwareName, getLabwareDisplayLocation, getFinalLabwareLocation, + getWellRange, } from './utils' +import type { PipetteName } from '@opentrons/shared-data' type PipettingRunTimeCommmand = | AspirateRunTimeCommand @@ -122,8 +124,20 @@ export const PipettingCommandText = ({ }) } case 'pickUpTip': { + const pipetteId = command.params.pipetteId + const pipetteName: + | PipetteName + | undefined = robotSideAnalysis.pipettes.find( + pip => pip.id === pipetteId + )?.pipetteName + return t('pickup_tip', { - well_name: wellName, + well_range: getWellRange( + pipetteId, + allPreviousCommands, + wellName, + pipetteName + ), labware: getLabwareName(robotSideAnalysis, labwareId), labware_location: displayLocation, }) diff --git a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx index 28ef08f940f..0ce3a0e6ded 100644 --- a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx +++ b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx @@ -219,7 +219,7 @@ describe('CommandText', () => { { i18nInstance: i18n } )[0] getByText( - 'Picking up tip from A1 of Opentrons 96 Tip Rack 300 µL in Slot 9' + 'Picking up tip(s) from A1 of Opentrons 96 Tip Rack 300 µL in Slot 9' ) } }) diff --git a/app/src/organisms/CommandText/index.tsx b/app/src/organisms/CommandText/index.tsx index 67e76526ab1..8ce1605f5fa 100644 --- a/app/src/organisms/CommandText/index.tsx +++ b/app/src/organisms/CommandText/index.tsx @@ -165,6 +165,25 @@ export function CommandText(props: Props): JSX.Element | null { ) } + case 'configureNozzleLayout': { + const { configuration_params, pipetteId } = command.params + const pipetteName = robotSideAnalysis.pipettes.find( + pip => pip.id === pipetteId + )?.pipetteName + + // TODO (sb, 11/9/23): Add support for other configurations when needed + return ( + + {t('configure_nozzle_layout', { + amount: configuration_params.style === 'COLUMN' ? '8' : 'all', + pipette: + pipetteName != null + ? getPipetteNameSpecs(pipetteName)?.displayName + : '', + })} + + ) + } case 'prepareToAspirate': { const { pipetteId } = command.params const pipetteName = robotSideAnalysis.pipettes.find( diff --git a/app/src/organisms/CommandText/utils/getWellRange.ts b/app/src/organisms/CommandText/utils/getWellRange.ts new file mode 100644 index 00000000000..8baa6c0b709 --- /dev/null +++ b/app/src/organisms/CommandText/utils/getWellRange.ts @@ -0,0 +1,46 @@ +import { + getPipetteNameSpecs, + PipetteName, + RunTimeCommand, +} from '@opentrons/shared-data' + +/** + * @param pipetteName name of pipette being used + * @param commands list of commands to search within + * @param wellName the target well for pickup tip + * @returns WellRange string of wells pipette will pickup tips from + */ +export function getWellRange( + pipetteId: string, + commands: RunTimeCommand[], + wellName: string, + pipetteName?: PipetteName +): string { + const pipetteChannels = pipetteName + ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 + : 1 + let usedChannels = pipetteChannels + if (pipetteChannels === 96) { + for (const c of commands.reverse()) { + if ( + c.commandType === 'configureNozzleLayout' && + c.params?.pipetteId === pipetteId + ) { + // TODO(sb, 11/9/23): add support for quadrant and row configurations when needed + if (c.params.configuration_params.style === 'SINGLE') { + usedChannels = 1 + } else if (c.params.configuration_params.style === 'COLUMN') { + usedChannels = 8 + } + break + } + } + } + if (usedChannels === 96) { + return 'A1 - H12' + } else if (usedChannels === 8) { + const column = wellName.substr(1) + return `A${column} - H${column}` + } + return wellName +} diff --git a/app/src/organisms/CommandText/utils/index.ts b/app/src/organisms/CommandText/utils/index.ts index 0b7a5c24124..5435a292d11 100644 --- a/app/src/organisms/CommandText/utils/index.ts +++ b/app/src/organisms/CommandText/utils/index.ts @@ -5,3 +5,4 @@ export * from './getModuleDisplayLocation' export * from './getLiquidDisplayName' export * from './getLabwareDisplayLocation' export * from './getFinalLabwareLocation' +export * from './getWellRange' diff --git a/shared-data/command/types/setup.ts b/shared-data/command/types/setup.ts index f8fb78752e5..e5b3184241d 100644 --- a/shared-data/command/types/setup.ts +++ b/shared-data/command/types/setup.ts @@ -71,7 +71,20 @@ export interface LoadFixtureRunTimeCommand result?: LoadLabwareResult } +export interface ConfigureNozzleLayoutCreateCommand + extends CommonCommandCreateInfo { + commandType: 'configureNozzleLayout' + params: ConfigureNozzleLayoutParams +} + +export interface ConfigureNozzleLayoutRunTimeCommand + extends CommonCommandRunTimeInfo, + ConfigureNozzleLayoutCreateCommand { + result?: {} +} + export type SetupRunTimeCommand = + | ConfigureNozzleLayoutRunTimeCommand | LoadPipetteRunTimeCommand | LoadLabwareRunTimeCommand | LoadFixtureRunTimeCommand @@ -80,6 +93,7 @@ export type SetupRunTimeCommand = | MoveLabwareRunTimeCommand export type SetupCreateCommand = + | ConfigureNozzleLayoutCreateCommand | LoadPipetteCreateCommand | LoadLabwareCreateCommand | LoadFixtureCreateCommand @@ -161,3 +175,26 @@ interface LoadFixtureParams { loadName: FixtureLoadName fixtureId?: string } + +const COLUMN = 'COLUMN' +const SINGLE = 'SINGLE' +const ROW = 'ROW' +const QUADRANT = 'QUADRANT' +const EMPTY = 'EMPTY' + +export type NozzleConfigurationStyle = + | typeof COLUMN + | typeof SINGLE + | typeof ROW + | typeof QUADRANT + | typeof EMPTY + +interface NozzleConfigurationParams { + primary_nozzle: string + style: NozzleConfigurationStyle +} + +interface ConfigureNozzleLayoutParams { + pipetteId: string + configuration_params: NozzleConfigurationParams +} From 53dbd31efee9aa7e0155f76c5693d5a8196ba2c8 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 15 Nov 2023 12:37:39 -0500 Subject: [PATCH 13/46] fix(app): do not require probe presence on 96 (#13986) The 96 channel pipette is not capable of continuously checking for the presence of a tip (or indeed a calibration probe). It needs to run a special routing, exposed through ot3api.get_tip_presence_status() or ot3api.verify_tip_presence(), to check for a tip. This happens automatically in most of the places we use tips - aka inside other protocol engine commands - but it does not happen automatically and all the time. This is usually fine, but when we're doing calibration we do it through a protocol engine maintenance run where the way we interact with the robot is dispatching PE commands - and we don't ever dispatch a PE command that implicitly causes a tip check. That means that the 96 can't be relied on to get the presence of a tip. The long term fix for this is to add a checkTipPresence command to the engine, at least as a maintenance command; in the short term, we can disable the presence check for 96 channel pipettes to unblock testing. Closes RQA-1892 --- app/src/organisms/LabwarePositionCheck/AttachProbe.tsx | 3 ++- app/src/organisms/PipetteWizardFlows/AttachProbe.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx index f54032e48f1..48f9353147e 100644 --- a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx @@ -78,6 +78,7 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { (instrument): instrument is PipetteData => instrument.ok && instrument.mount === pipetteMount ) + const is96Channel = attachedPipette?.data.channels === 96 React.useEffect(() => { // move into correct position for probe attach on mount @@ -103,7 +104,7 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { setIsPending(true) refetch() .then(() => { - if (attachedPipette?.state?.tipDetected) { + if (is96Channel || attachedPipette?.state?.tipDetected) { chainRunCommands( [ { commandType: 'home', params: { axes: [pipetteZMotorAxis] } }, diff --git a/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx b/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx index f12c7065661..388b6420fae 100644 --- a/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx +++ b/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx @@ -81,7 +81,7 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { setIsPending(true) refetch() .then(() => { - if (attachedPipette?.state?.tipDetected) { + if (is96Channel || attachedPipette?.state?.tipDetected) { chainRunCommands?.( [ { From a6229994a04d89bf288b9005f351d143ea751759 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Wed, 15 Nov 2023 13:46:42 -0500 Subject: [PATCH 14/46] feat(hardware-testing): Autofind hw testing devices (#13975) * first-pass scale finding code * asair finding * pressure sensor auto finding * format/lint fixes on scale * fixups from testing * raise errors when sensors aren't found --- .../hardware_testing/drivers/asair_sensor.py | 27 ++++++++++--- .../drivers/pressure_fixture.py | 39 +++++++++++++++++++ .../gravimetric/measurement/scale.py | 18 +++++++++ .../pipette_assembly_qc_ot3/__main__.py | 24 +++++------- .../production_qc/tip_iqc_ot3.py | 18 ++++----- 5 files changed, 95 insertions(+), 31 deletions(-) diff --git a/hardware-testing/hardware_testing/drivers/asair_sensor.py b/hardware-testing/hardware_testing/drivers/asair_sensor.py index eb9a360eace..4e30c743045 100644 --- a/hardware-testing/hardware_testing/drivers/asair_sensor.py +++ b/hardware-testing/hardware_testing/drivers/asair_sensor.py @@ -16,6 +16,8 @@ from serial.serialutil import SerialException # type: ignore[import] from hardware_testing.data import ui +from serial.tools.list_ports import comports # type: ignore[import] + log = logging.getLogger(__name__) USB_VID = 0x0403 @@ -72,17 +74,32 @@ def get_serial(self) -> str: ... -def BuildAsairSensor(simulate: bool) -> AsairSensorBase: +def BuildAsairSensor(simulate: bool, autosearch: bool = True) -> AsairSensorBase: """Try to find and return an Asair sensor, if not found return a simulator.""" ui.print_title("Connecting to Environmental sensor") if not simulate: - port = list_ports_and_select(device_name="Asair environmental sensor") - try: + if not autosearch: + port = list_ports_and_select(device_name="Asair environmental sensor") sensor = AsairSensor.connect(port) ui.print_info(f"Found sensor on port {port}") return sensor - except SerialException: - pass + else: + ports = comports() + assert ports + for _port in ports: + port = _port.device # type: ignore[attr-defined] + try: + ui.print_info(f"Trying to connect to env sensor on port {port}") + sensor = AsairSensor.connect(port) + ser_id = sensor.get_serial() + ui.print_info(f"Found env sensor {ser_id} on port {port}") + return sensor + except: # noqa: E722 + pass + use_sim = ui.get_user_answer("No env sensor found, use simulator?") + if not use_sim: + raise SerialException("No sensor found") + ui.print_info("no sensor found returning simulator") return SimAsairSensor() diff --git a/hardware-testing/hardware_testing/drivers/pressure_fixture.py b/hardware-testing/hardware_testing/drivers/pressure_fixture.py index 82b5ccbb2d3..7743a433534 100644 --- a/hardware-testing/hardware_testing/drivers/pressure_fixture.py +++ b/hardware-testing/hardware_testing/drivers/pressure_fixture.py @@ -6,8 +6,13 @@ from typing import List, Tuple from typing_extensions import Final, Literal +from hardware_testing.data import ui from opentrons.types import Point +from serial.tools.list_ports import comports # type: ignore[import] +from serial import SerialException +from hardware_testing.drivers import list_ports_and_select + FIXTURE_REBOOT_TIME = 2 FIXTURE_NUM_CHANNELS: Final[int] = 8 FIXTURE_BAUD_RATE: Final[int] = 115200 @@ -98,6 +103,40 @@ def read_all_pressure_channel(self) -> List[float]: return pressure +def connect_to_fixture( + simulate: bool, side: str = "left", autosearch: bool = True +) -> PressureFixtureBase: + """Try to find and return an presure fixture, if not found return a simulator.""" + ui.print_title("Connecting to presure fixture") + if not simulate: + if not autosearch: + port = list_ports_and_select(device_name="Pressure fixture") + fixture = PressureFixture.create(port=port, slot_side=side) + fixture.connect() + ui.print_info(f"Found fixture on port {port}") + return fixture + else: + ports = comports() + assert ports + for _port in ports: + port = _port.device # type: ignore[attr-defined] + try: + ui.print_info( + f"Trying to connect to Pressure fixture on port {port}" + ) + fixture = PressureFixture.create(port=port, slot_side=side) + fixture.connect() + ui.print_info(f"Found fixture on port {port}") + return fixture + except: # noqa: E722 + pass + use_sim = ui.get_user_answer("No pressure sensor found, use simulator?") + if not use_sim: + raise SerialException("No sensor found") + ui.print_info("no fixture found returning simulator") + return SimPressureFixture() + + class PressureFixture(PressureFixtureBase): """OT3 Pressure Fixture Driver.""" diff --git a/hardware-testing/hardware_testing/gravimetric/measurement/scale.py b/hardware-testing/hardware_testing/gravimetric/measurement/scale.py index e194a9a42b5..8514590d1b0 100644 --- a/hardware-testing/hardware_testing/gravimetric/measurement/scale.py +++ b/hardware-testing/hardware_testing/gravimetric/measurement/scale.py @@ -15,6 +15,9 @@ RadwagAmbiant, ) +from hardware_testing.data import ui +from serial.tools.list_ports import comports # type: ignore[import] + @dataclass class ScaleConfig: @@ -67,6 +70,21 @@ def build(cls, simulate: bool) -> "Scale": @classmethod def find_port(cls) -> str: """Find port.""" + ports = comports() + assert ports + for port in ports: + try: + ui.print_info(f"Checking port {port.device} for scale") + radwag = Scale(scale=RadwagScale.create(port.device)) + radwag.connect() + radwag.initialize() + scale_serial = radwag.read_serial_number() + radwag.disconnect() + ui.print_info(f"found scale {scale_serial} on port {port.device}") + return port.device + except: # noqa: E722 + pass + ui.print_info("Unable to find the scale: please connect") return list_ports_and_select(device_name="scale") @property diff --git a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py index 656a8387456..f9f60173eed 100644 --- a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py +++ b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py @@ -34,10 +34,9 @@ ) from hardware_testing import data -from hardware_testing.drivers import list_ports_and_select from hardware_testing.drivers.pressure_fixture import ( - PressureFixture, - SimPressureFixture, + PressureFixtureBase, + connect_to_fixture, ) from .pressure import ( # type: ignore[import] PRESSURE_FIXTURE_TIP_VOLUME, @@ -468,15 +467,10 @@ async def _aspirate_and_look_for_droplets( return leak_test_passed -def _connect_to_fixture(test_config: TestConfig) -> PressureFixture: - if not test_config.simulate and not test_config.skip_fixture: - if not test_config.fixture_port: - _port = list_ports_and_select("pressure-fixture") - else: - _port = "" - fixture = PressureFixture.create(port=_port, slot_side=test_config.fixture_side) - else: - fixture = SimPressureFixture() # type: ignore[assignment] +def _connect_to_fixture(test_config: TestConfig) -> PressureFixtureBase: + fixture = connect_to_fixture( + test_config.simulate or test_config.skip_fixture, side=test_config.fixture_side + ) fixture.connect() return fixture @@ -485,7 +479,7 @@ async def _read_pressure_and_check_results( api: OT3API, pipette_channels: int, pipette_volume: int, - fixture: PressureFixture, + fixture: PressureFixtureBase, tag: PressureEvent, write_cb: Callable, accumulate_raw_data_cb: Callable, @@ -599,7 +593,7 @@ async def _fixture_check_pressure( api: OT3API, mount: OT3Mount, test_config: TestConfig, - fixture: PressureFixture, + fixture: PressureFixtureBase, write_cb: Callable, accumulate_raw_data_cb: Callable, ) -> bool: @@ -694,7 +688,7 @@ async def _test_for_leak( mount: OT3Mount, test_config: TestConfig, tip_volume: int, - fixture: Optional[PressureFixture], + fixture: Optional[PressureFixtureBase], write_cb: Optional[Callable], accumulate_raw_data_cb: Optional[Callable], droplet_wait_seconds: int = 30, diff --git a/hardware-testing/hardware_testing/production_qc/tip_iqc_ot3.py b/hardware-testing/hardware_testing/production_qc/tip_iqc_ot3.py index 65f11196534..042443c4d32 100644 --- a/hardware-testing/hardware_testing/production_qc/tip_iqc_ot3.py +++ b/hardware-testing/hardware_testing/production_qc/tip_iqc_ot3.py @@ -1,13 +1,13 @@ """Tip IQC OT3.""" from asyncio import run, sleep -from typing import List, Union, Optional +from typing import List, Optional from opentrons.hardware_control.ot3api import OT3API -from hardware_testing.drivers import list_ports_and_select from hardware_testing.drivers.pressure_fixture import ( - PressureFixture, + PressureFixtureBase, SimPressureFixture, + connect_to_fixture, ) from hardware_testing.data.csv_report import CSVReport, CSVSection, CSVLine @@ -44,18 +44,14 @@ async def _find_position(api: OT3API, mount: OT3Mount, nominal: Point) -> Point: return await api.gantry_position(mount) -def _connect_to_fixture(simulate: bool) -> PressureFixture: - if not simulate: - _port = list_ports_and_select("pressure-fixture") - fixture = PressureFixture.create(port=_port, slot_side="left") - else: - fixture = SimPressureFixture() # type: ignore[assignment] +def _connect_to_fixture(simulate: bool) -> PressureFixtureBase: + fixture = connect_to_fixture(simulate) # type: ignore[assignment] fixture.connect() return fixture async def _read_pressure_data( - fixture: Union[PressureFixture, SimPressureFixture], + fixture: PressureFixtureBase, num_samples: int, interval: float = DEFAULT_PRESSURE_FIXTURE_READ_INTERVAL_SECONDS, ) -> List[float]: @@ -73,7 +69,7 @@ async def _read_and_store_pressure_data( report: CSVReport, tip: str, section: str, - fixture: Union[PressureFixture, SimPressureFixture], + fixture: PressureFixtureBase, ) -> None: num_samples = TEST_SECTIONS[section.lower()] data_hover = await _read_pressure_data(fixture, num_samples) From 75570a18a62f2ba3b36dc7bc9d99d6dfce6af224 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Wed, 15 Nov 2023 13:51:21 -0500 Subject: [PATCH 15/46] turn the bot flashing yellow during the blank trials and blue during regular trials (#13954) --- hardware-testing/hardware_testing/gravimetric/execute.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hardware-testing/hardware_testing/gravimetric/execute.py b/hardware-testing/hardware_testing/gravimetric/execute.py index d1a72f4e4c9..cf2b8fb1ecc 100644 --- a/hardware-testing/hardware_testing/gravimetric/execute.py +++ b/hardware-testing/hardware_testing/gravimetric/execute.py @@ -52,6 +52,8 @@ from .tips import MULTI_CHANNEL_TEST_ORDER import glob +from opentrons.hardware_control.types import StatusBarState + _MEASUREMENTS: List[Tuple[str, MeasurementData]] = list() _PREV_TRIAL_GRAMS: Optional[MeasurementData] = None @@ -593,7 +595,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq recorder._recording = GravimetricRecording() report.store_config_gm(resources.test_report, cfg) calibration_tip_in_use = True - + hw_api = resources.ctx._core.get_hardware() if resources.ctx.is_simulating(): _PREV_TRIAL_GRAMS = None _MEASUREMENTS = list() @@ -621,6 +623,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq average_aspirate_evaporation_ul = 0.0 average_dispense_evaporation_ul = 0.0 else: + hw_api.set_status_bar_state(StatusBarState.SOFTWARE_ERROR) ( average_aspirate_evaporation_ul, average_dispense_evaporation_ul, @@ -632,7 +635,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq resources.test_report, labware_on_scale, ) - + hw_api.set_status_bar_state(StatusBarState.IDLE) ui.print_info("dropping tip") if not cfg.same_tip: _drop_tip( From 9b1f62a7ad9edcf76e20c2595d143f98e2a7c242 Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Wed, 15 Nov 2023 13:53:15 -0500 Subject: [PATCH 16/46] feat(engine): load and move labware on, to and from addressable areas in PE (#13968) allows addressable areas to be used in the engine by loadLabware and moveLabware --- api/src/opentrons/execute.py | 2 + .../protocol_api/core/engine/deck_conflict.py | 11 + .../protocol_api/core/engine/protocol.py | 6 +- .../protocol_api/core/engine/stringify.py | 5 + api/src/opentrons/protocol_engine/__init__.py | 2 + .../protocol_engine/commands/__init__.py | 14 + .../commands/command_unions.py | 13 + .../protocol_engine/commands/load_labware.py | 15 +- .../protocol_engine/commands/move_labware.py | 16 +- .../commands/move_to_addressable_area.py | 74 ++++ .../protocol_engine/create_protocol_engine.py | 12 +- .../protocol_engine/errors/__init__.py | 10 + .../protocol_engine/errors/exceptions.py | 67 +++- .../resources/deck_configuration_provider.py | 175 ++++++--- .../resources/fixture_validation.py | 86 ++--- .../protocol_engine/slot_standardization.py | 15 +- .../state/addressable_areas.py | 339 ++++++++++++++++++ .../opentrons/protocol_engine/state/config.py | 5 + .../protocol_engine/state/geometry.py | 101 ++++-- .../protocol_engine/state/labware.py | 93 +---- .../opentrons/protocol_engine/state/state.py | 28 ++ api/src/opentrons/protocol_engine/types.py | 56 ++- .../create_simulating_runner.py | 1 + api/src/opentrons/simulate.py | 1 + api/tests/opentrons/conftest.py | 2 + .../core/engine/test_protocol_core.py | 4 +- .../test_deck_configuration_provider.py | 339 ++++++++++++++++++ .../protocol_engine/state/command_fixtures.py | 27 ++ .../state/test_addressable_area_store.py | 319 ++++++++++++++++ .../state/test_addressable_area_view.py | 250 +++++++++++++ .../state/test_geometry_view.py | 181 +++++++--- .../state/test_labware_view.py | 41 +-- shared-data/command/schemas/8.json | 86 +++++ .../deck/definitions/4/ot2_short_trash.json | 15 +- .../deck/definitions/4/ot2_standard.json | 15 +- .../protocol/models/shared_models.py | 1 + 36 files changed, 2091 insertions(+), 336 deletions(-) create mode 100644 api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py create mode 100644 api/src/opentrons/protocol_engine/state/addressable_areas.py create mode 100644 api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py create mode 100644 api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py create mode 100644 api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py index bd450db8086..4714a72a15b 100644 --- a/api/src/opentrons/execute.py +++ b/api/src/opentrons/execute.py @@ -653,6 +653,8 @@ def _get_protocol_engine_config() -> Config: # We deliberately omit ignore_pause=True because, in the current implementation of # opentrons.protocol_api.core.engine, that would incorrectly make # ProtocolContext.is_simulating() return True. + use_simulated_deck_config=True, + # TODO the above is not correct for this and it should use the robot's actual config ) diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py index 7314d8074cd..cd1c892c953 100644 --- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py +++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py @@ -10,6 +10,7 @@ DeckSlotLocation, ModuleLocation, OnLabwareLocation, + AddressableAreaLocation, OFF_DECK_LOCATION, ) from opentrons.protocol_engine.errors.exceptions import LabwareNotLoadedOnModuleError @@ -111,6 +112,16 @@ def _map_labware( ) -> Optional[Tuple[DeckSlotName, wrapped_deck_conflict.DeckItem]]: location_from_engine = engine_state.labware.get_location(labware_id=labware_id) + if isinstance(location_from_engine, AddressableAreaLocation): + # TODO need to deal with staging slots, which will raise the value error we are returning None with below + try: + deck_slot = DeckSlotName.from_primitive( + location_from_engine.addressableAreaName + ) + except ValueError: + return None + location_from_engine = DeckSlotLocation(slotName=deck_slot) + if isinstance(location_from_engine, DeckSlotLocation): # This labware is loaded directly into a deck slot. # Map it to a wrapped_deck_conflict.Labware. diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index d82edf8cee8..bd69753fd03 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -541,7 +541,7 @@ def get_deck_definition(self) -> DeckDefinitionV4: def get_slot_definition(self, slot: DeckSlotName) -> SlotDefV3: """Get the slot definition from the robot's deck.""" - return self._engine_client.state.labware.get_slot_definition(slot) + return self._engine_client.state.addressable_areas.get_slot_definition(slot) def _ensure_module_location( self, slot: DeckSlotName, module_type: ModuleType @@ -595,7 +595,9 @@ def get_labware_on_labware( def get_slot_center(self, slot_name: DeckSlotName) -> Point: """Get the absolute coordinate of a slot's center.""" - return self._engine_client.state.labware.get_slot_center_position(slot_name) + return self._engine_client.state.addressable_areas.get_addressable_area_center( + slot_name.id + ) def get_highest_z(self) -> float: """Get the highest Z point of all deck items.""" diff --git a/api/src/opentrons/protocol_api/core/engine/stringify.py b/api/src/opentrons/protocol_api/core/engine/stringify.py index fd4a90817cd..434dde1b08a 100644 --- a/api/src/opentrons/protocol_api/core/engine/stringify.py +++ b/api/src/opentrons/protocol_api/core/engine/stringify.py @@ -4,6 +4,7 @@ LabwareLocation, ModuleLocation, OnLabwareLocation, + AddressableAreaLocation, ) @@ -42,6 +43,10 @@ def _labware_location_string( labware_on_string = _labware_location_string(engine_client, labware_on) return f"{labware_name} on {labware_on_string}" + elif isinstance(location, AddressableAreaLocation): + # In practice this will always be a deck slot or staging slot + return f"slot {location.addressableAreaName}" + elif location == "offDeck": return "[off-deck]" diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py index 253e88dc33f..79e5129f093 100644 --- a/api/src/opentrons/protocol_engine/__init__.py +++ b/api/src/opentrons/protocol_engine/__init__.py @@ -35,6 +35,7 @@ DeckSlotLocation, ModuleLocation, OnLabwareLocation, + AddressableAreaLocation, OFF_DECK_LOCATION, Dimensions, EngineStatus, @@ -93,6 +94,7 @@ "DeckType", "ModuleLocation", "OnLabwareLocation", + "AddressableAreaLocation", "OFF_DECK_LOCATION", "Dimensions", "EngineStatus", diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py index 60c5e8350ea..70aa90b3c7a 100644 --- a/api/src/opentrons/protocol_engine/commands/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/__init__.py @@ -177,6 +177,14 @@ MoveToWellCommandType, ) +from .move_to_addressable_area import ( + MoveToAddressableArea, + MoveToAddressableAreaParams, + MoveToAddressableAreaCreate, + MoveToAddressableAreaResult, + MoveToAddressableAreaCommandType, +) + from .wait_for_resume import ( WaitForResume, WaitForResumeParams, @@ -404,6 +412,12 @@ "MoveToWellParams", "MoveToWellResult", "MoveToWellCommandType", + # move to addressable area command models + "MoveToAddressableArea", + "MoveToAddressableAreaParams", + "MoveToAddressableAreaCreate", + "MoveToAddressableAreaResult", + "MoveToAddressableAreaCommandType", # wait for resume command models "WaitForResume", "WaitForResumeParams", diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index 4387a9178ec..464ed80f374 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -154,6 +154,14 @@ MoveToWellCommandType, ) +from .move_to_addressable_area import ( + MoveToAddressableArea, + MoveToAddressableAreaParams, + MoveToAddressableAreaCreate, + MoveToAddressableAreaResult, + MoveToAddressableAreaCommandType, +) + from .wait_for_resume import ( WaitForResume, WaitForResumeParams, @@ -275,6 +283,7 @@ MoveRelative, MoveToCoordinates, MoveToWell, + MoveToAddressableArea, PrepareToAspirate, WaitForResume, WaitForDuration, @@ -333,6 +342,7 @@ MoveRelativeParams, MoveToCoordinatesParams, MoveToWellParams, + MoveToAddressableAreaParams, PrepareToAspirateParams, WaitForResumeParams, WaitForDurationParams, @@ -392,6 +402,7 @@ MoveRelativeCommandType, MoveToCoordinatesCommandType, MoveToWellCommandType, + MoveToAddressableAreaCommandType, PrepareToAspirateCommandType, WaitForResumeCommandType, WaitForDurationCommandType, @@ -450,6 +461,7 @@ MoveRelativeCreate, MoveToCoordinatesCreate, MoveToWellCreate, + MoveToAddressableAreaCreate, PrepareToAspirateCreate, WaitForResumeCreate, WaitForDurationCreate, @@ -508,6 +520,7 @@ MoveRelativeResult, MoveToCoordinatesResult, MoveToWellResult, + MoveToAddressableAreaResult, PrepareToAspirateResult, WaitForResumeResult, WaitForDurationResult, diff --git a/api/src/opentrons/protocol_engine/commands/load_labware.py b/api/src/opentrons/protocol_engine/commands/load_labware.py index 614c702df51..480537a5bb6 100644 --- a/api/src/opentrons/protocol_engine/commands/load_labware.py +++ b/api/src/opentrons/protocol_engine/commands/load_labware.py @@ -7,8 +7,13 @@ from opentrons_shared_data.labware.labware_definition import LabwareDefinition from ..errors import LabwareIsNotAllowedInLocationError -from ..resources import labware_validation -from ..types import LabwareLocation, OnLabwareLocation, DeckSlotLocation +from ..resources import labware_validation, fixture_validation +from ..types import ( + LabwareLocation, + OnLabwareLocation, + DeckSlotLocation, + AddressableAreaLocation, +) from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate @@ -105,6 +110,12 @@ async def execute(self, params: LoadLabwareParams) -> LoadLabwareResult: f"{params.loadName} is not allowed in slot {params.location.slotName}" ) + if isinstance(params.location, AddressableAreaLocation): + if not fixture_validation.is_deck_slot(params.location.addressableAreaName): + raise LabwareIsNotAllowedInLocationError( + f"Cannot load {params.loadName} onto addressable area {params.location.addressableAreaName}" + ) + loaded_labware = await self._equipment.load_labware( load_name=params.loadName, namespace=params.namespace, diff --git a/api/src/opentrons/protocol_engine/commands/move_labware.py b/api/src/opentrons/protocol_engine/commands/move_labware.py index 682f2a58a22..49bb9920153 100644 --- a/api/src/opentrons/protocol_engine/commands/move_labware.py +++ b/api/src/opentrons/protocol_engine/commands/move_labware.py @@ -8,12 +8,13 @@ from ..types import ( LabwareLocation, OnLabwareLocation, + AddressableAreaLocation, LabwareMovementStrategy, LabwareOffsetVector, LabwareMovementOffsetData, ) from ..errors import LabwareMovementNotAllowedError, NotSupportedOnRobotType -from ..resources import labware_validation +from ..resources import labware_validation, fixture_validation from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate if TYPE_CHECKING: @@ -83,7 +84,9 @@ def __init__( self._labware_movement = labware_movement self._run_control = run_control - async def execute(self, params: MoveLabwareParams) -> MoveLabwareResult: + async def execute( # noqa: C901 + self, params: MoveLabwareParams + ) -> MoveLabwareResult: """Move a loaded labware to a new location.""" # Allow propagation of LabwareNotLoadedError. current_labware = self._state_view.labware.get(labware_id=params.labwareId) @@ -97,6 +100,15 @@ async def execute(self, params: MoveLabwareParams) -> MoveLabwareResult: f"Cannot move fixed trash labware '{current_labware_definition.parameters.loadName}'." ) + if isinstance(params.newLocation, AddressableAreaLocation): + area_name = params.newLocation.addressableAreaName + if not fixture_validation.is_gripper_waste_chute( + area_name + ) and not fixture_validation.is_deck_slot(area_name): + raise LabwareMovementNotAllowedError( + f"Cannot move {current_labware.loadName} to addressable area {area_name}" + ) + available_new_location = self._state_view.geometry.ensure_location_not_occupied( location=params.newLocation ) diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py new file mode 100644 index 00000000000..883da9ff8fd --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py @@ -0,0 +1,74 @@ +"""Move to well command request, result, and implementation models.""" +from __future__ import annotations +from pydantic import Field +from typing import TYPE_CHECKING, Optional, Type +from typing_extensions import Literal + +# from ..types import DeckPoint +from .pipetting_common import ( + PipetteIdMixin, + MovementMixin, + DestinationPositionResult, +) +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate + +if TYPE_CHECKING: + from ..execution import MovementHandler + +MoveToAddressableAreaCommandType = Literal["moveToAddressableArea"] + + +class MoveToAddressableAreaParams(PipetteIdMixin, MovementMixin): + """Payload required to move a pipette to a specific addressable area.""" + + addressableAreaName: str = Field( + description=( + "The name of the addressable area that you want to use." + " Valid values are the `id`s of `addressableArea`s in the" + " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)." + ) + ) + + +class MoveToAddressableAreaResult(DestinationPositionResult): + """Result data from the execution of a MoveToAddressableArea command.""" + + pass + + +class MoveToAddressableAreaImplementation( + AbstractCommandImpl[MoveToAddressableAreaParams, MoveToAddressableAreaResult] +): + """Move to addressable area command implementation.""" + + def __init__(self, movement: MovementHandler, **kwargs: object) -> None: + self._movement = movement + + async def execute( + self, params: MoveToAddressableAreaParams + ) -> MoveToAddressableAreaResult: + """Move the requested pipette to the requested addressable area.""" + raise NotImplementedError() + + +class MoveToAddressableArea( + BaseCommand[MoveToAddressableAreaParams, MoveToAddressableAreaResult] +): + """Move to addressable area command model.""" + + commandType: MoveToAddressableAreaCommandType = "moveToAddressableArea" + params: MoveToAddressableAreaParams + result: Optional[MoveToAddressableAreaResult] + + _ImplementationCls: Type[ + MoveToAddressableAreaImplementation + ] = MoveToAddressableAreaImplementation + + +class MoveToAddressableAreaCreate(BaseCommandCreate[MoveToAddressableAreaParams]): + """Move to addressable area command creation request model.""" + + commandType: MoveToAddressableAreaCommandType = "moveToAddressableArea" + params: MoveToAddressableAreaParams + + _CommandCls: Type[MoveToAddressableArea] = MoveToAddressableArea diff --git a/api/src/opentrons/protocol_engine/create_protocol_engine.py b/api/src/opentrons/protocol_engine/create_protocol_engine.py index adb4657d2af..d6a10fada2b 100644 --- a/api/src/opentrons/protocol_engine/create_protocol_engine.py +++ b/api/src/opentrons/protocol_engine/create_protocol_engine.py @@ -12,18 +12,25 @@ from .state import Config, StateStore from .types import PostRunHardwareState +# TODO move this type to a better location +from .state.addressable_areas import DeckConfiguration + # TODO(mm, 2023-06-16): Arguably, this not being a context manager makes us prone to forgetting to # clean it up properly, especially in tests. See e.g. https://opentrons.atlassian.net/browse/RSS-222 async def create_protocol_engine( - hardware_api: HardwareControlAPI, config: Config, load_fixed_trash: bool = False + hardware_api: HardwareControlAPI, + config: Config, + load_fixed_trash: bool = False, + deck_configuration: typing.Optional[DeckConfiguration] = None, ) -> ProtocolEngine: """Create a ProtocolEngine instance. Arguments: hardware_api: Hardware control API to pass down to dependencies. config: ProtocolEngine configuration. - load_fixed_trash: Automatically load fixed trash labware in engine + load_fixed_trash: Automatically load fixed trash labware in engine. + deck_configuration: The initial deck configuration the engine will be instantiated with. """ deck_data = DeckDataProvider(config.deck_type) deck_definition = await deck_data.get_deck_definition() @@ -40,6 +47,7 @@ async def create_protocol_engine( deck_fixed_labware=deck_fixed_labware, is_door_open=hardware_api.door_state is DoorState.OPEN, module_calibration_offsets=module_calibration_offsets, + deck_configuration=deck_configuration, ) return ProtocolEngine(state_store=state_store, hardware_api=hardware_api) diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index 642d4ff6cd8..0e0a4148fec 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -27,7 +27,12 @@ ModuleNotOnDeckError, ModuleNotConnectedError, SlotDoesNotExistError, + CutoutDoesNotExistError, FixtureDoesNotExistError, + AddressableAreaDoesNotExistError, + FixtureDoesNotProvideAreasError, + AreaNotInDeckConfigurationError, + IncompatibleAddressableAreaError, FailedToPlanMoveError, MustHomeError, RunStoppedError, @@ -88,7 +93,12 @@ "ModuleNotOnDeckError", "ModuleNotConnectedError", "SlotDoesNotExistError", + "CutoutDoesNotExistError", "FixtureDoesNotExistError", + "AddressableAreaDoesNotExistError", + "FixtureDoesNotProvideAreasError", + "AreaNotInDeckConfigurationError", + "IncompatibleAddressableAreaError", "FailedToPlanMoveError", "MustHomeError", "RunStoppedError", diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 7f4304f8097..2527077f58d 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -373,8 +373,21 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) +class CutoutDoesNotExistError(ProtocolEngineError): + """Raised when referencing a cutout that does not exist.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a CutoutDoesNotExistError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + class FixtureDoesNotExistError(ProtocolEngineError): - """Raised when referencing an addressable area (aka fixture) that does not exist.""" + """Raised when referencing a cutout fixture that does not exist.""" def __init__( self, @@ -386,6 +399,58 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) +class AddressableAreaDoesNotExistError(ProtocolEngineError): + """Raised when referencing an addressable area that does not exist.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a AddressableAreaDoesNotExistError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class FixtureDoesNotProvideAreasError(ProtocolEngineError): + """Raised when a cutout fixture does not provide any addressable areas for a requested cutout.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a FixtureDoesNotProvideAreasError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class AreaNotInDeckConfigurationError(ProtocolEngineError): + """Raised when an addressable area is referenced that is not provided by a deck configuration.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a AreaNotInDeckConfigurationError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class IncompatibleAddressableAreaError(ProtocolEngineError): + """Raised when two non-compatible addressable areas are referenced during analysis.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a IncompatibleAddressableAreaError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + # TODO(mc, 2020-11-06): flesh out with structured data to replicate # existing LabwareHeightError class FailedToPlanMoveError(ProtocolEngineError): diff --git a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py index cc24a572a70..144f59f3e20 100644 --- a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py +++ b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py @@ -1,72 +1,139 @@ """Deck configuration resource provider.""" -from dataclasses import dataclass -from typing import List, Set, Dict +from typing import List, Set, Tuple, Optional -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, AddressableArea +from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, CutoutFixture +from opentrons.types import Point -from ..errors import FixtureDoesNotExistError +from ..types import ( + AddressableArea, + PotentialCutoutFixture, + DeckPoint, + Dimensions, + AddressableOffsetVector, +) +from ..errors import ( + CutoutDoesNotExistError, + FixtureDoesNotExistError, + AddressableAreaDoesNotExistError, + FixtureDoesNotProvideAreasError, +) -@dataclass(frozen=True) -class DeckCutoutFixture: - """Basic cutout fixture data class.""" +def get_cutout_position(cutout_id: str, deck_definition: DeckDefinitionV4) -> DeckPoint: + """Get the base position of a cutout on the deck.""" + for cutout in deck_definition["locations"]["cutouts"]: + if cutout_id == cutout["id"]: + position = cutout["position"] + return DeckPoint(x=position[0], y=position[1], z=position[2]) + else: + raise CutoutDoesNotExistError(f"Could not find cutout with name {cutout_id}") - name: str - # TODO(jbl 10-30-2023) this is in reference to the cutout ID that is supplied in mayMountTo in the definition. - # We might want to make this not a string. - cutout_slot_location: str +def get_cutout_fixture( + cutout_fixture_id: str, deck_definition: DeckDefinitionV4 +) -> CutoutFixture: + """Gets cutout fixture from deck that matches the cutout fixture ID provided.""" + for cutout_fixture in deck_definition["cutoutFixtures"]: + if cutout_fixture["id"] == cutout_fixture_id: + return cutout_fixture + raise FixtureDoesNotExistError( + f"Could not find cutout fixture with name {cutout_fixture_id}" + ) -class DeckConfigurationProvider: - """Provider class to ingest deck configuration data and retrieve relevant deck definition data.""" - _configuration: Dict[str, DeckCutoutFixture] +def get_provided_addressable_area_names( + cutout_fixture_id: str, cutout_id: str, deck_definition: DeckDefinitionV4 +) -> List[str]: + """Gets a list of the addressable areas provided by the cutout fixture on the cutout.""" + cutout_fixture = get_cutout_fixture(cutout_fixture_id, deck_definition) + try: + return cutout_fixture["providesAddressableAreas"][cutout_id] + except KeyError as exception: + raise FixtureDoesNotProvideAreasError( + f"Cutout fixture {cutout_fixture['id']} does not provide addressable areas for {cutout_id}" + ) from exception - def __init__( - self, - deck_definition: DeckDefinitionV4, - deck_configuration: List[DeckCutoutFixture], - ) -> None: - """Initialize a DeckDataProvider.""" - self._deck_definition = deck_definition - self._configuration = { - cutout_fixture.cutout_slot_location: cutout_fixture - for cutout_fixture in deck_configuration - } - def get_addressable_areas_for_cutout_fixture( - self, cutout_fixture_id: str, cutout_id: str - ) -> Set[str]: - """Get the allowable addressable areas for a cutout fixture loaded on a specific cutout slot.""" - for cutout_fixture in self._deck_definition["cutoutFixtures"]: - if cutout_fixture_id == cutout_fixture["id"]: - return set( - cutout_fixture["providesAddressableAreas"].get(cutout_id, []) +def get_potential_cutout_fixtures( + addressable_area_name: str, deck_definition: DeckDefinitionV4 +) -> Tuple[str, Set[PotentialCutoutFixture]]: + """Given an addressable area name, gets the cutout ID associated with it and a set of potential fixtures.""" + potential_fixtures = [] + for cutout_fixture in deck_definition["cutoutFixtures"]: + for cutout_id, provided_areas in cutout_fixture[ + "providesAddressableAreas" + ].items(): + if addressable_area_name in provided_areas: + potential_fixtures.append( + PotentialCutoutFixture( + cutout_id=cutout_id, + cutout_fixture_id=cutout_fixture["id"], + ) ) + # This following logic is making the assumption that every addressable area can only go on one cutout, though + # it may have multiple cutout fixtures that supply it on that cutout. If this assumption changes, some of the + # following logic will have to be readjusted + assert ( + potential_fixtures + ), f"No potential fixtures for addressable area {addressable_area_name}" + cutout_id = potential_fixtures[0].cutout_id + assert all(cutout_id == fixture.cutout_id for fixture in potential_fixtures) + return cutout_id, set(potential_fixtures) - raise FixtureDoesNotExistError( - f'Could not resolve "{cutout_fixture_id}" to a fixture.' - ) - def get_configured_addressable_areas(self) -> Set[str]: - """Get a list of all addressable areas the robot is configured for.""" - configured_addressable_areas = set() - for cutout_id, cutout_fixture in self._configuration.items(): - addressable_areas = self.get_addressable_areas_for_cutout_fixture( - cutout_fixture.name, cutout_id +def get_addressable_area_from_name( + addressable_area_name: str, + cutout_position: DeckPoint, + deck_definition: DeckDefinitionV4, +) -> AddressableArea: + """Given a name and a cutout position, get an addressable area on the deck.""" + for addressable_area in deck_definition["locations"]["addressableAreas"]: + if addressable_area["id"] == addressable_area_name: + area_offset = addressable_area["offsetFromCutoutFixture"] + position = AddressableOffsetVector( + x=area_offset[0] + cutout_position.x, + y=area_offset[1] + cutout_position.y, + z=area_offset[2] + cutout_position.z, ) - configured_addressable_areas.update(addressable_areas) - return configured_addressable_areas + bounding_box = Dimensions( + x=addressable_area["boundingBox"]["xDimension"], + y=addressable_area["boundingBox"]["yDimension"], + z=addressable_area["boundingBox"]["zDimension"], + ) + drop_tips_deck_offset = addressable_area.get("dropTipsOffset") + drop_tip_location: Optional[Point] + if drop_tips_deck_offset: + drop_tip_location = Point( + x=drop_tips_deck_offset[0] + cutout_position.x, + y=drop_tips_deck_offset[1] + cutout_position.y, + z=drop_tips_deck_offset[2] + cutout_position.z, + ) + else: + drop_tip_location = None - def get_addressable_area_definition( - self, addressable_area_name: str - ) -> AddressableArea: - """Get the addressable area definition from the relevant deck definition.""" - for addressable_area in self._deck_definition["locations"]["addressableAreas"]: - if addressable_area_name == addressable_area["id"]: - return addressable_area + drop_labware_deck_offset = addressable_area.get("dropLabwareOffset") + drop_labware_location: Optional[Point] + if drop_labware_deck_offset: + drop_labware_location = Point( + x=drop_labware_deck_offset[0] + cutout_position.x, + y=drop_labware_deck_offset[1] + cutout_position.y, + z=drop_labware_deck_offset[2] + cutout_position.z, + ) + else: + drop_labware_location = None - raise FixtureDoesNotExistError( - f'Could not resolve "{addressable_area_name}" to a fixture.' - ) + return AddressableArea( + area_name=addressable_area["id"], + display_name=addressable_area["displayName"], + bounding_box=bounding_box, + position=position, + compatible_module_types=addressable_area.get( + "compatibleModuleTypes", [] + ), + drop_tip_location=drop_tip_location, + drop_labware_location=drop_labware_location, + ) + raise AddressableAreaDoesNotExistError( + f"Could not find addressable area with name {addressable_area_name}" + ) diff --git a/api/src/opentrons/protocol_engine/resources/fixture_validation.py b/api/src/opentrons/protocol_engine/resources/fixture_validation.py index 3eed2f90b22..bee59415d11 100644 --- a/api/src/opentrons/protocol_engine/resources/fixture_validation.py +++ b/api/src/opentrons/protocol_engine/resources/fixture_validation.py @@ -1,69 +1,43 @@ -"""Validation file for fixtures and addressable area reference checking functions.""" +"""Validation file for addressable area reference checking functions.""" -from typing import List +from opentrons.types import DeckSlotName -from opentrons_shared_data.deck.deck_definitions import Locations, CutoutFixture -from opentrons.hardware_control.modules.types import ModuleModel, ModuleType +def is_waste_chute(addressable_area_name: str) -> bool: + """Check if an addressable area is a Waste Chute.""" + return addressable_area_name in { + "1and8ChannelWasteChute", + "96ChannelWasteChute", + "gripperWasteChute", + } -def validate_fixture_id(fixtureList: List[CutoutFixture], load_name: str) -> bool: - """Check that the loaded fixture has an existing definition.""" - for fixture in fixtureList: - if fixture.id == load_name: - return True - return False +def is_gripper_waste_chute(addressable_area_name: str) -> bool: + """Check if an addressable area is a gripper-movement-compatible Waste Chute.""" + return addressable_area_name == "gripperWasteChute" -def validate_fixture_location_is_allowed(fixture: CutoutFixture, location: str) -> bool: - """Validate that the fixture is allowed to load into the provided location according to the deck definitions.""" - return location in fixture.mayMountTo +def is_drop_tip_waste_chute(addressable_area_name: str) -> bool: + """Check if an addressable area is a Waste Chute compatible for dropping tips.""" + return addressable_area_name in {"1and8ChannelWasteChute", "96ChannelWasteChute"} -def validate_is_wastechute(load_name: str) -> bool: - """Check if a fixture is a Waste Chute.""" - return ( - load_name == "wasteChuteRightAdapterCovered" - or load_name == "wasteChuteRightAdapterNoCover" - or load_name == "stagingAreaSlotWithWasteChuteRightAdapterCovered" - or load_name == "stagingAreaSlotWithWasteChuteRightAdapterNoCover" - ) +def is_trash(addressable_area_name: str) -> bool: + """Check if an addressable area is a trash bin.""" + return addressable_area_name in {"movableTrash", "fixedTrash", "shortFixedTrash"} -def validate_module_is_compatible_with_fixture( - locations: Locations, fixture: CutoutFixture, module: ModuleModel -) -> bool: - """Validate that the fixture allows the loading of a specified module.""" - module_name = ModuleType.from_model(module).name - for key in fixture.providesAddressableAreas.keys(): - for area in fixture.providesAddressableAreas[key]: - for l_area in locations.addressableAreas: - if l_area.id == area: - if l_area.compatibleModuleTypes is None: - return False - elif module_name in l_area.compatibleModuleTypes: - return True - return False +def is_staging_slot(addressable_area_name: str) -> bool: + """Check if an addressable area is a staging area slot.""" + return addressable_area_name in {"A4", "B4", "C4", "D4"} -def validate_fixture_allows_drop_tip( - locations: Locations, fixture: CutoutFixture -) -> bool: - """Validate that the fixture allows tips to be dropped in it's addressable areas.""" - for key in fixture.providesAddressableAreas.keys(): - for area in fixture.providesAddressableAreas[key]: - for l_area in locations.addressableAreas: - if l_area.id == area and l_area.ableToDropTips: - return True - return False - -def validate_fixture_allows_drop_labware( - locations: Locations, fixture: CutoutFixture -) -> bool: - """Validate that the fixture allows labware to be dropped in it's addressable areas.""" - for key in fixture.providesAddressableAreas.keys(): - for area in fixture.providesAddressableAreas[key]: - for l_area in locations.addressableAreas: - if l_area.id == area and l_area.ableToDropLabware: - return True - return False +def is_deck_slot(addressable_area_name: str) -> bool: + """Check if an addressable area is a deck slot (including staging area slots).""" + if is_staging_slot(addressable_area_name): + return True + try: + DeckSlotName.from_primitive(addressable_area_name) + except ValueError: + return False + return True diff --git a/api/src/opentrons/protocol_engine/slot_standardization.py b/api/src/opentrons/protocol_engine/slot_standardization.py index 9b2e352393a..c4e733b3ca6 100644 --- a/api/src/opentrons/protocol_engine/slot_standardization.py +++ b/api/src/opentrons/protocol_engine/slot_standardization.py @@ -24,7 +24,7 @@ OFF_DECK_LOCATION, DeckSlotLocation, LabwareLocation, - NonStackedLocation, + AddressableAreaLocation, LabwareOffsetCreate, ModuleLocation, OnLabwareLocation, @@ -124,21 +124,14 @@ def _standardize_labware_location( if isinstance(original, DeckSlotLocation): return _standardize_deck_slot_location(original, robot_type) elif ( - isinstance(original, (ModuleLocation, OnLabwareLocation)) + isinstance( + original, (ModuleLocation, OnLabwareLocation, AddressableAreaLocation) + ) or original == OFF_DECK_LOCATION ): return original -def _standardize_adapter_location( - original: NonStackedLocation, robot_type: RobotType -) -> NonStackedLocation: - if isinstance(original, DeckSlotLocation): - return _standardize_deck_slot_location(original, robot_type) - elif isinstance(original, ModuleLocation) or original == OFF_DECK_LOCATION: - return original - - def _standardize_deck_slot_location( original: DeckSlotLocation, robot_type: RobotType ) -> DeckSlotLocation: diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py new file mode 100644 index 00000000000..674923b3485 --- /dev/null +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -0,0 +1,339 @@ +"""Basic addressable area data state and store.""" +from dataclasses import dataclass +from typing import Dict, Set, Union, List, Tuple + +from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 + +from opentrons.types import Point, DeckSlotName + +from ..commands import ( + Command, + LoadLabwareResult, + LoadModuleResult, + MoveLabwareResult, + MoveToAddressableAreaResult, +) +from ..errors import ( + IncompatibleAddressableAreaError, + AreaNotInDeckConfigurationError, + SlotDoesNotExistError, +) +from ..resources import deck_configuration_provider +from ..types import ( + DeckSlotLocation, + AddressableAreaLocation, + AddressableArea, + PotentialCutoutFixture, +) +from ..actions import Action, UpdateCommandAction +from .config import Config +from .abstract_store import HasState, HandlesActions + + +@dataclass +class AddressableAreaState: + """State of all loaded addressable area resources.""" + + loaded_addressable_areas_by_name: Dict[str, AddressableArea] + potential_cutout_fixtures_by_cutout_id: Dict[str, Set[PotentialCutoutFixture]] + deck_definition: DeckDefinitionV4 + use_simulated_deck_config: bool + + +def _get_conflicting_addressable_areas( + potential_cutout_fixtures: Set[PotentialCutoutFixture], + loaded_addressable_areas: Set[str], + deck_definition: DeckDefinitionV4, +) -> Set[str]: + loaded_areas_on_cutout = set() + for fixture in potential_cutout_fixtures: + loaded_areas_on_cutout.update( + deck_configuration_provider.get_provided_addressable_area_names( + fixture.cutout_fixture_id, + fixture.cutout_id, + deck_definition, + ) + ) + loaded_areas_on_cutout.intersection_update(loaded_addressable_areas) + return loaded_areas_on_cutout + + +# TODO make the below some sort of better type +DeckConfiguration = List[Tuple[str, str]] # cutout_id, cutout_fixture_id + + +class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions): + """Addressable area state container.""" + + _state: AddressableAreaState + + def __init__( + self, + deck_configuration: DeckConfiguration, + config: Config, + deck_definition: DeckDefinitionV4, + ) -> None: + """Initialize an addressable area store and its state.""" + if config.use_simulated_deck_config: + loaded_addressable_areas_by_name = {} + else: + addressable_areas = self._get_addressable_areas_from_deck_configuration( + deck_configuration, + deck_definition, + ) + loaded_addressable_areas_by_name = { + area.area_name: area for area in addressable_areas + } + + self._state = AddressableAreaState( + loaded_addressable_areas_by_name=loaded_addressable_areas_by_name, + potential_cutout_fixtures_by_cutout_id={}, + deck_definition=deck_definition, + use_simulated_deck_config=config.use_simulated_deck_config, + ) + + def handle_action(self, action: Action) -> None: + """Modify state in reaction to an action.""" + if isinstance(action, UpdateCommandAction): + self._handle_command(action.command) + # TODO have an action to reload the deck configuration. + + def _handle_command(self, command: Command) -> None: + """Modify state in reaction to a command.""" + if isinstance(command.result, LoadLabwareResult): + location = command.params.location + if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)): + self._check_location_is_addressable_area(location) + + elif isinstance(command.result, MoveLabwareResult): + location = command.params.newLocation + if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)): + self._check_location_is_addressable_area(location) + + elif isinstance(command.result, LoadModuleResult): + self._check_location_is_addressable_area(command.params.location) + + elif isinstance(command.result, MoveToAddressableAreaResult): + addressable_area_name = command.params.addressableAreaName + self._check_location_is_addressable_area(addressable_area_name) + + @staticmethod + def _get_addressable_areas_from_deck_configuration( + deck_config: DeckConfiguration, deck_definition: DeckDefinitionV4 + ) -> List[AddressableArea]: + """Load all provided addressable areas with a valid deck configuration.""" + # TODO uncomment once execute is hooked up with this properly + # assert ( + # len(deck_config) == 12 + # ), f"{len(deck_config)} cutout fixture ids provided." + addressable_areas = [] + for cutout_id, cutout_fixture_id in deck_config: + provided_addressable_areas = ( + deck_configuration_provider.get_provided_addressable_area_names( + cutout_fixture_id, cutout_id, deck_definition + ) + ) + cutout_position = deck_configuration_provider.get_cutout_position( + cutout_id, deck_definition + ) + for addressable_area_name in provided_addressable_areas: + addressable_areas.append( + deck_configuration_provider.get_addressable_area_from_name( + addressable_area_name, + cutout_position, + deck_definition, + ) + ) + return addressable_areas + + def _check_location_is_addressable_area( + self, location: Union[DeckSlotLocation, AddressableAreaLocation, str] + ) -> None: + if isinstance(location, DeckSlotLocation): + addressable_area_name = location.slotName.id + elif isinstance(location, AddressableAreaLocation): + addressable_area_name = location.addressableAreaName + else: + addressable_area_name = location + + if addressable_area_name not in self._state.loaded_addressable_areas_by_name: + # TODO Uncomment this out once robot server side stuff is hooked up + # if not self._state.use_simulated_deck_config: + # raise AreaNotInDeckConfigurationError( + # f"{addressable_area_name} not provided by deck configuration." + # ) + cutout_id = self._validate_addressable_area_for_simulation( + addressable_area_name + ) + cutout_position = deck_configuration_provider.get_cutout_position( + cutout_id, self._state.deck_definition + ) + addressable_area = ( + deck_configuration_provider.get_addressable_area_from_name( + addressable_area_name, cutout_position, self._state.deck_definition + ) + ) + self._state.loaded_addressable_areas_by_name[ + addressable_area.area_name + ] = addressable_area + + def _validate_addressable_area_for_simulation( + self, addressable_area_name: str + ) -> str: + """Given an addressable area name, validate it can exist on the deck and return cutout id associated with it.""" + ( + cutout_id, + potential_fixtures, + ) = deck_configuration_provider.get_potential_cutout_fixtures( + addressable_area_name, self._state.deck_definition + ) + + if cutout_id in self._state.potential_cutout_fixtures_by_cutout_id: + existing_potential_fixtures = ( + self._state.potential_cutout_fixtures_by_cutout_id[cutout_id] + ) + remaining_fixtures = existing_potential_fixtures.intersection( + set(potential_fixtures) + ) + if not remaining_fixtures: + loaded_areas_on_cutout = _get_conflicting_addressable_areas( + existing_potential_fixtures, + set(self.state.loaded_addressable_areas_by_name), + self._state.deck_definition, + ) + raise IncompatibleAddressableAreaError( + f"Cannot load {addressable_area_name}, not compatible with one or more of" + f" the following areas: {loaded_areas_on_cutout}" + ) + self._state.potential_cutout_fixtures_by_cutout_id[ + cutout_id + ] = remaining_fixtures + else: + self._state.potential_cutout_fixtures_by_cutout_id[cutout_id] = set( + potential_fixtures + ) + return cutout_id + + +class AddressableAreaView(HasState[AddressableAreaState]): + """Read-only addressable area state view.""" + + _state: AddressableAreaState + + def __init__(self, state: AddressableAreaState) -> None: + """Initialize the computed view of addressable area state. + + Arguments: + state: Addressable area state dataclass used for all calculations. + """ + self._state = state + + def get_addressable_area(self, addressable_area_name: str) -> AddressableArea: + """Get addressable area.""" + if not self._state.use_simulated_deck_config: + return self._get_loaded_addressable_area(addressable_area_name) + else: + return self._get_addressable_area_for_simulation(addressable_area_name) + + def get_all(self) -> List[str]: + """Get a list of all loaded addressable area names.""" + return list(self._state.loaded_addressable_areas_by_name) + + def _get_loaded_addressable_area( + self, addressable_area_name: str + ) -> AddressableArea: + """Get an addressable area that has been loaded into state. Will raise error if it does not exist.""" + try: + return self._state.loaded_addressable_areas_by_name[addressable_area_name] + except KeyError: + raise AreaNotInDeckConfigurationError( + f"{addressable_area_name} not provided by deck configuration." + ) + + def _get_addressable_area_for_simulation( + self, addressable_area_name: str + ) -> AddressableArea: + """Get an addressable area that may not have been already loaded for a simulated run. + + Since this may be the first time this addressable area has been called, and it might not exist in the store + yet (and if not won't until the result completes), we have to check if it is theoretically possible and then + get the area data from the deck configuration provider. + """ + if addressable_area_name in self._state.loaded_addressable_areas_by_name: + return self._state.loaded_addressable_areas_by_name[addressable_area_name] + + ( + cutout_id, + potential_fixtures, + ) = deck_configuration_provider.get_potential_cutout_fixtures( + addressable_area_name, self._state.deck_definition + ) + + if cutout_id in self._state.potential_cutout_fixtures_by_cutout_id: + if not self._state.potential_cutout_fixtures_by_cutout_id[ + cutout_id + ].intersection(potential_fixtures): + loaded_areas_on_cutout = _get_conflicting_addressable_areas( + self._state.potential_cutout_fixtures_by_cutout_id[cutout_id], + set(self._state.loaded_addressable_areas_by_name), + self.state.deck_definition, + ) + raise IncompatibleAddressableAreaError( + f"Cannot load {addressable_area_name}, not compatible with one or more of" + f" the following areas: {loaded_areas_on_cutout}" + ) + + cutout_position = deck_configuration_provider.get_cutout_position( + cutout_id, self._state.deck_definition + ) + return deck_configuration_provider.get_addressable_area_from_name( + addressable_area_name, cutout_position, self._state.deck_definition + ) + + def get_addressable_area_position(self, addressable_area_name: str) -> Point: + """Get the position of an addressable area.""" + # TODO This should be the regular `get_addressable_area` once Robot Server deck config and tests is hooked up + addressable_area = self._get_addressable_area_for_simulation( + addressable_area_name + ) + position = addressable_area.position + return Point(x=position.x, y=position.y, z=position.z) + + def get_addressable_area_center(self, addressable_area_name: str) -> Point: + """Get the (x, y, z) position of the center of the area.""" + addressable_area = self.get_addressable_area(addressable_area_name) + position = addressable_area.position + bounding_box = addressable_area.bounding_box + return Point( + x=position.x + bounding_box.x / 2, + y=position.y + bounding_box.y / 2, + z=position.z, + ) + + def get_addressable_area_height(self, addressable_area_name: str) -> float: + """Get the z height of an addressable area.""" + addressable_area = self.get_addressable_area(addressable_area_name) + return addressable_area.bounding_box.z + + def get_slot_definition(self, slot: DeckSlotName) -> SlotDefV3: + """Get the definition of a slot in the deck.""" + try: + # TODO This should be the regular `get_addressable_area` once Robot Server deck config and tests is hooked up + addressable_area = self._get_addressable_area_for_simulation(slot.id) + except (AreaNotInDeckConfigurationError, IncompatibleAddressableAreaError): + raise SlotDoesNotExistError( + f"Slot ID {slot.id} does not exist in deck {self._state.deck_definition['otId']}" + ) + position = addressable_area.position + bounding_box = addressable_area.bounding_box + return { + "id": addressable_area.area_name, + "position": [position.x, position.y, position.z], + "boundingBox": { + "xDimension": bounding_box.x, + "yDimension": bounding_box.y, + "zDimension": bounding_box.z, + }, + "displayName": addressable_area.display_name, + "compatibleModuleTypes": addressable_area.compatible_module_types, + } diff --git a/api/src/opentrons/protocol_engine/state/config.py b/api/src/opentrons/protocol_engine/state/config.py index f1ba812bb8f..c5ba5fb07db 100644 --- a/api/src/opentrons/protocol_engine/state/config.py +++ b/api/src/opentrons/protocol_engine/state/config.py @@ -17,10 +17,14 @@ class Config: or pretending to control. ignore_pause: The engine should no-op instead of waiting for pauses and delays to complete. + use_virtual_pipettes: The engine should no-op instead of calling + instruments' hardware control API use_virtual_modules: The engine should no-op instead of calling modules' hardware control API. use_virtual_gripper: The engine should no-op instead of calling gripper hardware control API. + use_simulated_deck_config: The engine should lazily populate the deck + configuration instead of loading a provided configuration block_on_door_open: Protocol execution should pause if the front door is opened. """ @@ -31,4 +35,5 @@ class Config: use_virtual_pipettes: bool = False use_virtual_modules: bool = False use_virtual_gripper: bool = False + use_simulated_deck_config: bool = False block_on_door_open: bool = False diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 7c26be23098..4051d2383c9 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -7,6 +7,7 @@ from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN from .. import errors +from ..resources import fixture_validation from ..types import ( OFF_DECK_LOCATION, LoadedLabware, @@ -28,11 +29,13 @@ TipGeometry, LabwareMovementOffsetData, OnDeckLabwareLocation, + AddressableAreaLocation, ) from .config import Config from .labware import LabwareView from .modules import ModuleView from .pipettes import PipetteView +from .addressable_areas import AddressableAreaView from opentrons_shared_data.pipette import PIPETTE_X_SPAN from opentrons_shared_data.pipette.dev_types import ChannelCount @@ -66,12 +69,14 @@ def __init__( labware_view: LabwareView, module_view: ModuleView, pipette_view: PipetteView, + addressable_area_view: AddressableAreaView, ) -> None: """Initialize a GeometryView instance.""" self._config = config self._labware = labware_view self._modules = module_view self._pipettes = pipette_view + self._addressable_areas = addressable_area_view self._last_drop_tip_location_spot: Optional[_TipDropSection] = None def get_labware_highest_z(self, labware_id: str) -> float: @@ -100,7 +105,15 @@ def get_all_labware_highest_z(self) -> float: default=0.0, ) - return max(highest_labware_z, highest_module_z) + highest_addressable_area_z = max( + ( + self._addressable_areas.get_addressable_area_height(area_name) + for area_name in self._addressable_areas.get_all() + ), + default=0.0, + ) + + return max(highest_labware_z, highest_module_z, highest_addressable_area_z) def get_min_travel_z( self, @@ -125,7 +138,7 @@ def get_min_travel_z( def get_labware_parent_nominal_position(self, labware_id: str) -> Point: """Get the position of the labware's uncalibrated parent slot (deck, module, or another labware).""" slot_name = self.get_ancestor_slot_name(labware_id) - slot_pos = self._labware.get_slot_position(slot_name) + slot_pos = self._addressable_areas.get_addressable_area_position(slot_name.id) labware_data = self._labware.get(labware_id) offset = self._get_labware_position_offset(labware_id, labware_data.location) @@ -151,7 +164,7 @@ def _get_labware_position_offset( on modules as well as stacking overlaps. Does not include module calibration offset or LPC offset. """ - if isinstance(labware_location, DeckSlotLocation): + if isinstance(labware_location, (AddressableAreaLocation, DeckSlotLocation)): return LabwareOffsetVector(x=0, y=0, z=0) elif isinstance(labware_location, ModuleLocation): module_id = labware_location.moduleId @@ -227,7 +240,9 @@ def _get_calibrated_module_offset( return self._normalize_module_calibration_offset( module_location, offset_data ) - elif isinstance(location, DeckSlotLocation): + elif isinstance(location, (DeckSlotLocation, AddressableAreaLocation)): + # TODO we might want to do a check here to make sure addressable area location is a standard deck slot + # and raise if its not (or maybe we don't actually care since modules will never be loaded elsewhere) return ModuleOffsetVector(x=0, y=0, z=0) elif isinstance(location, OnLabwareLocation): labware_data = self._labware.get(location.labwareId) @@ -445,6 +460,15 @@ def get_ancestor_slot_name(self, labware_id: str) -> DeckSlotName: elif isinstance(labware.location, OnLabwareLocation): below_labware_id = labware.location.labwareId slot_name = self.get_ancestor_slot_name(below_labware_id) + elif isinstance(labware.location, AddressableAreaLocation): + area_name = labware.location.addressableAreaName + # TODO we might want to eventually return some sort of staging slot name when we're ready to work through + # the linting nightmare it will create + if fixture_validation.is_staging_slot(area_name): + raise ValueError( + "Cannot get ancestor slot name for labware on staging slot." + ) + slot_name = DeckSlotName.from_primitive(area_name) elif labware.location == OFF_DECK_LOCATION: raise errors.LabwareNotOnDeckError( f"Labware {labware_id} does not have a slot associated with it" @@ -457,6 +481,8 @@ def ensure_location_not_occupied( self, location: LabwareLocation ) -> LabwareLocation: """Ensure that the location does not already have equipment in it.""" + if isinstance(location, AddressableAreaLocation): + self._labware.raise_if_labware_in_location(location) if isinstance(location, (DeckSlotLocation, ModuleLocation)): self._labware.raise_if_labware_in_location(location) self._modules.raise_if_module_in_location(location) @@ -465,7 +491,9 @@ def ensure_location_not_occupied( def get_labware_grip_point( self, labware_id: str, - location: Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation], + location: Union[ + DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation + ], ) -> Point: """Get the grip point of the labware as placed on the given location. @@ -480,16 +508,33 @@ def get_labware_grip_point( grip_height_from_labware_bottom = ( self._labware.get_grip_height_from_labware_bottom(labware_id) ) - location_slot: DeckSlotName + location_name: str if isinstance(location, DeckSlotLocation): - location_slot = location.slotName + location_name = location.slotName.id offset = LabwareOffsetVector(x=0, y=0, z=0) + elif isinstance(location, AddressableAreaLocation): + location_name = location.addressableAreaName + if fixture_validation.is_gripper_waste_chute(location_name): + gripper_waste_chute = self._addressable_areas.get_addressable_area( + location_name + ) + drop_labware_location = gripper_waste_chute.drop_labware_location + if drop_labware_location is None: + raise ValueError( + f"{location_name} does not have a drop labware location associated with it" + ) + return drop_labware_location + Point(z=grip_height_from_labware_bottom) + # Location should have been pre-validated so this will be a deck/staging area slot + else: + offset = LabwareOffsetVector(x=0, y=0, z=0) else: if isinstance(location, ModuleLocation): - location_slot = self._modules.get_location(location.moduleId).slotName + location_name = self._modules.get_location( + location.moduleId + ).slotName.id else: # OnLabwareLocation - location_slot = self.get_ancestor_slot_name(location.labwareId) + location_name = self.get_ancestor_slot_name(location.labwareId).id labware_offset = self._get_labware_position_offset(labware_id, location) # Get the calibrated offset if the on labware location is on top of a module, otherwise return empty one cal_offset = self._get_calibrated_module_offset(location) @@ -499,11 +544,13 @@ def get_labware_grip_point( z=labware_offset.z + cal_offset.z, ) - slot_center = self._labware.get_slot_center_position(location_slot) + location_center = self._addressable_areas.get_addressable_area_center( + location_name + ) return Point( - slot_center.x + offset.x, - slot_center.y + offset.y, - slot_center.z + offset.z + grip_height_from_labware_bottom, + location_center.x + offset.x, + location_center.y + offset.y, + location_center.z + offset.z + grip_height_from_labware_bottom, ) def get_extra_waypoints( @@ -517,8 +564,8 @@ def get_extra_waypoints( middle_slot = DeckSlotName.SLOT_5.to_equivalent_for_robot_type( self._config.robot_type ) - middle_slot_center = self._labware.get_slot_center_position( - slot=middle_slot, + middle_slot_center = self._addressable_areas.get_addressable_area_center( + addressable_area_name=middle_slot.id, ) return [(middle_slot_center.x, middle_slot_center.y)] return [] @@ -706,10 +753,18 @@ def get_final_labware_movement_offset_vectors( @staticmethod def ensure_valid_gripper_location( location: LabwareLocation, - ) -> Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation]: + ) -> Union[ + DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation + ]: """Ensure valid on-deck location for gripper, otherwise raise error.""" if not isinstance( - location, (DeckSlotLocation, ModuleLocation, OnLabwareLocation) + location, + ( + DeckSlotLocation, + ModuleLocation, + OnLabwareLocation, + AddressableAreaLocation, + ), ): raise errors.LabwareMovementNotAllowedError( "Off-deck labware movements are not supported using the gripper." @@ -721,7 +776,9 @@ def get_total_nominal_gripper_offset_for_move_type( ) -> LabwareOffsetVector: """Get the total of the offsets to be used to pick up labware in its current location.""" if move_type == _GripperMoveType.PICK_UP_LABWARE: - if isinstance(location, (ModuleLocation, DeckSlotLocation)): + if isinstance( + location, (ModuleLocation, DeckSlotLocation, AddressableAreaLocation) + ): return self._nominal_gripper_offsets_for_location(location).pickUpOffset else: # If it's a labware on a labware (most likely an adapter), @@ -741,7 +798,9 @@ def get_total_nominal_gripper_offset_for_move_type( ).pickUpOffset ) else: - if isinstance(location, (ModuleLocation, DeckSlotLocation)): + if isinstance( + location, (ModuleLocation, DeckSlotLocation, AddressableAreaLocation) + ): return self._nominal_gripper_offsets_for_location(location).dropOffset else: # If it's a labware on a labware (most likely an adapter), @@ -765,7 +824,9 @@ def _nominal_gripper_offsets_for_location( self, location: OnDeckLabwareLocation ) -> LabwareMovementOffsetData: """Provide the default gripper offset data for the given location type.""" - if isinstance(location, DeckSlotLocation): + if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)): + # TODO we might need a separate type of gripper offset for addressable areas but that also might just + # be covered by the drop labware offset/location offsets = self._labware.get_deck_default_gripper_offsets() elif isinstance(location, ModuleLocation): offsets = self._modules.get_default_gripper_offsets(location.moduleId) diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index b40a00c7b65..010574799c1 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -16,18 +16,18 @@ cast, ) -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 from opentrons_shared_data.gripper.constants import LABWARE_GRIP_FORCE from opentrons_shared_data.labware.labware_definition import LabwareRole from opentrons_shared_data.pipette.dev_types import LabwareUri -from opentrons.types import DeckSlotName, Point, MountType +from opentrons.types import DeckSlotName, MountType from opentrons.protocols.api_support.constants import OPENTRONS_NAMESPACE from opentrons.protocols.models import LabwareDefinition, WellDefinition from opentrons.calibration_storage.helpers import uri_from_details from .. import errors -from ..resources import DeckFixedLabware, labware_validation +from ..resources import DeckFixedLabware, labware_validation, fixture_validation from ..commands import ( Command, LoadLabwareResult, @@ -36,6 +36,7 @@ from ..types import ( DeckSlotLocation, OnLabwareLocation, + AddressableAreaLocation, NonStackedLocation, Dimensions, LabwareOffset, @@ -47,6 +48,7 @@ ModuleModel, OverlapOffset, LabwareMovementOffsetData, + OFF_DECK_LOCATION, ) from ..actions import ( Action, @@ -203,6 +205,13 @@ def _handle_command(self, command: Command) -> None: new_offset_id = command.result.offsetId self._state.labware_by_id[labware_id].offsetId = new_offset_id + if isinstance( + new_location, AddressableAreaLocation + ) and fixture_validation.is_gripper_waste_chute( + new_location.addressableAreaName + ): + # If a labware has been moved into a waste chute it's been chuted away and is now technically off deck + new_location = OFF_DECK_LOCATION self._state.labware_by_id[labware_id].location = new_location def _add_labware_offset(self, labware_offset: LabwareOffset) -> None: @@ -308,82 +317,6 @@ def get_deck_definition(self) -> DeckDefinitionV4: """Get the current deck definition.""" return self._state.deck_definition - def get_slot_definition(self, slot: DeckSlotName) -> SlotDefV3: - """Get the definition of a slot in the deck.""" - deck_def = self.get_deck_definition() - - # TODO(jbl 2023-10-19 this is all incredibly hacky and ultimately we should get rid of SlotDefV3, and maybe - # move all this to another store/provider. However for now, this can be more or less equivalent and not break - # things TM TM TM - - for cutout in deck_def["locations"]["cutouts"]: - if cutout["id"].endswith(slot.id): - base_position = cutout["position"] - break - else: - raise errors.SlotDoesNotExistError( - f"Slot ID {slot.id} does not exist in deck {deck_def['otId']}" - ) - - slot_def: SlotDefV3 - # Slot 12/fixed trash for ot2 is a little weird so if its that just return some hardcoded stuff - if slot.id == "12": - slot_def = { - "id": "12", - "position": base_position, - "boundingBox": { - "xDimension": 128.0, - "yDimension": 86.0, - "zDimension": 0, - }, - "displayName": "Slot 12", - "compatibleModuleTypes": [], - } - return slot_def - - for area in deck_def["locations"]["addressableAreas"]: - if area["id"] == slot.id: - offset = area["offsetFromCutoutFixture"] - position = [ - offset[0] + base_position[0], - offset[1] + base_position[1], - offset[2] + base_position[2], - ] - slot_def = { - "id": area["id"], - "position": position, - "boundingBox": area["boundingBox"], - "displayName": area["displayName"], - "compatibleModuleTypes": area["compatibleModuleTypes"], - } - if area.get("matingSurfaceUnitVector"): - slot_def["matingSurfaceUnitVector"] = area[ - "matingSurfaceUnitVector" - ] - return slot_def - - raise errors.SlotDoesNotExistError( - f"Slot ID {slot.id} does not exist in deck {deck_def['otId']}" - ) - - def get_slot_position(self, slot: DeckSlotName) -> Point: - """Get the position of a deck slot.""" - slot_def = self.get_slot_definition(slot) - position = slot_def["position"] - - return Point(x=position[0], y=position[1], z=position[2]) - - def get_slot_center_position(self, slot: DeckSlotName) -> Point: - """Get the (x, y, z) position of the center of the slot.""" - slot_def = self.get_slot_definition(slot) - position = slot_def["position"] - - return Point( - x=position[0] + slot_def["boundingBox"]["xDimension"] / 2, - y=position[1] + slot_def["boundingBox"]["yDimension"] / 2, - z=position[2] + slot_def["boundingBox"]["zDimension"] / 2, - ) - def get_definition_by_uri(self, uri: LabwareUri) -> LabwareDefinition: """Get the labware definition matching loadName namespace and version.""" try: @@ -730,7 +663,7 @@ def is_fixed_trash(self, labware_id: str) -> bool: return self.get_fixed_trash_id() == labware_id def raise_if_labware_in_location( - self, location: Union[DeckSlotLocation, ModuleLocation] + self, location: Union[DeckSlotLocation, ModuleLocation, AddressableAreaLocation] ) -> None: """Raise an error if the specified location has labware in it.""" for labware in self.get_all(): diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index 3c402701810..63f7d14db74 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -14,6 +14,12 @@ from .abstract_store import HasState, HandlesActions from .change_notifier import ChangeNotifier from .commands import CommandState, CommandStore, CommandView +from .addressable_areas import ( + AddressableAreaState, + AddressableAreaStore, + AddressableAreaView, + DeckConfiguration, +) from .labware import LabwareState, LabwareStore, LabwareView from .pipettes import PipetteState, PipetteStore, PipetteView from .modules import ModuleState, ModuleStore, ModuleView @@ -32,6 +38,7 @@ class State: """Underlying engine state.""" commands: CommandState + addressable_areas: AddressableAreaState labware: LabwareState pipettes: PipetteState modules: ModuleState @@ -44,6 +51,7 @@ class StateView(HasState[State]): _state: State _commands: CommandView + _addressable_areas: AddressableAreaView _labware: LabwareView _pipettes: PipetteView _modules: ModuleView @@ -58,6 +66,11 @@ def commands(self) -> CommandView: """Get state view selectors for commands state.""" return self._commands + @property + def addressable_areas(self) -> AddressableAreaView: + """Get state view selectors for addressable area state.""" + return self._addressable_areas + @property def labware(self) -> LabwareView: """Get state view selectors for labware state.""" @@ -101,6 +114,7 @@ def config(self) -> Config: def get_summary(self) -> StateSummary: """Get protocol run data.""" error = self._commands.get_error() + # TODO maybe add summary here for AA return StateSummary.construct( status=self._commands.get_status(), errors=[] if error is None else [error], @@ -131,6 +145,7 @@ def __init__( is_door_open: bool, change_notifier: Optional[ChangeNotifier] = None, module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None, + deck_configuration: Optional[DeckConfiguration] = None, ) -> None: """Initialize a StateStore and its substores. @@ -143,9 +158,17 @@ def __init__( is_door_open: Whether the robot's door is currently open. change_notifier: Internal state change notifier. module_calibration_offsets: Module offsets to preload. + deck_configuration: The initial deck configuration the addressable area store will be instantiated with. """ self._command_store = CommandStore(config=config, is_door_open=is_door_open) self._pipette_store = PipetteStore() + if deck_configuration is None: + deck_configuration = [] + self._addressable_area_store = AddressableAreaStore( + deck_configuration=deck_configuration, + config=config, + deck_definition=deck_definition, + ) self._labware_store = LabwareStore( deck_fixed_labware=deck_fixed_labware, deck_definition=deck_definition, @@ -159,6 +182,7 @@ def __init__( self._substores: List[HandlesActions] = [ self._command_store, self._pipette_store, + self._addressable_area_store, self._labware_store, self._module_store, self._liquid_store, @@ -243,6 +267,7 @@ def _get_next_state(self) -> State: """Get a new instance of the state value object.""" return State( commands=self._command_store.state, + addressable_areas=self._addressable_area_store.state, labware=self._labware_store.state, pipettes=self._pipette_store.state, modules=self._module_store.state, @@ -257,6 +282,7 @@ def _initialize_state(self) -> None: # Base states self._state = state self._commands = CommandView(state.commands) + self._addressable_areas = AddressableAreaView(state.addressable_areas) self._labware = LabwareView(state.labware) self._pipettes = PipetteView(state.pipettes) self._modules = ModuleView(state.modules) @@ -269,6 +295,7 @@ def _initialize_state(self) -> None: labware_view=self._labware, module_view=self._modules, pipette_view=self._pipettes, + addressable_area_view=self._addressable_areas, ) self._motion = MotionView( config=self._config, @@ -283,6 +310,7 @@ def _update_state_views(self) -> None: next_state = self._get_next_state() self._state = next_state self._commands._state = next_state.commands + self._addressable_areas._state = next_state.addressable_areas self._labware._state = next_state.labware self._pipettes._state = next_state.pipettes self._modules._state = next_state.modules diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index b00e8ee1af6..fd13af9e6c2 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -9,7 +9,7 @@ from typing_extensions import Literal, TypeGuard from opentrons_shared_data.pipette.dev_types import PipetteNameType -from opentrons.types import MountType, DeckSlotName +from opentrons.types import MountType, DeckSlotName, Point from opentrons.hardware_control.modules import ( ModuleType as ModuleType, ) @@ -18,6 +18,7 @@ # convenience re-export of LabwareUri type LabwareUri as LabwareUri, ) +from opentrons_shared_data.module.dev_types import ModuleType as SharedDataModuleType class EngineStatus(str, Enum): @@ -54,6 +55,19 @@ class DeckSlotLocation(BaseModel): ) +class AddressableAreaLocation(BaseModel): + """The location of something place in an addressable area. This is a superset of deck slots.""" + + addressableAreaName: str = Field( + ..., + description=( + "The name of the addressable area that you want to use." + " Valid values are the `id`s of `addressableArea`s in the" + " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)." + ), + ) + + class ModuleLocation(BaseModel): """The location of something placed atop a hardware module.""" @@ -76,13 +90,21 @@ class OnLabwareLocation(BaseModel): OFF_DECK_LOCATION: _OffDeckLocationType = "offDeck" LabwareLocation = Union[ - DeckSlotLocation, ModuleLocation, OnLabwareLocation, _OffDeckLocationType + DeckSlotLocation, + ModuleLocation, + OnLabwareLocation, + _OffDeckLocationType, + AddressableAreaLocation, ] """Union of all locations where it's legal to keep a labware.""" -OnDeckLabwareLocation = Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation] +OnDeckLabwareLocation = Union[ + DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation +] -NonStackedLocation = Union[DeckSlotLocation, ModuleLocation, _OffDeckLocationType] +NonStackedLocation = Union[ + DeckSlotLocation, AddressableAreaLocation, ModuleLocation, _OffDeckLocationType +] """Union of all locations where it's legal to keep a labware that can't be stacked on another labware""" @@ -390,6 +412,10 @@ class OverlapOffset(Vec3f): """Offset representing overlap space of one labware on top of another labware or module.""" +class AddressableOffsetVector(Vec3f): + """Offset, in deck coordinates, from nominal to actual position of an addressable area.""" + + class LabwareMovementOffsetData(BaseModel): """Offsets to be used during labware movement.""" @@ -637,6 +663,28 @@ class LabwareMovementStrategy(str, Enum): MANUAL_MOVE_WITHOUT_PAUSE = "manualMoveWithoutPause" +@dataclass(frozen=True) +class PotentialCutoutFixture: + """Cutout and cutout fixture id associated with a potential cutout fixture that can be on the deck.""" + + cutout_id: str + cutout_fixture_id: str + + +@dataclass(frozen=True) +class AddressableArea: + """Addressable area that has been loaded.""" + + area_name: str + display_name: str + bounding_box: Dimensions + position: AddressableOffsetVector + compatible_module_types: List[SharedDataModuleType] + # TODO do we need "ableToDropLabware" in the definition? + drop_tip_location: Optional[Point] + drop_labware_location: Optional[Point] + + class PostRunHardwareState(Enum): """State of robot gantry & motors after a stop is performed and the hardware API is reset. diff --git a/api/src/opentrons/protocol_runner/create_simulating_runner.py b/api/src/opentrons/protocol_runner/create_simulating_runner.py index ff4df1020f7..0c60af3a45c 100644 --- a/api/src/opentrons/protocol_runner/create_simulating_runner.py +++ b/api/src/opentrons/protocol_runner/create_simulating_runner.py @@ -57,6 +57,7 @@ async def create_simulating_runner( ignore_pause=True, use_virtual_modules=True, use_virtual_gripper=True, + use_simulated_deck_config=True, use_virtual_pipettes=(not feature_flags.disable_fast_protocol_upload()), ), load_fixed_trash=should_load_fixed_trash(protocol_config), diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index 2dc744432c0..c46259572fb 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -927,6 +927,7 @@ def _get_protocol_engine_config(robot_type: RobotType) -> Config: use_virtual_pipettes=True, use_virtual_modules=True, use_virtual_gripper=True, + use_simulated_deck_config=True, ) diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index 979fe9b936a..f28a0723b01 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -308,6 +308,8 @@ def _make_ot3_pe_ctx( use_virtual_pipettes=True, use_virtual_modules=True, use_virtual_gripper=True, + # TODO figure out if we will want to use a "real" deck config here or if we are fine with simulated + use_simulated_deck_config=True, block_on_door_open=False, ), drop_tips_after_run=False, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py index 749f6cc4f60..084c9ff71c5 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py @@ -1367,7 +1367,9 @@ def test_get_slot_center( ) -> None: """It should return a slot center from engine state.""" decoy.when( - mock_engine_client.state.labware.get_slot_center_position(DeckSlotName.SLOT_2) + mock_engine_client.state.addressable_areas.get_addressable_area_center( + DeckSlotName.SLOT_2.id + ) ).then_return(Point(1, 2, 3)) result = subject.get_slot_center(DeckSlotName.SLOT_2) diff --git a/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py new file mode 100644 index 00000000000..b8528fcaa7c --- /dev/null +++ b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py @@ -0,0 +1,339 @@ +"""Test deck configuration provider.""" +from typing import List, Set + +import pytest +from pytest_lazyfixture import lazy_fixture # type: ignore[import] + +from opentrons_shared_data.deck import load as load_deck +from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 + +from opentrons.types import Point + +from opentrons.protocol_engine.errors import ( + FixtureDoesNotExistError, + CutoutDoesNotExistError, + AddressableAreaDoesNotExistError, + FixtureDoesNotProvideAreasError, +) +from opentrons.protocol_engine.types import ( + AddressableArea, + PotentialCutoutFixture, + DeckPoint, + Dimensions, + AddressableOffsetVector, +) +from opentrons.protocols.api_support.deck_type import ( + SHORT_TRASH_DECK, + STANDARD_OT2_DECK, + STANDARD_OT3_DECK, +) + +from opentrons.protocol_engine.resources import deck_configuration_provider as subject + + +@pytest.fixture(scope="session") +def ot2_standard_deck_def() -> DeckDefinitionV4: + """Get the OT-2 standard deck definition.""" + return load_deck(STANDARD_OT2_DECK, 4) + + +@pytest.fixture(scope="session") +def ot2_short_trash_deck_def() -> DeckDefinitionV4: + """Get the OT-2 standard deck definition.""" + return load_deck(SHORT_TRASH_DECK, 4) + + +@pytest.fixture(scope="session") +def ot3_standard_deck_def() -> DeckDefinitionV4: + """Get the OT-2 standard deck definition.""" + return load_deck(STANDARD_OT3_DECK, 4) + + +@pytest.mark.parametrize( + ("cutout_id", "expected_deck_point", "deck_def"), + [ + ( + "cutout5", + DeckPoint(x=132.5, y=90.5, z=0.0), + lazy_fixture("ot2_standard_deck_def"), + ), + ( + "cutout5", + DeckPoint(x=132.5, y=90.5, z=0.0), + lazy_fixture("ot2_short_trash_deck_def"), + ), + ( + "cutoutC2", + DeckPoint(x=164.0, y=107, z=0.0), + lazy_fixture("ot3_standard_deck_def"), + ), + ], +) +def test_get_cutout_position( + cutout_id: str, + expected_deck_point: DeckPoint, + deck_def: DeckDefinitionV4, +) -> None: + """It should get the deck position for the requested cutout id.""" + cutout_position = subject.get_cutout_position(cutout_id, deck_def) + assert cutout_position == expected_deck_point + + +def test_get_cutout_position_raises( + ot3_standard_deck_def: DeckDefinitionV4, +) -> None: + """It should raise if there is no cutout with that ID in the deck definition.""" + with pytest.raises(CutoutDoesNotExistError): + subject.get_cutout_position("theFunCutout", ot3_standard_deck_def) + + +@pytest.mark.parametrize( + ("cutout_fixture_id", "expected_display_name", "deck_def"), + [ + ("singleStandardSlot", "Standard Slot", lazy_fixture("ot2_standard_deck_def")), + ( + "singleStandardSlot", + "Standard Slot", + lazy_fixture("ot2_short_trash_deck_def"), + ), + ( + "singleRightSlot", + "Standard Slot Right", + lazy_fixture("ot3_standard_deck_def"), + ), + ], +) +def test_get_cutout_fixture( + cutout_fixture_id: str, + expected_display_name: str, + deck_def: DeckDefinitionV4, +) -> None: + """It should get the cutout fixture given the cutout fixture id.""" + cutout_fixture = subject.get_cutout_fixture(cutout_fixture_id, deck_def) + assert cutout_fixture["displayName"] == expected_display_name + + +def test_get_cutout_fixture_raises( + ot3_standard_deck_def: DeckDefinitionV4, +) -> None: + """It should raise if the given cutout fixture id does not exist.""" + with pytest.raises(FixtureDoesNotExistError): + subject.get_cutout_fixture("theFunFixture", ot3_standard_deck_def) + + +@pytest.mark.parametrize( + ("cutout_fixture_id", "cutout_id", "expected_areas", "deck_def"), + [ + ( + "singleStandardSlot", + "cutout1", + ["1"], + lazy_fixture("ot2_standard_deck_def"), + ), + ( + "singleStandardSlot", + "cutout1", + ["1"], + lazy_fixture("ot2_short_trash_deck_def"), + ), + ( + "stagingAreaRightSlot", + "cutoutD3", + ["D3", "D4"], + lazy_fixture("ot3_standard_deck_def"), + ), + ], +) +def test_get_provided_addressable_area_names( + cutout_fixture_id: str, + cutout_id: str, + expected_areas: List[str], + deck_def: DeckDefinitionV4, +) -> None: + """It should get the provided addressable area for the cutout fixture and cutout.""" + provided_addressable_areas = subject.get_provided_addressable_area_names( + cutout_fixture_id, cutout_id, deck_def + ) + assert provided_addressable_areas == expected_areas + + +def test_get_provided_addressable_area_raises( + ot3_standard_deck_def: DeckDefinitionV4, +) -> None: + """It should raise if the cutout fixture does not provide areas for the given cutout id.""" + with pytest.raises(FixtureDoesNotProvideAreasError): + subject.get_provided_addressable_area_names( + "singleRightSlot", "theFunCutout", ot3_standard_deck_def + ) + + +@pytest.mark.parametrize( + ( + "addressable_area_name", + "expected_cutout_id", + "expected_potential_fixtures", + "deck_def", + ), + [ + ( + "3", + "cutout3", + { + PotentialCutoutFixture( + cutout_id="cutout3", + cutout_fixture_id="singleStandardSlot", + ) + }, + lazy_fixture("ot2_standard_deck_def"), + ), + ( + "3", + "cutout3", + { + PotentialCutoutFixture( + cutout_id="cutout3", + cutout_fixture_id="singleStandardSlot", + ) + }, + lazy_fixture("ot2_short_trash_deck_def"), + ), + ( + "D3", + "cutoutD3", + { + PotentialCutoutFixture( + cutout_id="cutoutD3", cutout_fixture_id="singleRightSlot" + ), + PotentialCutoutFixture( + cutout_id="cutoutD3", cutout_fixture_id="stagingAreaRightSlot" + ), + }, + lazy_fixture("ot3_standard_deck_def"), + ), + ], +) +def test_get_potential_cutout_fixtures( + addressable_area_name: str, + expected_cutout_id: str, + expected_potential_fixtures: Set[PotentialCutoutFixture], + deck_def: DeckDefinitionV4, +) -> None: + """It should get a cutout id and a set of potential cutout fixtures for an addressable area name.""" + cutout_id, potential_fixtures = subject.get_potential_cutout_fixtures( + addressable_area_name, deck_def + ) + assert cutout_id == expected_cutout_id + assert potential_fixtures == expected_potential_fixtures + + +def test_get_potential_cutout_fixtures_raises( + ot3_standard_deck_def: DeckDefinitionV4, +) -> None: + """It should raise if there is no fixtures that provide the requested area.""" + with pytest.raises(AssertionError): + subject.get_potential_cutout_fixtures("theFunArea", ot3_standard_deck_def) + + +# TODO put in fixed trash for OT2 decks +@pytest.mark.parametrize( + ("addressable_area_name", "expected_addressable_area", "deck_def"), + [ + ( + "1", + AddressableArea( + area_name="1", + display_name="Slot 1", + bounding_box=Dimensions(x=128.0, y=86.0, z=0), + position=AddressableOffsetVector(x=1, y=2, z=3), + compatible_module_types=[ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType", + ], + drop_tip_location=None, + drop_labware_location=None, + ), + lazy_fixture("ot2_standard_deck_def"), + ), + ( + "1", + AddressableArea( + area_name="1", + display_name="Slot 1", + bounding_box=Dimensions(x=128.0, y=86.0, z=0), + position=AddressableOffsetVector(x=1, y=2, z=3), + compatible_module_types=[ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType", + ], + drop_tip_location=None, + drop_labware_location=None, + ), + lazy_fixture("ot2_short_trash_deck_def"), + ), + ( + "D1", + AddressableArea( + area_name="D1", + display_name="Slot D1", + bounding_box=Dimensions(x=128.0, y=86.0, z=0), + position=AddressableOffsetVector(x=1, y=2, z=3), + compatible_module_types=[ + "temperatureModuleType", + "heaterShakerModuleType", + "magneticBlockType", + ], + drop_tip_location=None, + drop_labware_location=None, + ), + lazy_fixture("ot3_standard_deck_def"), + ), + ( + "movableTrashB3", + AddressableArea( + area_name="movableTrashB3", + display_name="Trash Bin", + bounding_box=Dimensions(x=246.5, y=91.5, z=40), + position=AddressableOffsetVector(x=-16, y=-0.75, z=3), + compatible_module_types=[], + drop_tip_location=Point(x=124.25, y=47.75, z=43.0), + drop_labware_location=None, + ), + lazy_fixture("ot3_standard_deck_def"), + ), + ( + "gripperWasteChute", + AddressableArea( + area_name="gripperWasteChute", + display_name="Gripper Waste Chute", + bounding_box=Dimensions(x=155.0, y=86.0, z=154.0), + position=AddressableOffsetVector(x=-12.5, y=2, z=3), + compatible_module_types=[], + drop_tip_location=None, + drop_labware_location=Point(x=65, y=31, z=139.5), + ), + lazy_fixture("ot3_standard_deck_def"), + ), + ], +) +def test_get_addressable_area_from_name( + addressable_area_name: str, + expected_addressable_area: AddressableArea, + deck_def: DeckDefinitionV4, +) -> None: + """It should get the deck position for the requested cutout id.""" + addressable_area = subject.get_addressable_area_from_name( + addressable_area_name, DeckPoint(x=1, y=2, z=3), deck_def + ) + assert addressable_area == expected_addressable_area + + +def test_get_addressable_area_from_name_raises( + ot3_standard_deck_def: DeckDefinitionV4, +) -> None: + """It should raise if there is no addressable area by that name in the deck.""" + with pytest.raises(AddressableAreaDoesNotExistError): + subject.get_addressable_area_from_name( + "theFunArea", DeckPoint(x=1, y=2, z=3), ot3_standard_deck_def + ) diff --git a/api/tests/opentrons/protocol_engine/state/command_fixtures.py b/api/tests/opentrons/protocol_engine/state/command_fixtures.py index ef548377a3e..fc576d5c683 100644 --- a/api/tests/opentrons/protocol_engine/state/command_fixtures.py +++ b/api/tests/opentrons/protocol_engine/state/command_fixtures.py @@ -9,9 +9,12 @@ from opentrons.protocol_engine import ErrorOccurrence, commands as cmd from opentrons.protocol_engine.types import ( DeckPoint, + ModuleModel, + ModuleDefinition, MovementAxis, WellLocation, LabwareLocation, + DeckSlotLocation, LabwareMovementStrategy, ) @@ -159,6 +162,30 @@ def create_load_pipette_command( ) +def create_load_module_command( + module_id: str, + location: DeckSlotLocation, + model: ModuleModel, +) -> cmd.LoadModule: + """Get a completed LoadModule command.""" + params = cmd.LoadModuleParams(moduleId=module_id, location=location, model=model) + result = cmd.LoadModuleResult( + moduleId=module_id, + model=model, + serialNumber=None, + definition=ModuleDefinition.construct(), # type: ignore[call-arg] + ) + + return cmd.LoadModule( + id="command-id", + key="command-key", + status=cmd.CommandStatus.SUCCEEDED, + createdAt=datetime.now(), + params=params, + result=result, + ) + + def create_aspirate_command( pipette_id: str, volume: float, diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py new file mode 100644 index 00000000000..018332293b4 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py @@ -0,0 +1,319 @@ +"""Addressable area state store tests.""" +import pytest + +from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.labware.labware_definition import Parameters +from opentrons.protocols.models import LabwareDefinition +from opentrons.types import DeckSlotName + +from opentrons.protocol_engine.commands import Command +from opentrons.protocol_engine.actions import UpdateCommandAction +from opentrons.protocol_engine.errors import ( + # AreaNotInDeckConfigurationError, + IncompatibleAddressableAreaError, +) +from opentrons.protocol_engine.state import Config +from opentrons.protocol_engine.state.addressable_areas import ( + AddressableAreaStore, + AddressableAreaState, + DeckConfiguration, +) +from opentrons.protocol_engine.types import ( + DeckType, + ModuleModel, + LabwareMovementStrategy, + DeckSlotLocation, + AddressableAreaLocation, +) + +from .command_fixtures import ( + create_load_labware_command, + create_load_module_command, + create_move_labware_command, +) + + +def _make_deck_config() -> DeckConfiguration: + return [ + ("cutoutA1", "singleLeftSlot"), + ("cutoutB1", "singleLeftSlot"), + ("cutoutC1", "singleLeftSlot"), + ("cutoutD1", "singleLeftSlot"), + ("cutoutA2", "singleCenterSlot"), + ("cutoutB2", "singleCenterSlot"), + ("cutoutC2", "singleCenterSlot"), + ("cutoutD2", "singleCenterSlot"), + ("cutoutA3", "trashBinAdapter"), + ("cutoutB3", "singleRightSlot"), + ("cutoutC3", "stagingAreaRightSlot"), + ("cutoutD3", "wasteChuteRightAdapterNoCover"), + ] + + +@pytest.fixture +def simulated_subject( + ot3_standard_deck_def: DeckDefinitionV4, +) -> AddressableAreaStore: + """Get an AddressableAreaStore test subject, under simulated deck conditions.""" + return AddressableAreaStore( + deck_configuration=[], + config=Config( + use_simulated_deck_config=True, + robot_type="OT-3 Standard", + deck_type=DeckType.OT3_STANDARD, + ), + deck_definition=ot3_standard_deck_def, + ) + + +@pytest.fixture +def subject( + ot3_standard_deck_def: DeckDefinitionV4, +) -> AddressableAreaStore: + """Get an AddressableAreaStore test subject.""" + return AddressableAreaStore( + deck_configuration=_make_deck_config(), + config=Config( + use_simulated_deck_config=False, + robot_type="OT-3 Standard", + deck_type=DeckType.OT3_STANDARD, + ), + deck_definition=ot3_standard_deck_def, + ) + + +def test_initial_state_simulated( + ot3_standard_deck_def: DeckDefinitionV4, + simulated_subject: AddressableAreaStore, +) -> None: + """It should create the Addressable Area store with no loaded addressable areas.""" + assert simulated_subject.state == AddressableAreaState( + loaded_addressable_areas_by_name={}, + potential_cutout_fixtures_by_cutout_id={}, + deck_definition=ot3_standard_deck_def, + use_simulated_deck_config=True, + ) + + +def test_initial_state( + ot3_standard_deck_def: DeckDefinitionV4, + subject: AddressableAreaStore, +) -> None: + """It should create the Addressable Area store with loaded addressable areas.""" + assert subject.state.potential_cutout_fixtures_by_cutout_id == {} + assert not subject.state.use_simulated_deck_config + assert subject.state.deck_definition == ot3_standard_deck_def + # Loading 9 regular slots, 1 trash, 2 Staging Area slots and 3 waste chute types + assert len(subject.state.loaded_addressable_areas_by_name) == 15 + + +@pytest.mark.parametrize( + ("command", "expected_area"), + ( + ( + create_load_labware_command( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + labware_id="test-labware-id", + definition=LabwareDefinition.construct( # type: ignore[call-arg] + parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] + namespace="bleh", + version=123, + ), + offset_id="offset-id", + display_name="display-name", + ), + "A1", + ), + ( + create_load_labware_command( + location=AddressableAreaLocation(addressableAreaName="A4"), + labware_id="test-labware-id", + definition=LabwareDefinition.construct( # type: ignore[call-arg] + parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] + namespace="bleh", + version=123, + ), + offset_id="offset-id", + display_name="display-name", + ), + "A4", + ), + ( + create_load_module_command( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + module_id="test-module-id", + model=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "A1", + ), + ( + create_move_labware_command( + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + strategy=LabwareMovementStrategy.USING_GRIPPER, + ), + "A1", + ), + ( + create_move_labware_command( + new_location=AddressableAreaLocation(addressableAreaName="A4"), + strategy=LabwareMovementStrategy.USING_GRIPPER, + ), + "A4", + ), + ), +) +def test_addressable_area_referencing_commands_load_on_simulated_deck( + command: Command, + expected_area: str, + simulated_subject: AddressableAreaStore, +) -> None: + """It should check and store the addressable area when referenced in a command.""" + simulated_subject.handle_action( + UpdateCommandAction(private_result=None, command=command) + ) + assert expected_area in simulated_subject.state.loaded_addressable_areas_by_name + + +@pytest.mark.parametrize( + "command", + ( + create_load_labware_command( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), + labware_id="test-labware-id", + definition=LabwareDefinition.construct( # type: ignore[call-arg] + parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] + namespace="bleh", + version=123, + ), + offset_id="offset-id", + display_name="display-name", + ), + create_load_module_command( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), + module_id="test-module-id", + model=ModuleModel.TEMPERATURE_MODULE_V2, + ), + create_move_labware_command( + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), + strategy=LabwareMovementStrategy.USING_GRIPPER, + ), + ), +) +def test_handles_command_simulated_raises( + command: Command, + simulated_subject: AddressableAreaStore, +) -> None: + """It should raise when two incompatible areas are referenced.""" + initial_command = create_move_labware_command( + new_location=AddressableAreaLocation(addressableAreaName="gripperWasteChute"), + strategy=LabwareMovementStrategy.USING_GRIPPER, + ) + + simulated_subject.handle_action( + UpdateCommandAction(private_result=None, command=initial_command) + ) + + with pytest.raises(IncompatibleAddressableAreaError): + simulated_subject.handle_action( + UpdateCommandAction(private_result=None, command=command) + ) + + +@pytest.mark.parametrize( + ("command", "expected_area"), + ( + ( + create_load_labware_command( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + labware_id="test-labware-id", + definition=LabwareDefinition.construct( # type: ignore[call-arg] + parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] + namespace="bleh", + version=123, + ), + offset_id="offset-id", + display_name="display-name", + ), + "A1", + ), + ( + create_load_labware_command( + location=AddressableAreaLocation(addressableAreaName="C4"), + labware_id="test-labware-id", + definition=LabwareDefinition.construct( # type: ignore[call-arg] + parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] + namespace="bleh", + version=123, + ), + offset_id="offset-id", + display_name="display-name", + ), + "C4", + ), + ( + create_load_module_command( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + module_id="test-module-id", + model=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "A1", + ), + ( + create_move_labware_command( + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + strategy=LabwareMovementStrategy.USING_GRIPPER, + ), + "A1", + ), + ( + create_move_labware_command( + new_location=AddressableAreaLocation(addressableAreaName="C4"), + strategy=LabwareMovementStrategy.USING_GRIPPER, + ), + "C4", + ), + ), +) +def test_addressable_area_referencing_commands_load( + command: Command, + expected_area: str, + subject: AddressableAreaStore, +) -> None: + """It should check that the addressable area is in the deck config.""" + subject.handle_action(UpdateCommandAction(private_result=None, command=command)) + assert expected_area in subject.state.loaded_addressable_areas_by_name + + +# TODO Uncomment this out once this check is back in +# @pytest.mark.parametrize( +# "command", +# ( +# create_load_labware_command( +# location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), +# labware_id="test-labware-id", +# definition=LabwareDefinition.construct( # type: ignore[call-arg] +# parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] +# namespace="bleh", +# version=123, +# ), +# offset_id="offset-id", +# display_name="display-name", +# ), +# create_load_module_command( +# location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), +# module_id="test-module-id", +# model=ModuleModel.TEMPERATURE_MODULE_V2, +# ), +# create_move_labware_command( +# new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), +# strategy=LabwareMovementStrategy.USING_GRIPPER, +# ), +# ), +# ) +# def test_handles_load_labware_raises( +# command: Command, +# subject: AddressableAreaStore, +# ) -> None: +# """It should raise when referencing an addressable area not in the deck config.""" +# with pytest.raises(AreaNotInDeckConfigurationError): +# subject.handle_action(UpdateCommandAction(private_result=None, command=command)) diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py new file mode 100644 index 00000000000..fc084d13f09 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py @@ -0,0 +1,250 @@ +"""Addressable area state view tests.""" +import inspect + +import pytest +from decoy import Decoy +from typing import Dict, Set, Optional, cast + +from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons.types import Point, DeckSlotName + +from opentrons.protocol_engine.errors import ( + AreaNotInDeckConfigurationError, + IncompatibleAddressableAreaError, + # SlotDoesNotExistError, +) +from opentrons.protocol_engine.resources import deck_configuration_provider +from opentrons.protocol_engine.state.addressable_areas import ( + AddressableAreaState, + AddressableAreaView, +) +from opentrons.protocol_engine.types import ( + AddressableArea, + PotentialCutoutFixture, + Dimensions, + DeckPoint, + AddressableOffsetVector, +) + + +@pytest.fixture(autouse=True) +def patch_mock_move_types(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None: + """Mock out move_types.py functions.""" + for name, func in inspect.getmembers( + deck_configuration_provider, inspect.isfunction + ): + monkeypatch.setattr(deck_configuration_provider, name, decoy.mock(func=func)) + + +def get_addressable_area_view( + loaded_addressable_areas_by_name: Optional[Dict[str, AddressableArea]] = None, + potential_cutout_fixtures_by_cutout_id: Optional[ + Dict[str, Set[PotentialCutoutFixture]] + ] = None, + deck_definition: Optional[DeckDefinitionV4] = None, + use_simulated_deck_config: bool = False, +) -> AddressableAreaView: + """Get a labware view test subject.""" + state = AddressableAreaState( + loaded_addressable_areas_by_name=loaded_addressable_areas_by_name or {}, + potential_cutout_fixtures_by_cutout_id=potential_cutout_fixtures_by_cutout_id + or {}, + deck_definition=deck_definition or cast(DeckDefinitionV4, {"otId": "fake"}), + use_simulated_deck_config=use_simulated_deck_config, + ) + + return AddressableAreaView(state=state) + + +def test_get_loaded_addressable_area() -> None: + """It should get the loaded addressable area.""" + addressable_area = AddressableArea( + area_name="area", + display_name="fancy name", + bounding_box=Dimensions(x=1, y=2, z=3), + position=AddressableOffsetVector(x=7, y=8, z=9), + compatible_module_types=["magneticModuleType"], + drop_tip_location=Point(11, 22, 33), + drop_labware_location=None, + ) + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={"abc": addressable_area} + ) + + assert subject.get_addressable_area("abc") is addressable_area + + +def test_get_loaded_addressable_area_raises() -> None: + """It should raise if the addressable area does not exist.""" + subject = get_addressable_area_view() + + with pytest.raises(AreaNotInDeckConfigurationError): + subject.get_addressable_area("abc") + + +def test_get_addressable_area_for_simulation_already_loaded() -> None: + """It should get the addressable area for a simulation that has not been loaded yet.""" + addressable_area = AddressableArea( + area_name="area", + display_name="fancy name", + bounding_box=Dimensions(x=1, y=2, z=3), + position=AddressableOffsetVector(x=7, y=8, z=9), + compatible_module_types=["magneticModuleType"], + drop_tip_location=Point(11, 22, 33), + drop_labware_location=None, + ) + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={"abc": addressable_area}, + use_simulated_deck_config=True, + ) + + assert subject.get_addressable_area("abc") is addressable_area + + +def test_get_addressable_area_for_simulation_not_loaded(decoy: Decoy) -> None: + """It should get the addressable area for a simulation that has not been loaded yet.""" + subject = get_addressable_area_view( + potential_cutout_fixtures_by_cutout_id={ + "123": {PotentialCutoutFixture(cutout_id="123", cutout_fixture_id="blah")} + }, + use_simulated_deck_config=True, + ) + + addressable_area = AddressableArea( + area_name="area", + display_name="fancy name", + bounding_box=Dimensions(x=1, y=2, z=3), + position=AddressableOffsetVector(x=7, y=8, z=9), + compatible_module_types=["magneticModuleType"], + drop_tip_location=Point(11, 22, 33), + drop_labware_location=None, + ) + + decoy.when( + deck_configuration_provider.get_potential_cutout_fixtures( + "abc", subject.state.deck_definition + ) + ).then_return( + ("123", {PotentialCutoutFixture(cutout_id="123", cutout_fixture_id="blah")}) + ) + + decoy.when( + deck_configuration_provider.get_cutout_position( + "123", subject.state.deck_definition + ) + ).then_return(DeckPoint(x=1, y=2, z=3)) + + decoy.when( + deck_configuration_provider.get_addressable_area_from_name( + "abc", DeckPoint(x=1, y=2, z=3), subject.state.deck_definition + ) + ).then_return(addressable_area) + + assert subject.get_addressable_area("abc") is addressable_area + + +def test_get_addressable_area_for_simulation_raises(decoy: Decoy) -> None: + """It should raise if the requested addressable area is incompatible with loaded ones.""" + subject = get_addressable_area_view( + potential_cutout_fixtures_by_cutout_id={ + "123": {PotentialCutoutFixture(cutout_id="789", cutout_fixture_id="bleh")} + }, + use_simulated_deck_config=True, + ) + + decoy.when( + deck_configuration_provider.get_potential_cutout_fixtures( + "abc", subject.state.deck_definition + ) + ).then_return( + ("123", {PotentialCutoutFixture(cutout_id="123", cutout_fixture_id="blah")}) + ) + + decoy.when( + deck_configuration_provider.get_provided_addressable_area_names( + "bleh", "789", subject.state.deck_definition + ) + ).then_return([]) + + with pytest.raises(IncompatibleAddressableAreaError): + subject.get_addressable_area("abc") + + +def test_get_addressable_area_position() -> None: + """It should get the absolute location of the addressable area.""" + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={ + "abc": AddressableArea( + area_name="area", + display_name="fancy name", + bounding_box=Dimensions(x=10, y=20, z=30), + position=AddressableOffsetVector(x=1, y=2, z=3), + compatible_module_types=[], + drop_tip_location=None, + drop_labware_location=None, + ) + } + ) + + result = subject.get_addressable_area_position("abc") + assert result == Point(1, 2, 3) + + +def test_get_addressable_area_center() -> None: + """It should get the absolute location of an addressable area's center.""" + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={ + "abc": AddressableArea( + area_name="area", + display_name="fancy name", + bounding_box=Dimensions(x=10, y=20, z=30), + position=AddressableOffsetVector(x=1, y=2, z=3), + compatible_module_types=[], + drop_tip_location=None, + drop_labware_location=None, + ) + } + ) + + result = subject.get_addressable_area_center("abc") + assert result == Point(6, 12, 3) + + +def test_get_slot_definition() -> None: + """It should return a deck slot's definition.""" + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={ + "6": AddressableArea( + area_name="area", + display_name="fancy name", + bounding_box=Dimensions(x=1, y=2, z=3), + position=AddressableOffsetVector(x=7, y=8, z=9), + compatible_module_types=["magneticModuleType"], + drop_tip_location=None, + drop_labware_location=None, + ) + } + ) + + result = subject.get_slot_definition(DeckSlotName.SLOT_6) + + assert result == { + "id": "area", + "position": [7, 8, 9], + "boundingBox": { + "xDimension": 1, + "yDimension": 2, + "zDimension": 3, + }, + "displayName": "fancy name", + "compatibleModuleTypes": ["magneticModuleType"], + } + + +# TODO Uncomment once Robot Server deck config and tests is hooked up +# def test_get_slot_definition_raises_with_bad_slot_name() -> None: +# """It should raise a SlotDoesNotExistError if a bad slot name is given.""" +# subject = get_addressable_area_view() +# +# with pytest.raises(SlotDoesNotExistError): +# subject.get_slot_definition(DeckSlotName.SLOT_A1) diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index e46dd87d58a..86e74a88985 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -40,6 +40,7 @@ from opentrons.protocol_engine.state.labware import LabwareView from opentrons.protocol_engine.state.modules import ModuleView from opentrons.protocol_engine.state.pipettes import PipetteView, StaticPipetteConfig +from opentrons.protocol_engine.state.addressable_areas import AddressableAreaView from opentrons.protocol_engine.state.geometry import GeometryView, _GripperMoveType @@ -61,6 +62,12 @@ def mock_pipette_view(decoy: Decoy) -> PipetteView: return decoy.mock(cls=PipetteView) +@pytest.fixture +def addressable_area_view(decoy: Decoy) -> AddressableAreaView: + """Get a mock in the shape of a AddressableAreaView.""" + return decoy.mock(cls=AddressableAreaView) + + @pytest.fixture(autouse=True) def patch_mock_move_types(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None: """Mock out move_types.py functions.""" @@ -70,7 +77,10 @@ def patch_mock_move_types(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None @pytest.fixture def subject( - labware_view: LabwareView, module_view: ModuleView, mock_pipette_view: PipetteView + labware_view: LabwareView, + module_view: ModuleView, + mock_pipette_view: PipetteView, + addressable_area_view: AddressableAreaView, ) -> GeometryView: """Get a GeometryView with its store dependencies mocked out.""" return GeometryView( @@ -81,12 +91,14 @@ def subject( labware_view=labware_view, module_view=module_view, pipette_view=mock_pipette_view, + addressable_area_view=addressable_area_view, ) def test_get_labware_parent_position( decoy: Decoy, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should return a deck slot position for labware in a deck slot.""" @@ -98,9 +110,9 @@ def test_get_labware_parent_position( offsetId=None, ) decoy.when(labware_view.get("labware-id")).then_return(labware_data) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - Point(1, 2, 3) - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(Point(1, 2, 3)) result = subject.get_labware_parent_position("labware-id") @@ -129,6 +141,7 @@ def test_get_labware_parent_position_on_module( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -145,9 +158,9 @@ def test_get_labware_parent_position_on_module( decoy.when(module_view.get_location("module-id")).then_return( DeckSlotLocation(slotName=DeckSlotName.SLOT_3) ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - Point(1, 2, 3) - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(Point(1, 2, 3)) decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) decoy.when( module_view.get_nominal_module_offset( @@ -178,6 +191,7 @@ def test_get_labware_parent_position_on_labware( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -200,9 +214,9 @@ def test_get_labware_parent_position_on_labware( decoy.when(module_view.get_location("module-id")).then_return( DeckSlotLocation(slotName=DeckSlotName.SLOT_3) ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - Point(1, 2, 3) - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(Point(1, 2, 3)) decoy.when(labware_view.get("adapter-id")).then_return(adapter_data) decoy.when(labware_view.get_dimensions("adapter-id")).then_return( Dimensions(x=123, y=456, z=5) @@ -299,6 +313,7 @@ def test_get_labware_origin_position( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should return a deck slot position with the labware's offset as its origin.""" @@ -312,9 +327,9 @@ def test_get_labware_origin_position( decoy.when(labware_view.get("labware-id")).then_return(labware_data) decoy.when(labware_view.get_definition("labware-id")).then_return(well_plate_def) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - Point(1, 2, 3) - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(Point(1, 2, 3)) expected_parent = Point(1, 2, 3) expected_offset = Point( @@ -333,6 +348,7 @@ def test_get_labware_highest_z( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should get the absolute location of a labware's highest Z point.""" @@ -351,9 +367,9 @@ def test_get_labware_highest_z( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(slot_pos) highest_z = subject.get_labware_highest_z("labware-id") @@ -365,6 +381,7 @@ def test_get_module_labware_highest_z( well_plate_def: LabwareDefinition, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -384,9 +401,9 @@ def test_get_module_labware_highest_z( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(slot_pos) decoy.when(module_view.get_location("module-id")).then_return( DeckSlotLocation(slotName=DeckSlotName.SLOT_3) ) @@ -421,11 +438,13 @@ def test_get_all_labware_highest_z_no_equipment( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should return 0 if no loaded equipment.""" decoy.when(module_view.get_all()).then_return([]) decoy.when(labware_view.get_all()).then_return([]) + decoy.when(addressable_area_view.get_all()).then_return([]) result = subject.get_all_labware_highest_z() @@ -439,6 +458,7 @@ def test_get_all_labware_highest_z( falcon_tuberack_def: LabwareDefinition, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should get the highest Z amongst all labware.""" @@ -469,6 +489,7 @@ def test_get_all_labware_highest_z( reservoir_offset = LabwareOffsetVector(x=1, y=-2, z=3) decoy.when(module_view.get_all()).then_return([]) + decoy.when(addressable_area_view.get_all()).then_return([]) decoy.when(labware_view.get_all()).then_return([plate, off_deck_lw, reservoir]) decoy.when(labware_view.get("plate-id")).then_return(plate) @@ -491,12 +512,12 @@ def test_get_all_labware_highest_z( reservoir_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - Point(1, 2, 3) - ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - Point(4, 5, 6) - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(Point(1, 2, 3)) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(Point(4, 5, 6)) plate_z = subject.get_labware_highest_z("plate-id") reservoir_z = subject.get_labware_highest_z("reservoir-id") @@ -510,6 +531,7 @@ def test_get_all_labware_highest_z_with_modules( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should get the highest Z including modules.""" @@ -517,6 +539,8 @@ def test_get_all_labware_highest_z_with_modules( module_2 = LoadedModule.construct(id="module-id-2") # type: ignore[call-arg] decoy.when(labware_view.get_all()).then_return([]) + decoy.when(addressable_area_view.get_all()).then_return([]) + decoy.when(module_view.get_all()).then_return([module_1, module_2]) decoy.when(module_view.get_overall_height("module-id-1")).then_return(42.0) decoy.when(module_view.get_overall_height("module-id-2")).then_return(1337.0) @@ -526,6 +550,30 @@ def test_get_all_labware_highest_z_with_modules( assert result == 1337.0 +def test_get_all_labware_highest_z_with_addressable_area( + decoy: Decoy, + labware_view: LabwareView, + module_view: ModuleView, + addressable_area_view: AddressableAreaView, + subject: GeometryView, +) -> None: + """It should get the highest Z including addressable areas.""" + decoy.when(labware_view.get_all()).then_return([]) + decoy.when(module_view.get_all()).then_return([]) + + decoy.when(addressable_area_view.get_all()).then_return(["abc", "xyz"]) + decoy.when(addressable_area_view.get_addressable_area_height("abc")).then_return( + 42.0 + ) + decoy.when(addressable_area_view.get_addressable_area_height("xyz")).then_return( + 1337.0 + ) + + result = subject.get_all_labware_highest_z() + + assert result == 1337.0 + + @pytest.mark.parametrize( ["location", "min_z_height", "expected_min_z"], [ @@ -543,6 +591,7 @@ def test_get_min_travel_z( well_plate_def: LabwareDefinition, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, location: Optional[CurrentWell], min_z_height: Optional[float], expected_min_z: float, @@ -562,12 +611,13 @@ def test_get_min_travel_z( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( LabwareOffsetVector(x=0, y=0, z=3) ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - Point(0, 0, 3) - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(Point(0, 0, 3)) decoy.when(module_view.get_all()).then_return([]) decoy.when(labware_view.get_all()).then_return([]) + decoy.when(addressable_area_view.get_all()).then_return([]) min_travel_z = subject.get_min_travel_z( "pipette-id", "labware-id", location, min_z_height @@ -580,6 +630,7 @@ def test_get_labware_position( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should return the slot position plus calibrated offset.""" @@ -598,9 +649,9 @@ def test_get_labware_position( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) position = subject.get_labware_position(labware_id="labware-id") @@ -615,6 +666,7 @@ def test_get_well_position( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should be able to get the position of a well top in a labware.""" @@ -634,9 +686,9 @@ def test_get_well_position( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) @@ -669,6 +721,7 @@ def test_get_module_labware_well_position( well_plate_def: LabwareDefinition, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -689,9 +742,9 @@ def test_get_module_labware_well_position( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) @@ -731,6 +784,7 @@ def test_get_well_position_with_top_offset( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should be able to get the position of a well top in a labware.""" @@ -750,9 +804,9 @@ def test_get_well_position_with_top_offset( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) @@ -777,6 +831,7 @@ def test_get_well_position_with_bottom_offset( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should be able to get the position of a well bottom in a labware.""" @@ -796,9 +851,9 @@ def test_get_well_position_with_bottom_offset( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) @@ -823,6 +878,7 @@ def test_get_well_position_with_center_offset( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should be able to get the position of a well center in a labware.""" @@ -842,9 +898,9 @@ def test_get_well_position_with_center_offset( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) @@ -869,6 +925,7 @@ def test_get_relative_well_location( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should get the relative location of a well given an absolute position.""" @@ -888,9 +945,9 @@ def test_get_relative_well_location( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) @@ -1147,6 +1204,7 @@ def test_get_labware_grip_point( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -1155,9 +1213,9 @@ def test_get_labware_grip_point( labware_view.get_grip_height_from_labware_bottom("labware-id") ).then_return(100) - decoy.when(labware_view.get_slot_center_position(DeckSlotName.SLOT_1)).then_return( - Point(x=101, y=102, z=103) - ) + decoy.when( + addressable_area_view.get_addressable_area_center(DeckSlotName.SLOT_1.id) + ).then_return(Point(x=101, y=102, z=103)) labware_center = subject.get_labware_grip_point( labware_id="labware-id", location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1) ) @@ -1169,6 +1227,7 @@ def test_get_labware_grip_point_on_labware( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -1200,9 +1259,9 @@ def test_get_labware_grip_point_on_labware( labware_view.get_labware_overlap_offsets("labware-id", "below-name") ).then_return(OverlapOffset(x=0, y=1, z=6)) - decoy.when(labware_view.get_slot_center_position(DeckSlotName.SLOT_4)).then_return( - Point(x=5, y=9, z=10) - ) + decoy.when( + addressable_area_view.get_addressable_area_center(DeckSlotName.SLOT_4.id) + ).then_return(Point(x=5, y=9, z=10)) grip_point = subject.get_labware_grip_point( labware_id="labware-id", location=OnLabwareLocation(labwareId="below-id") @@ -1215,6 +1274,7 @@ def test_get_labware_grip_point_for_labware_on_module( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -1245,9 +1305,9 @@ def test_get_labware_grip_point_for_labware_on_module( location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), ) ) - decoy.when(labware_view.get_slot_center_position(DeckSlotName.SLOT_4)).then_return( - Point(100, 200, 300) - ) + decoy.when( + addressable_area_view.get_addressable_area_center(DeckSlotName.SLOT_4.id) + ).then_return(Point(100, 200, 300)) result_grip_point = subject.get_labware_grip_point( labware_id="labware-id", location=ModuleLocation(moduleId="module-id") ) @@ -1267,6 +1327,7 @@ def test_get_extra_waypoints( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, location: Optional[CurrentWell], should_dodge: bool, expected_waypoints: List[Tuple[float, float]], @@ -1297,7 +1358,9 @@ def test_get_extra_waypoints( ).then_return(should_dodge) decoy.when( # Assume the subject's Config is for an OT-3, so use an OT-3 slot name. - labware_view.get_slot_center_position(slot=DeckSlotName.SLOT_C2) + addressable_area_view.get_addressable_area_center( + addressable_area_name=DeckSlotName.SLOT_C2.id + ) ).then_return(Point(x=11, y=22, z=33)) extra_waypoints = subject.get_extra_waypoints("to-labware-id", location) diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view.py b/api/tests/opentrons/protocol_engine/state/test_labware_view.py index 494b92ed548..d5b94adfbf5 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view.py @@ -20,7 +20,7 @@ STANDARD_OT3_DECK, ) from opentrons.protocols.models import LabwareDefinition -from opentrons.types import DeckSlotName, Point, MountType +from opentrons.types import DeckSlotName, MountType from opentrons.protocol_engine import errors from opentrons.protocol_engine.types import ( @@ -805,45 +805,6 @@ def test_get_deck_definition(ot2_standard_deck_def: DeckDefinitionV4) -> None: assert subject.get_deck_definition() == ot2_standard_deck_def -def test_get_slot_definition(ot2_standard_deck_def: DeckDefinitionV4) -> None: - """It should return a deck slot's definition.""" - subject = get_labware_view(deck_definition=ot2_standard_deck_def) - - result = subject.get_slot_definition(DeckSlotName.SLOT_6) - - assert result["id"] == "6" - assert result["displayName"] == "Slot 6" - - -def test_get_slot_definition_raises_with_bad_slot_name( - ot2_standard_deck_def: DeckDefinitionV4, -) -> None: - """It should raise a SlotDoesNotExistError if a bad slot name is given.""" - subject = get_labware_view(deck_definition=ot2_standard_deck_def) - - with pytest.raises(errors.SlotDoesNotExistError): - subject.get_slot_definition(DeckSlotName.SLOT_A1) - - -def test_get_slot_position(ot2_standard_deck_def: DeckDefinitionV4) -> None: - """It should get the absolute location of a deck slot's origin.""" - subject = get_labware_view(deck_definition=ot2_standard_deck_def) - - expected_position = Point(x=132.5, y=90.5, z=0.0) - result = subject.get_slot_position(DeckSlotName.SLOT_5) - - assert result == expected_position - - -def test_get_slot_center_position(ot2_standard_deck_def: DeckDefinitionV4) -> None: - """It should get the absolute location of a deck slot's center.""" - subject = get_labware_view(deck_definition=ot2_standard_deck_def) - - expected_center = Point(x=196.5, y=43.0, z=0.0) - result = subject.get_slot_center_position(DeckSlotName.SLOT_2) - assert result == expected_center - - def test_get_labware_offset_vector() -> None: """It should get a labware's offset vector.""" labware_without_offset = LoadedLabware( diff --git a/shared-data/command/schemas/8.json b/shared-data/command/schemas/8.json index 2bfeee8e49c..b44f044c02e 100644 --- a/shared-data/command/schemas/8.json +++ b/shared-data/command/schemas/8.json @@ -68,6 +68,9 @@ { "$ref": "#/definitions/MoveToWellCreate" }, + { + "$ref": "#/definitions/MoveToAddressableAreaCreate" + }, { "$ref": "#/definitions/PrepareToAspirateCreate" }, @@ -1232,6 +1235,19 @@ }, "required": ["labwareId"] }, + "AddressableAreaLocation": { + "title": "AddressableAreaLocation", + "description": "The location of something place in an addressable area. This is a superset of deck slots.", + "type": "object", + "properties": { + "addressableAreaName": { + "title": "Addressableareaname", + "description": "The name of the addressable area that you want to use. Valid values are the `id`s of `addressableArea`s in the [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck).", + "type": "string" + } + }, + "required": ["addressableAreaName"] + }, "LoadLabwareParams": { "title": "LoadLabwareParams", "description": "Payload required to load a labware into a slot.", @@ -1253,6 +1269,9 @@ { "enum": ["offDeck"], "type": "string" + }, + { + "$ref": "#/definitions/AddressableAreaLocation" } ] }, @@ -1580,6 +1599,9 @@ { "enum": ["offDeck"], "type": "string" + }, + { + "$ref": "#/definitions/AddressableAreaLocation" } ] }, @@ -1870,6 +1892,70 @@ }, "required": ["params"] }, + "MoveToAddressableAreaParams": { + "title": "MoveToAddressableAreaParams", + "description": "Payload required to move a pipette to a specific addressable area.", + "type": "object", + "properties": { + "minimumZHeight": { + "title": "Minimumzheight", + "description": "Optional minimal Z margin in mm. If this is larger than the API's default safe Z margin, it will make the arc higher. If it's smaller, it will have no effect.", + "type": "number" + }, + "forceDirect": { + "title": "Forcedirect", + "description": "If true, moving from one labware/well to another will not arc to the default safe z, but instead will move directly to the specified location. This will also force the `minimumZHeight` param to be ignored. A 'direct' movement is in X/Y/Z simultaneously.", + "default": false, + "type": "boolean" + }, + "speed": { + "title": "Speed", + "description": "Override the travel speed in mm/s. This controls the straight linear speed of motion.", + "type": "number" + }, + "pipetteId": { + "title": "Pipetteid", + "description": "Identifier of pipette to use for liquid handling.", + "type": "string" + }, + "addressableAreaName": { + "title": "Addressableareaname", + "description": "The name of the addressable area that you want to use. Valid values are the `id`s of `addressableArea`s in the [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck).", + "type": "string" + } + }, + "required": ["pipetteId", "addressableAreaName"] + }, + "MoveToAddressableAreaCreate": { + "title": "MoveToAddressableAreaCreate", + "description": "Move to addressable area command creation request model.", + "type": "object", + "properties": { + "commandType": { + "title": "Commandtype", + "default": "moveToAddressableArea", + "enum": ["moveToAddressableArea"], + "type": "string" + }, + "params": { + "$ref": "#/definitions/MoveToAddressableAreaParams" + }, + "intent": { + "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.", + "allOf": [ + { + "$ref": "#/definitions/CommandIntent" + } + ] + }, + "key": { + "title": "Key", + "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.", + "type": "string" + } + }, + "required": ["params"] + }, "PrepareToAspirateParams": { "title": "PrepareToAspirateParams", "description": "Parameters required to prepare a specific pipette for aspiration.", diff --git a/shared-data/deck/definitions/4/ot2_short_trash.json b/shared-data/deck/definitions/4/ot2_short_trash.json index 64ebd34a511..7dfb7cfc1aa 100644 --- a/shared-data/deck/definitions/4/ot2_short_trash.json +++ b/shared-data/deck/definitions/4/ot2_short_trash.json @@ -200,6 +200,18 @@ "heaterShakerModuleType" ] }, + { + "id": "12", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 12", + "compatibleModuleTypes": [] + }, { "id": "shortFixedTrash", "areaType": "fixedTrash", @@ -375,7 +387,8 @@ "cutout8": ["8"], "cutout9": ["9"], "cutout10": ["10"], - "cutout11": ["11"] + "cutout11": ["11"], + "cutout12": ["12"] } }, { diff --git a/shared-data/deck/definitions/4/ot2_standard.json b/shared-data/deck/definitions/4/ot2_standard.json index e28257ca332..eb6d446f69a 100644 --- a/shared-data/deck/definitions/4/ot2_standard.json +++ b/shared-data/deck/definitions/4/ot2_standard.json @@ -200,6 +200,18 @@ "heaterShakerModuleType" ] }, + { + "id": "12", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 12", + "compatibleModuleTypes": [] + }, { "id": "fixedTrash", "areaType": "fixedTrash", @@ -375,7 +387,8 @@ "cutout8": ["8"], "cutout9": ["9"], "cutout10": ["10"], - "cutout11": ["11"] + "cutout11": ["11"], + "cutout12": ["12"] } }, { diff --git a/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py b/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py index 48446e15f1b..ac0d92f48b9 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py @@ -81,6 +81,7 @@ class Location(BaseModel): slotName: Optional[str] moduleId: Optional[str] labwareId: Optional[str] + addressableAreaName: Optional[str] class ProfileStep(BaseModel): From 9e05537ce0b24d97ccd7c99c0d38bd50915004c6 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Wed, 15 Nov 2023 13:59:18 -0500 Subject: [PATCH 17/46] chore(hardware-testing): Mergeback robot testing tags (#13985) * chage the pre__pos and press_pos * modify Gauge height and Grip height * add gripper move to * home gripper after complete the cycles * format lint --------- Co-authored-by: Andy-Hu Co-authored-by: Andiiiiiiyy --- .../gripper_assembly_qc_ot3/test_width.py | 4 +- .../production_qc/z_stage_qc_ot3.py | 4 +- .../hardware_testing/scripts/gripper_move.py | 82 +++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 hardware-testing/hardware_testing/scripts/gripper_move.py diff --git a/hardware-testing/hardware_testing/production_qc/gripper_assembly_qc_ot3/test_width.py b/hardware-testing/hardware_testing/production_qc/gripper_assembly_qc_ot3/test_width.py index 7813a9e9340..453b038313b 100644 --- a/hardware-testing/hardware_testing/production_qc/gripper_assembly_qc_ot3/test_width.py +++ b/hardware-testing/hardware_testing/production_qc/gripper_assembly_qc_ot3/test_width.py @@ -16,8 +16,8 @@ from hardware_testing.opentrons_api.types import Axis, OT3Mount, Point FAILURE_THRESHOLD_MM = -3 -GAUGE_HEIGHT_MM = 40 -GRIP_HEIGHT_MM = 30 +GAUGE_HEIGHT_MM = 75 +GRIP_HEIGHT_MM = 48 TEST_WIDTHS_MM: List[float] = [60, 85.75, 62] SLOT_WIDTH_GAUGE: List[Optional[int]] = [None, 3, 9] GRIP_FORCES_NEWTON: List[float] = [10, 15, 20] diff --git a/hardware-testing/hardware_testing/production_qc/z_stage_qc_ot3.py b/hardware-testing/hardware_testing/production_qc/z_stage_qc_ot3.py index dd454177710..7806561568a 100644 --- a/hardware-testing/hardware_testing/production_qc/z_stage_qc_ot3.py +++ b/hardware-testing/hardware_testing/production_qc/z_stage_qc_ot3.py @@ -223,9 +223,9 @@ async def _force_gauge( await api.home([z_ax]) home_pos = await api.gantry_position(mount) LOG.info(f"Home Position: {home_pos}") - pre_test_pos = home_pos._replace(z=home_pos.z - 110) + pre_test_pos = home_pos._replace(z=home_pos.z - 15) LOG.info(f"Pre-Test Position: {pre_test_pos}") - press_pos = home_pos._replace(z=pre_test_pos.z - 113) + press_pos = home_pos._replace(z=pre_test_pos.z - 30) LOG.info(f"Press Position: {press_pos}") qc_pass = True diff --git a/hardware-testing/hardware_testing/scripts/gripper_move.py b/hardware-testing/hardware_testing/scripts/gripper_move.py new file mode 100644 index 00000000000..1b50f6cfb6f --- /dev/null +++ b/hardware-testing/hardware_testing/scripts/gripper_move.py @@ -0,0 +1,82 @@ +"""Demo OT3 Gantry Functionality.""" +# Author: Carlos Ferandez None: + hw_api = await build_async_ot3_hardware_api( + is_simulating=args.simulate, use_defaults=True + ) + await asyncio.sleep(1) + await hw_api.cache_instruments() + timeout_start = time.time() + timeout = 60 * 60 * 3 + count = 0 + x_offset = 80 + y_offset = 44 + try: + await hw_api.home() + await asyncio.sleep(1) + await hw_api.set_lights(rails=True) + home_position = await hw_api.current_position_ot3(mount) + await hw_api.grip(force_newtons=None, stay_engaged=True) + print(f"home: {home_position}") + x_home = home_position[Axis.X] - x_offset + y_home = home_position[Axis.Y] - y_offset + z_home = home_position[Axis.Z_G] + while time.time() < timeout_start + timeout: + # while True: + print(f"time: {time.time()-timeout_start}") + await hw_api.move_to(mount, Point(x_home, y_home, z_home)) + await hw_api.move_to(mount, Point(x_home, y_home, z_home - 190)) + count += 1 + print(f"cycle: {count}") + await hw_api.home() + except KeyboardInterrupt: + await hw_api.disengage_axes([Axis.X, Axis.Y, Axis.G]) + finally: + await hw_api.disengage_axes([Axis.X, Axis.Y, Axis.G]) + await hw_api.clean_up() + + +if __name__ == "__main__": + slot_locs = [ + "A1", + "A2", + "A3", + "B1", + "B2", + "B3:", + "C1", + "C2", + "C3", + "D1", + "D2", + "D3", + ] + parser = argparse.ArgumentParser() + parser.add_argument("--simulate", action="store_true") + parser.add_argument("--trough", action="store_true") + parser.add_argument("--tiprack", action="store_true") + parser.add_argument( + "--mount", type=str, choices=["left", "right", "gripper"], default="gripper" + ) + args = parser.parse_args() + if args.mount == "left": + mount = OT3Mount.LEFT + if args.mount == "gripper": + mount = OT3Mount.GRIPPER + else: + mount = OT3Mount.RIGHT + asyncio.run(_main()) From 21f0dac19052b47d2235763fa858b57249d655d0 Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 15 Nov 2023 14:56:35 -0500 Subject: [PATCH 18/46] refactor(components): update deck map styling (#13982) * refactor(components): update deck map styling --- app/src/molecules/DeckThumbnail/index.tsx | 9 ++- .../__tests__/SetupLabwareMap.test.tsx | 3 +- .../OnDeviceDisplay/ProtocolDetails/Deck.tsx | 6 +- .../src/hardware-sim/BaseDeck/BaseDeck.tsx | 4 + .../BaseDeck/WasteChuteFixture.tsx | 2 +- .../src/hardware-sim/Deck/FlexTrash.tsx | 5 +- .../hardware-sim/Labware/LabwareRender.tsx | 3 + .../labwareInternals/LabwareOutline.css | 2 +- .../labwareInternals/LabwareOutline.tsx | 75 +++++++++++++++---- .../labwareInternals/StaticLabware.tsx | 3 +- 10 files changed, 88 insertions(+), 24 deletions(-) diff --git a/app/src/molecules/DeckThumbnail/index.tsx b/app/src/molecules/DeckThumbnail/index.tsx index e75e244dca1..eefe2a3c1c6 100644 --- a/app/src/molecules/DeckThumbnail/index.tsx +++ b/app/src/molecules/DeckThumbnail/index.tsx @@ -29,10 +29,16 @@ import type { interface DeckThumbnailProps extends StyleProps { protocolAnalysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput | null showSlotLabels?: boolean + isOnDevice?: boolean } export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element | null { - const { protocolAnalysis, showSlotLabels = false, ...styleProps } = props + const { + protocolAnalysis, + showSlotLabels = false, + isOnDevice = false, + ...styleProps + } = props const attachedModules = useAttachedModules() if (protocolAnalysis == null || protocolAnalysis.errors.length) return null @@ -116,6 +122,7 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element | null { labwareLocations={labwareLocations} moduleLocations={moduleLocations} showSlotLabels={showSlotLabels} + isOnDevice={isOnDevice} {...styleProps} >
    ) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx index 079ef1fcdb3..02fae05cf58 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx @@ -3,7 +3,6 @@ import { when, resetAllWhenMocks } from 'jest-when' import { StaticRouter } from 'react-router-dom' import { renderWithProviders, - componentPropsMatcher, partialComponentPropsMatcher, LabwareRender, Module, @@ -111,7 +110,7 @@ describe('SetupLabwareMap', () => { when(mockLabwareRender) .mockReturnValue(
    ) // this (default) empty div will be returned when LabwareRender isn't called with expected labware definition .calledWith( - componentPropsMatcher({ + partialComponentPropsMatcher({ definition: fixture_tiprack_300_ul, }) ) diff --git a/app/src/pages/OnDeviceDisplay/ProtocolDetails/Deck.tsx b/app/src/pages/OnDeviceDisplay/ProtocolDetails/Deck.tsx index ce1f640a6ce..4f10d40e790 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolDetails/Deck.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolDetails/Deck.tsx @@ -22,7 +22,11 @@ export const Deck = (props: { protocolId: string }): JSX.Element => { return ( {mostRecentAnalysis != null ? ( - + ) : null} ) diff --git a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx index 0760fc10fa8..ac565632877 100644 --- a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx +++ b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx @@ -71,6 +71,7 @@ interface BaseDeckProps { darkFill?: string children?: React.ReactNode showSlotLabels?: boolean + isOnDevice?: boolean } export function BaseDeck(props: BaseDeckProps): JSX.Element { @@ -87,6 +88,7 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { showExpansion = true, children, showSlotLabels = true, + isOnDevice = false, } = props const deckDef = getDeckDefFromRobotType(robotType) @@ -201,6 +203,7 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { definition={nestedLabwareDef} onLabwareClick={onLabwareClick} wellFill={nestedLabwareWellFill} + isOnDevice={isOnDevice} /> ) : null} {moduleChildren} @@ -238,6 +241,7 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { definition={definition} onLabwareClick={onLabwareClick} wellFill={wellFill ?? undefined} + isOnDevice={isOnDevice} /> {labwareChildren} diff --git a/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx b/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx index 0ef8b60d848..b698dfdbd54 100644 --- a/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx +++ b/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx @@ -93,7 +93,7 @@ export function WasteChute(props: WasteChuteProps): JSX.Element { justifyContent={JUSTIFY_CENTER} width="100%" > - + Waste chute diff --git a/components/src/hardware-sim/Deck/FlexTrash.tsx b/components/src/hardware-sim/Deck/FlexTrash.tsx index 6a2d28ac20c..6fcd3b0a057 100644 --- a/components/src/hardware-sim/Deck/FlexTrash.tsx +++ b/components/src/hardware-sim/Deck/FlexTrash.tsx @@ -8,7 +8,7 @@ import { import { Icon } from '../../icons' import { Flex, Text } from '../../primitives' import { ALIGN_CENTER, JUSTIFY_CENTER } from '../../styles' -import { BORDERS, TYPOGRAPHY } from '../../ui-style-constants' +import { BORDERS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { RobotCoordsForeignObject } from './RobotCoordsForeignObject' import trashDef from '@opentrons/shared-data/labware/definitions/2/opentrons_1_trash_3200ml_fixed/1.json' @@ -89,6 +89,7 @@ export const FlexTrash = ({ backgroundColor={backgroundColor} borderRadius={BORDERS.radiusSoftCorners} justifyContent={JUSTIFY_CENTER} + gridGap={SPACING.spacing8} width="100%" > {rotateDegrees === '180' ? ( @@ -105,7 +106,7 @@ export const FlexTrash = ({ void highlightLabware?: boolean + /** This will be used for displaying outline of labware */ + isOnDevice?: boolean } export const LabwareRender = (props: LabwareRenderProps): JSX.Element => { @@ -77,6 +79,7 @@ export const LabwareRender = (props: LabwareRenderProps): JSX.Element => { hover={props.hover} onLabwareClick={props.onLabwareClick} highlightLabware={props.highlightLabware} + isOnDevice={props.isOnDevice} /> {props.wellStroke && ( - + {isOnDevice ? ( + + + + + + + + + + + + + + + ) : ( + + )} ) } diff --git a/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx b/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx index 0e7994969a6..2312a42f811 100644 --- a/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx +++ b/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx @@ -18,6 +18,7 @@ export interface StaticLabwareProps { hover?: boolean onLabwareClick?: () => void highlightLabware?: boolean + isOnDevice?: boolean } const TipDecoration = React.memo(function TipDecoration(props: { @@ -38,7 +39,6 @@ const TipDecoration = React.memo(function TipDecoration(props: { export function StaticLabwareComponent(props: StaticLabwareProps): JSX.Element { const { isTiprack } = props.definition.parameters - return ( @@ -46,6 +46,7 @@ export function StaticLabwareComponent(props: StaticLabwareProps): JSX.Element { definition={props.definition} hover={props.hover} highlight={props.highlightLabware === true} + isOnDevice={props.isOnDevice} /> From d8ab4fb17063fa73e4f8979dcd7ddff12b368305 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Wed, 15 Nov 2023 15:02:58 -0500 Subject: [PATCH 19/46] chore(hardware-testing): Mergeback hw testing tags into edge (#13974) * only calibarte and skip next test steps then * updata pipette single pick_up_num=60 * P50M submerges to -3mm * changed test speeds currents for plunger and jaws, removed partial tip pickup * edits to plunger speeds, changing jaw test order * moves to reload position at end of script * formatting and linting * removed right mount homing * added limits for pressure sensor * Update 96 1000ul blow out submerge from 20 to 30 * re-set blow_out_submerged to 20 * Update 96ch 1000ul increments * debug- re-set the increment aspirate volume for tip 1000 * update 96ch increments * pull in only the correct changes for p50m * chore(hardware-testing): update gravimetric patch files * format/lint --------- Co-authored-by: Andiiiiiiyy Co-authored-by: wweiye <275241708@qq.com> Co-authored-by: Andy Sigler Co-authored-by: David Gomez Co-authored-by: Jerome <1458798121@qq.com> --- .../hardware_testing/data/__init__.py | 7 +- .../gravimetric/increments.py | 18 +- .../gravimetric/liquid_class/defaults.py | 11 +- .../gravimetric/overrides/api.patch | 12 +- .../gravimetric/overrides/shared-data.patch | 180 ++++++ .../ninety_six_assembly_qc_ot3/__main__.py | 2 +- .../test_capacitance.py | 2 +- .../test_droplets.py | 60 +- .../ninety_six_assembly_qc_ot3/test_jaws.py | 144 +++-- .../test_plunger.py | 6 +- .../test_pressure.py | 46 +- .../scripts/tip_pick_up_lifetime_test.py | 602 ++++++++++++++++++ 12 files changed, 980 insertions(+), 110 deletions(-) create mode 100644 hardware-testing/hardware_testing/scripts/tip_pick_up_lifetime_test.py diff --git a/hardware-testing/hardware_testing/data/__init__.py b/hardware-testing/hardware_testing/data/__init__.py index d0199e9ce34..4af701b24bd 100644 --- a/hardware-testing/hardware_testing/data/__init__.py +++ b/hardware-testing/hardware_testing/data/__init__.py @@ -4,7 +4,7 @@ from pathlib import Path from subprocess import check_output from time import time -from typing import Tuple, Union +from typing import Tuple, Union, List, Any from opentrons.config import infer_config_base_dir, IS_ROBOT @@ -134,3 +134,8 @@ def insert_data_to_file( contents.insert(line, data) with open(data_path, "w") as f: f.write("".join(contents)) + + +def convert_list_to_csv_line(elements: List[Any]) -> str: + """Convert list of something into CSV line.""" + return ",".join(str(elements)) diff --git a/hardware-testing/hardware_testing/gravimetric/increments.py b/hardware-testing/hardware_testing/gravimetric/increments.py index 5bf6b8efd3b..bbe79d0785f 100644 --- a/hardware-testing/hardware_testing/gravimetric/increments.py +++ b/hardware-testing/hardware_testing/gravimetric/increments.py @@ -302,18 +302,24 @@ }, 1000: { "default": [ - 2.000, 3.000, - 4.000, 5.000, - 6.000, 7.000, - 8.000, - 9.000, 10.000, + 15.000, + 20.000, 50.000, + 100.000, + 120.000, 200.000, - 1137.10, + 320.000, + 450.000, + 650.000, + 850.000, + 1000.00, + 1030.00, + 1050.00, + 1075.00, ], }, } diff --git a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py index 1bc0145e071..a37f21b1b36 100644 --- a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py +++ b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py @@ -8,8 +8,9 @@ interpolate, ) -_default_submerge_aspirate_mm = 2.5 -_default_submerge_dispense_mm = 2.5 +_default_submerge_aspirate_mm = 1.5 +_p50_multi_submerge_aspirate_mm = 1.5 +_default_submerge_dispense_mm = 1.5 _default_retract_mm = 5.0 _default_retract_discontinuity = 20 @@ -498,7 +499,7 @@ 50: { # P50 50: { # T50 1: AspirateSettings( # 1uL - z_submerge_depth=_default_submerge_aspirate_mm, + z_submerge_depth=_p50_multi_submerge_aspirate_mm, plunger_acceleration=_default_accel_p50_ul_sec_sec, plunger_flow_rate=35, # ul/sec delay=_default_aspirate_delay_seconds, @@ -508,7 +509,7 @@ trailing_air_gap=0.1, ), 10: AspirateSettings( # 10uL - z_submerge_depth=_default_submerge_aspirate_mm, + z_submerge_depth=_p50_multi_submerge_aspirate_mm, plunger_acceleration=_default_accel_p50_ul_sec_sec, plunger_flow_rate=23.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -518,7 +519,7 @@ trailing_air_gap=0.1, ), 50: AspirateSettings( # 50uL - z_submerge_depth=_default_submerge_aspirate_mm, + z_submerge_depth=_p50_multi_submerge_aspirate_mm, plunger_acceleration=_default_accel_p50_ul_sec_sec, plunger_flow_rate=35, # ul/sec delay=_default_aspirate_delay_seconds, diff --git a/hardware-testing/hardware_testing/gravimetric/overrides/api.patch b/hardware-testing/hardware_testing/gravimetric/overrides/api.patch index 07b69b55ec7..9f143e52892 100644 --- a/hardware-testing/hardware_testing/gravimetric/overrides/api.patch +++ b/hardware-testing/hardware_testing/gravimetric/overrides/api.patch @@ -1,8 +1,8 @@ diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py -index 174a8f76e4..01b81cd6a0 100644 +index 1f6dd0b4b5..1d0cb7b7e3 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py -@@ -456,11 +456,11 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): +@@ -432,11 +432,11 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): def set_current_volume(self, new_volume: float) -> None: assert new_volume >= 0 @@ -16,7 +16,7 @@ index 174a8f76e4..01b81cd6a0 100644 self._current_volume += volume_incr def remove_current_volume(self, volume_incr: float) -> None: -@@ -468,7 +468,8 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): +@@ -444,7 +444,8 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): self._current_volume -= volume_incr def ok_to_add_volume(self, volume_incr: float) -> bool: @@ -27,10 +27,10 @@ index 174a8f76e4..01b81cd6a0 100644 def ok_to_push_out(self, push_out_dist_mm: float) -> bool: return push_out_dist_mm <= ( diff --git a/api/src/opentrons/protocol_api/core/legacy/deck.py b/api/src/opentrons/protocol_api/core/legacy/deck.py -index b0ca38d294..f213febabd 100644 +index ea4068934b..1b21cac251 100644 --- a/api/src/opentrons/protocol_api/core/legacy/deck.py +++ b/api/src/opentrons/protocol_api/core/legacy/deck.py -@@ -47,11 +47,11 @@ class DeckItem(Protocol): +@@ -48,11 +48,11 @@ class DeckItem(Protocol): class Deck(UserDict): # type: ignore[type-arg] data: Dict[int, Optional[DeckItem]] @@ -47,7 +47,7 @@ index b0ca38d294..f213febabd 100644 for slot in self._definition["locations"]["orderedSlots"]: self.data[int(slot["id"])] = None diff --git a/api/src/opentrons/protocol_api/create_protocol_context.py b/api/src/opentrons/protocol_api/create_protocol_context.py -index f2d8e492ec..dd4fd9102e 100644 +index 5a64e70cf9..7d5047cc4b 100644 --- a/api/src/opentrons/protocol_api/create_protocol_context.py +++ b/api/src/opentrons/protocol_api/create_protocol_context.py @@ -22,6 +22,7 @@ from .deck import Deck diff --git a/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch b/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch index b2d08d109e9..c7243e0d27a 100644 --- a/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch +++ b/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch @@ -870,3 +870,183 @@ index 0000000000..8ad4397cba + } + ] +} +diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json +index f89fb178b5..5cd8acd638 100644 +--- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json ++++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json +@@ -20,50 +20,50 @@ + "aspirate": { + "default": { + "1": [ +- [0.462, 0.5646, 0.0415], +- [0.648, 0.3716, 0.1307], +- [1.032, 0.2742, 0.1938], +- [1.37, 0.1499, 0.3221], +- [2.014, 0.1044, 0.3845], +- [2.772, 0.0432, 0.5076], +- [3.05, -0.0809, 0.8517], +- [3.4, 0.0256, 0.5268], +- [3.962, 0.0612, 0.4057], +- [4.438, 0.0572, 0.4217], +- [5.164, 0.018, 0.5955], +- [5.966, 0.0095, 0.6393], +- [7.38, 0.0075, 0.6514], +- [9.128, 0.0049, 0.6705], +- [10.16, 0.0033, 0.6854], +- [13.812, 0.0024, 0.6948], +- [27.204, 0.0008, 0.7165], +- [50.614, 0.0002, 0.7328], +- [53.046, -0.0005, 0.7676] ++ [0.3100,0.5910,0.0197], ++ [0.3900,0.2586,0.1227], ++ [0.8600,0.3697,0.0794], ++ [1.2900,0.2310,0.1987], ++ [1.9300,0.1144,0.3491], ++ [2.7000,0.0536,0.4664], ++ [2.9500,-0.1041,0.8923], ++ [3.2800,0.0216,0.5214], ++ [3.7600,0.0480,0.4349], ++ [4.3800,0.0830,0.3032], ++ [5.0800,0.0153,0.5996], ++ [5.9000,0.0136,0.6083], ++ [7.2900,0.0070,0.6474], ++ [9.0400,0.0059,0.6551], ++ [10.0800,0.0045,0.6682], ++ [13.7400,0.0029,0.6842], ++ [27.1500,0.0010,0.7104], ++ [50.4800,0.0002,0.7319], ++ [52.8900,-0.0006,0.7703] + ] + } + }, + "dispense": { + "default": { + "1": [ +- [0.462, 0.5646, 0.0415], +- [0.648, 0.3716, 0.1307], +- [1.032, 0.2742, 0.1938], +- [1.37, 0.1499, 0.3221], +- [2.014, 0.1044, 0.3845], +- [2.772, 0.0432, 0.5076], +- [3.05, -0.0809, 0.8517], +- [3.4, 0.0256, 0.5268], +- [3.962, 0.0612, 0.4057], +- [4.438, 0.0572, 0.4217], +- [5.164, 0.018, 0.5955], +- [5.966, 0.0095, 0.6393], +- [7.38, 0.0075, 0.6514], +- [9.128, 0.0049, 0.6705], +- [10.16, 0.0033, 0.6854], +- [13.812, 0.0024, 0.6948], +- [27.204, 0.0008, 0.7165], +- [50.614, 0.0002, 0.7328], +- [53.046, -0.0005, 0.7676] ++ [0.3100,0.5910,0.0197], ++ [0.3900,0.2586,0.1227], ++ [0.8600,0.3697,0.0794], ++ [1.2900,0.2310,0.1987], ++ [1.9300,0.1144,0.3491], ++ [2.7000,0.0536,0.4664], ++ [2.9500,-0.1041,0.8923], ++ [3.2800,0.0216,0.5214], ++ [3.7600,0.0480,0.4349], ++ [4.3800,0.0830,0.3032], ++ [5.0800,0.0153,0.5996], ++ [5.9000,0.0136,0.6083], ++ [7.2900,0.0070,0.6474], ++ [9.0400,0.0059,0.6551], ++ [10.0800,0.0045,0.6682], ++ [13.7400,0.0029,0.6842], ++ [27.1500,0.0010,0.7104], ++ [50.4800,0.0002,0.7319], ++ [52.8900,-0.0006,0.7703] + ] + } + }, +diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json +index e925e4e401..603a2cf861 100644 +--- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json ++++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json +@@ -20,46 +20,48 @@ + "aspirate": { + "default": { + "1": [ +- [0.11, 0.207815, 0.040201], +- [0.65, 0.43933, 0.014735], +- [1.04, 0.256666, 0.133466], +- [1.67, 0.147126, 0.247388], +- [2.45, 0.078774, 0.361536], +- [2.89, 0.042387, 0.450684], +- [3.2, 0.014781, 0.530464], +- [3.79, 0.071819, 0.347944], +- [4.22, 0.051592, 0.424605], +- [4.93, 0.021219, 0.552775], +- [5.81, 0.023461, 0.541725], +- [7.21, 0.008959, 0.625982], +- [8.93, 0.005456, 0.651235], +- [10.0, 0.007108, 0.636489], +- [13.61, 0.002591, 0.681656], +- [26.99, 0.001163, 0.701094], +- [45.25, 0.000207, 0.726887] ++ [0.3000,0.4590,0.0586], ++ [0.4700,0.4300,0.0674], ++ [0.9000,0.3404,0.1095], ++ [1.2600,0.1925,0.2425], ++ [1.9500,0.1314,0.3195], ++ [2.7600,0.0604,0.4580], ++ [2.9500,-0.2085,1.2002], ++ [3.3300,0.0425,0.4597], ++ [3.8700,0.0592,0.4040], ++ [4.3100,0.0518,0.4327], ++ [5.0700,0.0264,0.5424], ++ [5.9300,0.0186,0.5818], ++ [7.3400,0.0078,0.6458], ++ [9.0800,0.0050,0.6664], ++ [10.0900,0.0022,0.6918], ++ [13.7400,0.0027,0.6868], ++ [27.1300,0.0009,0.7109], ++ [45.4300,-0.0038,0.8391] + ] + } + }, + "dispense": { + "default": { + "1": [ +- [0.11, 0.207815, 0.040201], +- [0.65, 0.43933, 0.014735], +- [1.04, 0.256666, 0.133466], +- [1.67, 0.147126, 0.247388], +- [2.45, 0.078774, 0.361536], +- [2.89, 0.042387, 0.450684], +- [3.2, 0.014781, 0.530464], +- [3.79, 0.071819, 0.347944], +- [4.22, 0.051592, 0.424605], +- [4.93, 0.021219, 0.552775], +- [5.81, 0.023461, 0.541725], +- [7.21, 0.008959, 0.625982], +- [8.93, 0.005456, 0.651235], +- [10.0, 0.007108, 0.636489], +- [13.61, 0.002591, 0.681656], +- [26.99, 0.001163, 0.701094], +- [45.25, 0.000207, 0.726887] ++ [0.3000,0.4590,0.0586], ++ [0.4700,0.4300,0.0674], ++ [0.9000,0.3404,0.1095], ++ [1.2600,0.1925,0.2425], ++ [1.9500,0.1314,0.3195], ++ [2.7600,0.0604,0.4580], ++ [2.9500,-0.2085,1.2002], ++ [3.3300,0.0425,0.4597], ++ [3.8700,0.0592,0.4040], ++ [4.3100,0.0518,0.4327], ++ [5.0700,0.0264,0.5424], ++ [5.9300,0.0186,0.5818], ++ [7.3400,0.0078,0.6458], ++ [9.0800,0.0050,0.6664], ++ [10.0900,0.0022,0.6918], ++ [13.7400,0.0027,0.6868], ++ [27.1300,0.0009,0.7109], ++ [45.4300,-0.0038,0.8391] + ] + } + }, diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/__main__.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/__main__.py index 441c016cee9..7495e9f5d2c 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/__main__.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/__main__.py @@ -31,7 +31,7 @@ async def _main(cfg: TestConfig) -> None: await api.home() home_pos = await api.gantry_position(mount) attach_pos = helpers_ot3.get_slot_calibration_square_position_ot3(5) - attach_pos = attach_pos._replace(z=home_pos.z) + attach_pos = attach_pos._replace(z=home_pos.z - 100) if not api.hardware_pipettes[mount.to_mount()]: # FIXME: Do not home the plunger using the normal home method. # See section below where we use OT3Controller to home it. diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py index 0689e23d492..b781bb57447 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py @@ -225,7 +225,7 @@ async def _probe(distance: float, speed: float) -> float: else: print("skipping deck-pf") - await api.home_z() + await api.home_z(OT3Mount.LEFT) if not api.is_simulator: ui.get_user_ready("REMOVE probe") await api.remove_tip(OT3Mount.LEFT) diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_droplets.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_droplets.py index 81bf72bd432..cb01bd15f04 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_droplets.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_droplets.py @@ -182,7 +182,7 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None: # GATHER NOMINAL POSITIONS trash_nominal = get_trash_nominal() tip_rack_96_a1_nominal = get_tiprack_96_nominal() - tip_rack_partial_a1_nominal = get_tiprack_partial_nominal() + # tip_rack_partial_a1_nominal = get_tiprack_partial_nominal() reservoir_a1_nominal = get_reservoir_nominal() reservoir_a1_actual: Optional[Point] = None @@ -223,32 +223,32 @@ async def _find_reservoir_pos() -> None: report(section, "droplets-96-tips", [duration, CSVResult.from_bool(result)]) await _drop_tip(api, trash_nominal) - if not api.is_simulator: - ui.get_user_ready(f"REMOVE 96 tip-rack from slot #{TIP_RACK_96_SLOT}") - ui.get_user_ready(f"ADD partial tip-rack to slot #{TIP_RACK_PARTIAL_SLOT}") - - # SAVE PARTIAL TIP-RACK POSITION - ui.print_header("JOG to Partial-Tip RACK") - await helpers_ot3.move_to_arched_ot3( - api, OT3Mount.LEFT, tip_rack_partial_a1_nominal + Point(z=10) - ) - await helpers_ot3.jog_mount_ot3(api, OT3Mount.LEFT) - tip_rack_partial_a1_actual = await api.gantry_position(OT3Mount.LEFT) - - # TEST PARTIAL-TIP - for test_name, details in PARTIAL_TESTS.items(): - ui.print_header(f"{test_name.upper().replace('-', ' ')}") - pick_up_position = tip_rack_partial_a1_actual + details[0] - await helpers_ot3.move_to_arched_ot3( - api, OT3Mount.LEFT, pick_up_position + Point(z=50) - ) - await _partial_pick_up(api, pick_up_position, current=details[1]) - await _find_reservoir_pos() - assert reservoir_a1_actual - result, duration = await aspirate_and_wait( - api, reservoir_a1_actual, seconds=NUM_SECONDS_TO_WAIT - ) - report( - section, f"droplets-{test_name}", [duration, CSVResult.from_bool(result)] - ) - await _drop_tip(api, trash_nominal) + # if not api.is_simulator: + # ui.get_user_ready(f"REMOVE 96 tip-rack from slot #{TIP_RACK_96_SLOT}") + # ui.get_user_ready(f"ADD partial tip-rack to slot #{TIP_RACK_PARTIAL_SLOT}") + # + # # SAVE PARTIAL TIP-RACK POSITION + # ui.print_header("JOG to Partial-Tip RACK") + # await helpers_ot3.move_to_arched_ot3( + # api, OT3Mount.LEFT, tip_rack_partial_a1_nominal + Point(z=10) + # ) + # await helpers_ot3.jog_mount_ot3(api, OT3Mount.LEFT) + # tip_rack_partial_a1_actual = await api.gantry_position(OT3Mount.LEFT) + # + # # TEST PARTIAL-TIP + # for test_name, details in PARTIAL_TESTS.items(): + # ui.print_header(f"{test_name.upper().replace('-', ' ')}") + # pick_up_position = tip_rack_partial_a1_actual + details[0] + # await helpers_ot3.move_to_arched_ot3( + # api, OT3Mount.LEFT, pick_up_position + Point(z=50) + # ) + # await _partial_pick_up(api, pick_up_position, current=details[1]) + # await _find_reservoir_pos() + # assert reservoir_a1_actual + # result, duration = await aspirate_and_wait( + # api, reservoir_a1_actual, seconds=NUM_SECONDS_TO_WAIT + # ) + # report( + # section, f"droplets-{test_name}", [duration, CSVResult.from_bool(result)] + # ) + # await _drop_tip(api, trash_nominal) diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_jaws.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_jaws.py index 4531fd08007..a6298dd758b 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_jaws.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_jaws.py @@ -13,12 +13,18 @@ from hardware_testing.opentrons_api import helpers_ot3 from hardware_testing.opentrons_api.types import Axis, OT3Mount -RETRACT_MM = 0.25 +# from opentrons.hardware_control.backends.ot3utils import axis_convert + + +RETRACT_MM = 0.25 # 0.25 MAX_TRAVEL = 29.8 - RETRACT_MM # FIXME: what is the max travel? -ENDSTOP_OVERRUN_MM = 0.25 +ENDSTOP_OVERRUN_MM = ( + 0.25 # FIXME: position cannot go negative, can't go past limit switch +) ENDSTOP_OVERRUN_SPEED = 5 -SPEEDS_TO_TEST: List[float] = [3, 6, 9, 12, 15] +SPEEDS_TO_TEST: List[float] = [8, 12] CURRENTS_SPEEDS: Dict[float, List[float]] = { + 0.7: SPEEDS_TO_TEST, 1.5: SPEEDS_TO_TEST, } @@ -35,29 +41,61 @@ def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: speeds = CURRENTS_SPEEDS[current] for speed in sorted(speeds): tag = _get_test_tag(current, speed) - lines.append(CSVLine(tag, [bool, bool, CSVResult])) + lines.append(CSVLine(tag, [bool, bool, bool, CSVResult])) return lines -async def _check_if_jaw_is_aligned_with_endstop(api: OT3API) -> Tuple[bool, bool]: +async def _check_if_jaw_is_aligned_with_endstop(api: OT3API) -> bool: if not api.is_simulator: - pass_no_hit = ui.get_user_answer("are both endstop Lights OFF") + pass_no_hit = ui.get_user_answer("are both endstop Lights OFF?") else: pass_no_hit = True if not pass_no_hit: ui.print_error("endstop hit too early") - return pass_no_hit, False - # now purposefully hit the endstop - await helpers_ot3.move_tip_motor_relative_ot3( - api, -RETRACT_MM - ENDSTOP_OVERRUN_MM, speed=ENDSTOP_OVERRUN_SPEED - ) + + return pass_no_hit + + # This currently does not work since jaws cannot move above 0 + # # now purposefully hit the endstop + # await helpers_ot3.move_tip_motor_relative_ot3( + # api, -RETRACT_MM-ENDSTOP_OVERRUN_MM, speed=ENDSTOP_OVERRUN_SPEED + # ) + # print(await api.get_limit_switches()) + # if not api.is_simulator: + # pass_hit = ui.get_user_answer("are both endstop Lights ON?") + # else: + # pass_hit = True + # if not pass_hit: + # ui.print_error("endstop did not hit") + # return pass_no_hit, pass_hit + + +async def jaw_precheck(api: OT3API, ax: Axis, speed: float) -> Tuple[bool, bool]: + """Check the LEDs work and jaws are aligned.""" + # HOME + print("homing...") + await api.home([ax]) + # Check LEDs can turn on when homed if not api.is_simulator: - pass_hit = ui.get_user_answer("are both endstop Lights ON") + led_check = ui.get_user_answer("are both endstop Lights ON?") else: - pass_hit = True - if not pass_hit: - ui.print_error("endstop did not hit") - return pass_no_hit, pass_hit + led_check = True + if not led_check: + ui.print_error("Endstop LED or homing failure") + return (led_check, False) + + print(f"retracting {RETRACT_MM} mm") + await helpers_ot3.move_tip_motor_relative_ot3(api, RETRACT_MM, speed=speed) + # Check Jaws are aligned + if not api.is_simulator: + jaws_aligned = ui.get_user_answer("are both endstop Lights OFF?") + else: + jaws_aligned = True + + if not jaws_aligned: + ui.print_error("Jaws Misaligned") + + return led_check, jaws_aligned async def run(api: OT3API, report: CSVReport, section: str) -> None: @@ -67,11 +105,14 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None: default_current = settings.run_current default_speed = settings.max_speed - async def _save_result(tag: str) -> bool: - no_hit, hit = await _check_if_jaw_is_aligned_with_endstop(api) - result = CSVResult.from_bool(no_hit and hit) - report(section, tag, [no_hit, hit, result]) - return no_hit and hit + async def _save_result(tag: str, led_check: bool, jaws_aligned: bool) -> bool: + if led_check and jaws_aligned: + no_hit = await _check_if_jaw_is_aligned_with_endstop(api) + else: + no_hit = False + result = CSVResult.from_bool(led_check and jaws_aligned and no_hit) + report(section, tag, [led_check, jaws_aligned, no_hit, result]) + return led_check and jaws_aligned and no_hit await api.home_z(OT3Mount.LEFT) slot_5 = helpers_ot3.get_slot_calibration_square_position_ot3(5) @@ -84,37 +125,40 @@ async def _save_result(tag: str) -> bool: speeds = CURRENTS_SPEEDS[current] for speed in sorted(speeds, reverse=False): ui.print_header(f"CURRENT: {current}, SPEED: {speed}") - # HOME - print("homing...") - await api.home([ax]) - print(f"retracting {RETRACT_MM} mm") - await helpers_ot3.move_tip_motor_relative_ot3(api, RETRACT_MM, speed=speed) - print(f"lowering run-current to {current} amps") - await helpers_ot3.set_gantry_load_per_axis_motion_settings_ot3( - api, ax, default_max_speed=speed - ) - await helpers_ot3.set_gantry_load_per_axis_current_settings_ot3( - api, ax, run_current=current - ) - # MOVE DOWN then UP - print(f"moving down/up {MAX_TRAVEL} mm at {speed} mm/sec") - await helpers_ot3.move_tip_motor_relative_ot3( - api, MAX_TRAVEL, speed=speed, motor_current=current - ) - await helpers_ot3.move_tip_motor_relative_ot3( - api, -MAX_TRAVEL, speed=speed, motor_current=current - ) - # RESET CURRENTS, CHECK, then HOME - await helpers_ot3.set_gantry_load_per_axis_motion_settings_ot3( - api, ax, default_max_speed=default_speed - ) - await helpers_ot3.set_gantry_load_per_axis_current_settings_ot3( - api, ax, run_current=default_current + + led_check, jaws_aligned = await jaw_precheck(api, ax, speed) + + if led_check and jaws_aligned: + print(f"lowering run-current to {current} amps") + await helpers_ot3.set_gantry_load_per_axis_motion_settings_ot3( + api, ax, default_max_speed=speed + ) + await helpers_ot3.set_gantry_load_per_axis_current_settings_ot3( + api, ax, run_current=current + ) + # MOVE DOWN then UP + print(f"moving down/up {MAX_TRAVEL} mm at {speed} mm/sec") + await helpers_ot3.move_tip_motor_relative_ot3( + api, MAX_TRAVEL, speed=speed, motor_current=current + ) + await helpers_ot3.move_tip_motor_relative_ot3( + api, -MAX_TRAVEL, speed=speed, motor_current=current + ) + # RESET CURRENTS, CHECK + await helpers_ot3.set_gantry_load_per_axis_motion_settings_ot3( + api, ax, default_max_speed=default_speed + ) + await helpers_ot3.set_gantry_load_per_axis_current_settings_ot3( + api, ax, run_current=default_current + ) + passed = await _save_result( + _get_test_tag(current, speed), led_check, jaws_aligned ) - passed = await _save_result(_get_test_tag(current, speed)) - print("homing...") - await api.home([ax]) + if not passed and not api.is_simulator: print(f"current {current} failed") print("skipping any remaining speeds at this current") break + + print("homing...") + await api.home([ax]) diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_plunger.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_plunger.py index 94e12df49ce..1f802e47599 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_plunger.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_plunger.py @@ -14,9 +14,11 @@ from hardware_testing.opentrons_api.types import Axis, OT3Mount PLUNGER_MAX_SKIP_MM = 0.1 -SPEEDS_TO_TEST: List[float] = [5, 8, 12, 16, 20] +SPEEDS_TO_TEST: List[float] = [5, 15, 22] CURRENTS_SPEEDS: Dict[float, List[float]] = { - 2.2: SPEEDS_TO_TEST, + 0.6: SPEEDS_TO_TEST, + 0.7: SPEEDS_TO_TEST, + 0.8: SPEEDS_TO_TEST, } diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_pressure.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_pressure.py index 66cd0ecede7..cca8ab3a42d 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_pressure.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_pressure.py @@ -24,6 +24,25 @@ ASPIRATE_VOLUME = 2 PRESSURE_READINGS = ["open-pa", "sealed-pa", "aspirate-pa", "dispense-pa"] +THRESHOLDS = { + "open-pa": ( + -10, + 10, + ), + "sealed-pa": ( + -30, + 30, + ), + "aspirate-pa": ( + -600, + -400, + ), + "dispense-pa": ( + 2500, + 3500, + ), +} + def _get_test_tag(probe: InstrumentProbeType, reading: str) -> str: assert reading in PRESSURE_READINGS, f"{reading} not in PRESSURE_READINGS" @@ -64,6 +83,17 @@ async def _read_from_sensor( return sum(readings) / num_readings +def check_value(test_value: float, test_name: str) -> CSVResult: + """Determine if value is within pass limits.""" + low_limit = THRESHOLDS[test_name][0] + high_limit = THRESHOLDS[test_name][1] + + if low_limit < test_value and test_value < high_limit: + return CSVResult.PASS + else: + return CSVResult.FAIL + + async def run(api: OT3API, report: CSVReport, section: str) -> None: """Run.""" await api.home_z(OT3Mount.LEFT) @@ -84,8 +114,8 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None: ui.print_error(f"{probe} pressure sensor not working, skipping") continue print(f"open-pa: {open_pa}") - # FIXME: create stricter pass/fail criteria - report(section, _get_test_tag(probe, "open-pa"), [open_pa, CSVResult.PASS]) + open_result = check_value(open_pa, "open-pa") + report(section, _get_test_tag(probe, "open-pa"), [open_pa, open_result]) # SEALED-Pa sealed_pa = 0.0 @@ -102,8 +132,8 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None: ui.print_error(f"{probe} pressure sensor not working, skipping") break print(f"sealed-pa: {sealed_pa}") - # FIXME: create stricter pass/fail criteria - report(section, _get_test_tag(probe, "sealed-pa"), [sealed_pa, CSVResult.PASS]) + sealed_result = check_value(sealed_pa, "sealed-pa") + report(section, _get_test_tag(probe, "sealed-pa"), [sealed_pa, sealed_result]) # ASPIRATE-Pa aspirate_pa = 0.0 @@ -117,9 +147,9 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None: ui.print_error(f"{probe} pressure sensor not working, skipping") break print(f"aspirate-pa: {aspirate_pa}") - # FIXME: create stricter pass/fail criteria + aspirate_result = check_value(aspirate_pa, "aspirate-pa") report( - section, _get_test_tag(probe, "aspirate-pa"), [aspirate_pa, CSVResult.PASS] + section, _get_test_tag(probe, "aspirate-pa"), [aspirate_pa, aspirate_result] ) # DISPENSE-Pa @@ -134,9 +164,9 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None: ui.print_error(f"{probe} pressure sensor not working, skipping") break print(f"dispense-pa: {dispense_pa}") - # FIXME: create stricter pass/fail criteria + dispense_result = check_value(dispense_pa, "dispense-pa") report( - section, _get_test_tag(probe, "dispense-pa"), [dispense_pa, CSVResult.PASS] + section, _get_test_tag(probe, "dispense-pa"), [dispense_pa, dispense_result] ) if not api.is_simulator: diff --git a/hardware-testing/hardware_testing/scripts/tip_pick_up_lifetime_test.py b/hardware-testing/hardware_testing/scripts/tip_pick_up_lifetime_test.py new file mode 100644 index 00000000000..5a449f64998 --- /dev/null +++ b/hardware-testing/hardware_testing/scripts/tip_pick_up_lifetime_test.py @@ -0,0 +1,602 @@ +"""Lifetime test.""" +import argparse +import asyncio +import time + +import os +import sys +import termios +import tty +import json +from typing import Dict, Tuple +from hardware_testing.opentrons_api import types +from hardware_testing.opentrons_api import helpers_ot3 +from hardware_testing import data + +from hardware_testing.opentrons_api.types import OT3Mount, Axis, Point + +from opentrons.hardware_control.types import CriticalPoint +from opentrons.hardware_control.ot3api import OT3API + + +def _convert(seconds: float) -> str: + weeks, seconds = divmod(seconds, 7 * 24 * 60 * 60) + days, seconds = divmod(seconds, 24 * 60 * 60) + hours, seconds = divmod(seconds, 60 * 60) + minutes, seconds = divmod(seconds, 60) + + return "%02d:%02d:%02d:%02d:%02d" % (weeks, days, hours, minutes, seconds) + + +def _getch() -> str: + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(fd) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + +async def jog( + api: OT3API, position: Dict[Axis, float], cp: CriticalPoint +) -> Dict[Axis, float]: + """Move the gantry.""" + step_size = [0.01, 0.05, 0.1, 0.5, 1, 10, 20, 50] + step_length_index = 3 + xy_speed = 60 + za_speed = 65 + information_str = """ + Click >> i << to move up + Click >> k << to move down + Click >> a << to move left + Click >> d << to move right + Click >> w << to move forward + Click >> s << to move back + Click >> + << to Increase the length of each step + Click >> - << to decrease the length of each step + Click >> Enter << to save position + Click >> q << to quit the test script + """ + print(information_str) + while True: + input = _getch() + if input == "a": + # minus x direction + sys.stdout.flush() + await api.move_rel( + mount, Point(-step_size[step_length_index], 0, 0), speed=xy_speed + ) + + elif input == "d": + # plus x direction + sys.stdout.flush() + await api.move_rel( + mount, Point(step_size[step_length_index], 0, 0), speed=xy_speed + ) + + elif input == "w": + # minus y direction + sys.stdout.flush() + await api.move_rel( + mount, Point(0, step_size[step_length_index], 0), speed=xy_speed + ) + + elif input == "s": + # plus y direction + sys.stdout.flush() + await api.move_rel( + mount, Point(0, -step_size[step_length_index], 0), speed=xy_speed + ) + + elif input == "i": + sys.stdout.flush() + await api.move_rel( + mount, Point(0, 0, step_size[step_length_index]), speed=za_speed + ) + + elif input == "k": + sys.stdout.flush() + await api.move_rel( + mount, Point(0, 0, -step_size[step_length_index]), speed=za_speed + ) + + elif input == "q": + sys.stdout.flush() + print("TEST CANCELLED") + quit() + + elif input == "+": + sys.stdout.flush() + step_length_index = step_length_index + 1 + if step_length_index >= 7: + step_length_index = 7 + + elif input == "-": + sys.stdout.flush() + step_length_index = step_length_index - 1 + if step_length_index <= 0: + step_length_index = 0 + + elif input == "\r": + sys.stdout.flush() + position = await api.current_position_ot3( + mount, refresh=True, critical_point=cp + ) + print("\r\n") + return position + position = await api.current_position_ot3( + mount, refresh=True, critical_point=cp + ) + + print( + "Coordinates: ", + round(position[Axis.X], 2), + ",", + round(position[Axis.Y], 2), + ",", + round(position[Axis.by_mount(mount)], 2), + " Motor Step: ", + step_size[step_length_index], + end="", + ) + print("\r", end="") + + +async def _calibrate_tip_racks( + api: OT3API, + mount: OT3Mount, + slot_loc: Dict[str, Tuple[float, float, int]], + AXIS: Axis, +) -> Dict[str, Tuple[float, float, float]]: + print("Calibrate tip rack positions\n") + calibrated_slot_loc = {} + + for key in slot_loc.keys(): + print(f"TIP RACK IN SLOT {key}\n") + await api.move_to(mount, Point(slot_loc[key][0], slot_loc[key][1], 250.0)) + await api.move_to( + mount, Point(slot_loc[key][0], slot_loc[key][1], slot_loc[key][2]) + ) + # tip_rack_position = await helpers_ot3.jog_mount_ot3(api, mount) + cur_pos = await api.current_position_ot3( + mount, critical_point=CriticalPoint.NOZZLE + ) + tip_rack_position = await jog(api, cur_pos, CriticalPoint.NOZZLE) + calibrated_slot_loc[key] = ( + tip_rack_position[Axis.X], + tip_rack_position[Axis.Y], + tip_rack_position[AXIS], + ) + await api.home([AXIS]) + + json_object = json.dumps(calibrated_slot_loc, indent=0) + # ("/home/root/calibrated_slot_locations.json", "w") + with open("/data/testing_data/calibrated_slot_locations.json", "w") as outfile: + outfile.write(json_object) + return calibrated_slot_loc + + +async def _main(is_simulating: bool, mount: types.OT3Mount) -> None: # noqa: C901 + path = "/data/testing_data/calibrated_slot_locations.json" + api = await helpers_ot3.build_async_ot3_hardware_api(is_simulating=is_simulating) + await api.home() + await api.home_plunger(mount) + + test_tag = "" + test_robot = "Tip Pick Up/Plunger Lifetime" + if args.test_tag: + test_tag = input("Enter test tag:\n\t>> ") + if args.test_robot: + test_robot = input("Enter robot ID:\n\t>> ") + + if mount == OT3Mount.LEFT: + AXIS = Axis.Z_L + else: + AXIS = Axis.Z_R + + # TIP_RACKS = args.tip_rack_num # default: 12 + PICKUPS_PER_TIP = args.pick_up_num # default: 20 + COLUMNS = 12 + ROWS = 8 + CYCLES = 1 + + test_pip = api.get_attached_instrument(mount) + + print("mount.id:{}".format(test_pip["pipette_id"])) + + slot_loc = { + "A1": (13.42, 394.92, 110), + "A2": (177.32, 394.92, 110), + "A3": (341.03, 394.92, 110), + "B1": (13.42, 288.42, 110), + "B2": (177.32, 288.92, 110), + "B3": (341.03, 288.92, 110), + "C1": (13.42, 181.92, 110), + "C2": (177.32, 181.92, 110), + "C3": (341.03, 181.92, 110), + "D1": (13.42, 75.5, 110), + "D2": (177.32, 75.5, 110), + "D3": (341.03, 75.5, 110), + } + + run_id = data.create_run_id() + test_name = "tip-pick-up-lifetime-test" + if args.restart_flag: + if os.path.exists(path): + with open(path, "r") as openfile: + complete_dict = json.load(openfile) + file_name = complete_dict["csv_name"] + else: + print("Slot locations calibration file not found.\n") + calibrated_slot_loc = await _calibrate_tip_racks(api, mount, slot_loc, AXIS) + else: + file_name = data.create_file_name( + test_name=test_name, + run_id=run_id, + tag=test_tag, + ) + header = [ + "Time (W:H:M:S)", + "Test Robot", + "Test Pipette", + "Tip Rack", + "Tip Number", + "Total Tip Pick Ups", + "Tip Presence - Tip Pick Up (P/F)", + "Tip Presence - Tip Eject (P/F)", + "Total Failures", + ] + header_str = data.convert_list_to_csv_line(header) + data.append_data_to_file( + test_name=test_name, run_id=run_id, file_name=file_name, data=header_str + ) + + print("test_pip", test_pip) + if len(test_pip) == 0: + print(f"No pipette recognized on {mount.name} mount\n") + sys.exit() + + print(f"\nTest pipette: {test_pip['name']}\n") + + if "single" in test_pip["name"]: + check_tip_presence = True + if args.pick_up_num == 60: + PICKUPS_PER_TIP = 60 + else: + PICKUPS_PER_TIP = args.pick_up_num + else: + ROWS = 1 + CYCLES = 2 + if args.pick_up_num == 60: + PICKUPS_PER_TIP = 60 + else: + PICKUPS_PER_TIP = args.pick_up_num + check_tip_presence = True + + # just for save calibrate file + if args.only_calibrate: + await _calibrate_tip_racks(api, mount, slot_loc, AXIS) + return + + # optional arg for tip rack calibration + if not args.load_cal: + calibrated_slot_loc = await _calibrate_tip_racks(api, mount, slot_loc, AXIS) + else: + # import calibrated json file + # path = '/home/root/.opentrons/testing_data/calibrated_slot_locations.json' + print("Loading calibration data...\n") + path = "/data/testing_data/calibrated_slot_locations.json" + if os.path.exists(path): + with open( + "/data/testing_data/calibrated_slot_locations.json", "r" + ) as openfile: + calibrated_slot_loc = json.load(openfile) + else: + print("Slot locations calibration file not found.\n") + calibrated_slot_loc = await _calibrate_tip_racks(api, mount, slot_loc, AXIS) + print("Calibration data successfully loaded!\n") + + # add cfg start slot + start_slot = int(str(args.start_slot_row_col_totalTips_totalFailure).split(":")[0]) + start_row = int(str(args.start_slot_row_col_totalTips_totalFailure).split(":")[1]) + start_col = int(str(args.start_slot_row_col_totalTips_totalFailure).split(":")[2]) + total_tip_num = int( + str(args.start_slot_row_col_totalTips_totalFailure).split(":")[3] + ) + total_fail_num = int( + str(args.start_slot_row_col_totalTips_totalFailure).split(":")[4] + ) + + start_time = time.perf_counter() + elapsed_time = 0.0 + rack = start_slot - 1 + total_pick_ups = total_tip_num - 1 + total_failures = total_fail_num + start_tip_nums = 1 + + # load complete information + if args.restart_flag: + if os.path.exists(path): + with open( + "/data/testing_data/calibrated_slot_locations.json", "r" + ) as openfile: + print("load complete information...\n") + load_complete_dict = json.load(openfile) + CYCLES = CYCLES - (load_complete_dict["cycle"] - 1) + rack = load_complete_dict["slot_num"] - 1 + total_pick_ups = load_complete_dict["total_tip_pick_up"] + total_failures = load_complete_dict["total_failure"] + start_slot = rack + 1 + start_row = load_complete_dict["row"] + start_col = load_complete_dict["col"] + start_tip_nums = load_complete_dict["tip_num"] + 1 + else: + print("Failed to load complete information.\n") + + start_slot = start_slot % 12 # fix bug for cycles + for i in range(start_slot - 1): + del calibrated_slot_loc[list(calibrated_slot_loc)[0]] + + for i in range(CYCLES): + print(f"\n=========== Cycle {i + 1}/{CYCLES} ===========\n") + if i > 0: + stop_time = time.perf_counter() + print("Replace tips before continuing test.") + input('\n\t>> Press "Enter" to continue.') + resume_time = time.perf_counter() + elapsed_time += resume_time - stop_time + print(f"Elapsed time: {_convert(resume_time-stop_time)}\n") + for key_index, key in enumerate(calibrated_slot_loc.keys()): + if key_index >= 12: + break + rack += 1 + await api.home([AXIS]) + await api.move_to( + mount, + Point(calibrated_slot_loc[key][0], calibrated_slot_loc[key][1], 250.0), + ) + await api.move_to( + mount, + Point( + calibrated_slot_loc[key][0], + calibrated_slot_loc[key][1], + calibrated_slot_loc[key][2] + 5, + ), + ) + for col in range(COLUMNS): + if col < start_col - 1: + continue + await api.move_to( + mount, + Point( + calibrated_slot_loc[key][0] + 9 * col, + calibrated_slot_loc[key][1], + calibrated_slot_loc[key][2] + 5, + ), + ) + for row in range(ROWS): + if col == start_col - 1 and row < start_row - 1: + continue + print("=================================\n") + print(f"Tip rack in slot {key}, Column: {col+1}, Row: {row+1}\n") + if "p1000" in test_pip["name"]: + if "1" in key: + tip_len = 95.6 + elif "2" in key: + tip_len = 58.35 + elif "3" in key: + tip_len = 57.9 + else: + tip_len = 57.9 + print(f"Tip length: {tip_len} mm\n") + if row > 0: + await api.move_rel(mount, delta=Point(y=-9)) + await api.move_to( + mount, + Point( + calibrated_slot_loc[key][0] + 9 * col, + calibrated_slot_loc[key][1] - 9 * row, + calibrated_slot_loc[key][2] + 5, + ), + ) + await api.move_to( + mount, + Point( + calibrated_slot_loc[key][0] + 9 * col, + calibrated_slot_loc[key][1] - 9 * row, + calibrated_slot_loc[key][2], + ), + ) + start_pos = await api.gantry_position(mount) + for pick_up in range(PICKUPS_PER_TIP): + await api.move_to(mount, start_pos) + if ( + col == start_col - 1 + and row == start_row - 1 + and pick_up < start_tip_nums - 1 + ): + continue + print("= = = = = = = = = = = = = = = = =\n") + print(f"Tip Pick Up #{pick_up+1}\n") + print("Picking up tip...\n") + await api.pick_up_tip(mount, tip_len) + total_pick_ups += 1 + + # check tip presence after tip pick up + + if check_tip_presence: + tip_presence_pick_up = await api.get_tip_presence_status( + mount + ) + # pick_up_keys = list(tip_presence_pick_up.keys()) + if ( + tip_presence_pick_up == 1 + ): # (tip_presence_pick_up[pick_up_keys[0]]): + print("\t>> Tip detected!\n") + tip_presence_pick_up_flag = True + else: + tip_presence_eject = await api.get_tip_presence_status( + mount + ) + print("GET Tip presenc{}".format(tip_presence_eject)) + total_failures += 1 + tip_presence_pick_up_flag = False + print( + f"\t>> Tip not detected! Total failures: {total_failures}\n" + ) + else: + tip_presence_pick_up_flag = False + + # move plunger from blowout to top, back to blow_out + ( + top_pos, + bottom_pos, + _, + _, + ) = helpers_ot3.get_plunger_positions_ot3(api, mount) + + print("Move to bottom plunger position\n") + await helpers_ot3.move_plunger_absolute_ot3( + api, mount, bottom_pos + ) + print("Move to top plunger position\n") + await helpers_ot3.move_plunger_absolute_ot3(api, mount, top_pos) + print("Move to bottom plunger position\n") + await helpers_ot3.move_plunger_absolute_ot3( + api, mount, bottom_pos + ) + + # check tip presence after tip drop + print("Dropping tip...\n") + await api.drop_tip(mount) + if check_tip_presence: + tip_presence_eject = await api.get_tip_presence_status( + mount + ) + # drop_tip_keys = list(tip_presence_eject.keys()) + if ( + tip_presence_eject == 1 + ): # (tip_presence_eject[drop_tip_keys[0]]): + print("GET Tip presenc{}".format(tip_presence_eject)) + print("\t>> Tip detected after ejecting tip!\n") + print("\t>> Canceling script...\n") + total_failures += 1 + tip_presence_eject_flag = True + else: + print("\t>> Tip not detected!\n") + tip_presence_eject_flag = False + else: + tip_presence_eject_flag = False + + # save test data and continue loop/exit based on tip eject success + + cycle_data = [ + _convert(time.perf_counter() - elapsed_time - start_time), + test_robot, + test_pip["pipette_id"], + rack, + pick_up + 1, + total_pick_ups, + tip_presence_pick_up_flag, + tip_presence_eject_flag, + total_failures, + ] + cycle_data_str = data.convert_list_to_csv_line(cycle_data) + data.append_data_to_file( + test_name=test_name, + run_id=run_id, + file_name=file_name, + data=cycle_data_str, + ) + + # save the last complate information + + if os.path.exists(path): + with open( + "/data/testing_data/calibrated_slot_locations.json", "r" + ) as openfile: + print("Recording...\n") + calibrated_slot_loc = json.load(openfile) + complete_dict = { + "cycle": i + 1, + "slot_num": rack, + "tip_num": pick_up + 1, + "total_tip_pick_up": total_pick_ups, + "total_failure": total_failures, + "col": col + 1, + "row": row + 1, + "csv_name": file_name, + } + calibrated_slot_loc.update(complete_dict) + with open( + "/data/testing_data/calibrated_slot_locations.json", + "w", + ) as writefile: + json.dump(calibrated_slot_loc, writefile) + + else: + print("Slot locations calibration file not found.\n") + print("Failed to record complete information.\n") + + if tip_presence_eject_flag: + await api.home() + sys.exit() + + # adjust row increment + print("Moving to next row...\n") + # await api.move_rel(mount, delta=Point(z=5)) + + # adjust column increment + await api.move_to( + mount, + Point( + calibrated_slot_loc[key][0] + 9 * col, + calibrated_slot_loc[key][1] - 9 * row, + calibrated_slot_loc[key][2] + 5, + ), + ) + print("Moving to next column...\n") + + # release start + start_col = 1 + start_row = 1 + start_tip_nums = 1 + + print("=================================\n") + print(f"\nCYCLE {i+1} COMPLETE\n") + await api.home() + await api.home_plunger(mount) + + print("=================================\n") + print("1/4 LIFETIME TEST COMPLETE\n") + await api.home() + + +if __name__ == "__main__": + mount_options = { + "left": types.OT3Mount.LEFT, + "right": types.OT3Mount.RIGHT, + "gripper": types.OT3Mount.GRIPPER, + } + parser = argparse.ArgumentParser() + parser.add_argument("--simulate", action="store_true") + parser.add_argument( + "--mount", type=str, choices=list(mount_options.keys()), default="left" + ) + parser.add_argument("--pick_up_num", type=int, default=60) + # parser.add_argument("--tip_rack_num", type=int, default=12) + parser.add_argument("--load_cal", action="store_true") + parser.add_argument("--test_tag", action="store_true") + parser.add_argument("--test_robot", action="store_true") + parser.add_argument("--restart_flag", action="store_true") + parser.add_argument( + "--start_slot_row_col_totalTips_totalFailure", type=str, default="1:1:1:1:0" + ) + parser.add_argument("--only_calibrate", action="store_true") + # parser.add_argument("--check_tip", action="store_true") + args = parser.parse_args() + mount = mount_options[args.mount] + + asyncio.run(_main(args.simulate, mount)) From 095d1e3e7de7321c01ec7b57ae486c9a8de1e860 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Wed, 15 Nov 2023 15:03:42 -0500 Subject: [PATCH 20/46] fix(hardware_testing): Few minor fixes to enable p50m photometric (#13846) * Few minor fixes for p50m photometric * don't reduce current for 8 channel photometric tests * Change submerge depth to 1.5 instead of 2.5 * don't mess with the defaults --------- Co-authored-by: Mehdi Zaidi <55298601+meh-di@users.noreply.github.com> --- hardware-testing/Makefile | 6 ++++++ hardware-testing/hardware_testing/gravimetric/__main__.py | 1 + hardware-testing/hardware_testing/gravimetric/config.py | 1 + .../hardware_testing/gravimetric/execute_photometric.py | 4 ++-- hardware-testing/hardware_testing/gravimetric/helpers.py | 3 ++- 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/hardware-testing/Makefile b/hardware-testing/Makefile index d8af8fcca85..6054cd9fcfa 100755 --- a/hardware-testing/Makefile +++ b/hardware-testing/Makefile @@ -97,6 +97,12 @@ test-photometric-single: $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 1 --tip 50 --dye-well-col-offset 3 -$(MAKE) remove-patches-gravimetric +.PHONY: test-photometric-multi +test-photometric-multi: + -$(MAKE) apply-patches-gravimetric + $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 8 --tip 50 + -$(MAKE) remove-patches-gravimetric + .PHONY: test-photometric test-photometric: -$(MAKE) apply-patches-gravimetric diff --git a/hardware-testing/hardware_testing/gravimetric/__main__.py b/hardware-testing/hardware_testing/gravimetric/__main__.py index 4c54bb3a71e..8e9a7b1ff45 100644 --- a/hardware-testing/hardware_testing/gravimetric/__main__.py +++ b/hardware-testing/hardware_testing/gravimetric/__main__.py @@ -195,6 +195,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": args.pipette, "left", args.increment, + args.photometric, args.gantry_speed if not args.photometric else None, ) pipette_tag = helpers._get_tag_from_pipette( diff --git a/hardware-testing/hardware_testing/gravimetric/config.py b/hardware-testing/hardware_testing/gravimetric/config.py index a1febb6ddc7..e64259df908 100644 --- a/hardware-testing/hardware_testing/gravimetric/config.py +++ b/hardware-testing/hardware_testing/gravimetric/config.py @@ -301,6 +301,7 @@ def _get_liquid_probe_settings( }, ConfigType.photometric: { 1: 8, + 8: 12, 96: 5, }, } diff --git a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py index c2c5f314428..5b36acc46f3 100644 --- a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py +++ b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py @@ -319,7 +319,7 @@ def execute_trials( trial_count = 0 for volume in trials.keys(): ui.print_title(f"{volume} uL") - if cfg.pipette_channels == 1 and not resources.ctx.is_simulating(): + if cfg.pipette_channels != 96 and not resources.ctx.is_simulating(): ui.get_user_ready( f"put PLATE with prepped column {cfg.photoplate_column_offset} and remove SEAL" ) @@ -336,7 +336,7 @@ def execute_trials( resources.ctx, resources.pipette, cfg, location=next_tip_location ) _run_trial(trial) - if not trial.ctx.is_simulating() and trial.channel_count == 1: + if not trial.ctx.is_simulating() and trial.channel_count != 96: ui.get_user_ready("add SEAL to plate and remove from DECK") diff --git a/hardware-testing/hardware_testing/gravimetric/helpers.py b/hardware-testing/hardware_testing/gravimetric/helpers.py index 7836e069218..88d9586a50e 100644 --- a/hardware-testing/hardware_testing/gravimetric/helpers.py +++ b/hardware-testing/hardware_testing/gravimetric/helpers.py @@ -352,6 +352,7 @@ def _load_pipette( pipette_volume: int, pipette_mount: str, increment: bool, + photometric: bool, gantry_speed: Optional[int] = None, ) -> InstrumentContext: pip_name = f"flex_{pipette_channels}channel_{pipette_volume}" @@ -372,7 +373,7 @@ def _load_pipette( # NOTE: 8ch QC testing means testing 1 channel at a time, # so we need to decrease the pick-up current to work with 1 tip. - if pipette.channels == 8 and not increment: + if pipette.channels == 8 and not increment and not photometric: hwapi = get_sync_hw_api(ctx) mnt = OT3Mount.LEFT if pipette_mount == "left" else OT3Mount.RIGHT hwpipette: Pipette = hwapi.hardware_pipettes[mnt.to_mount()] From 67d3c55acbe2c71646b4dac275f148fdde3be77b Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 15 Nov 2023 15:58:17 -0500 Subject: [PATCH 21/46] chore: update update-from-usb fake release notes (#13984) * chore: update update-from-usb fake release notes * Flex --- app-shell-odd/src/system-update/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app-shell-odd/src/system-update/index.ts b/app-shell-odd/src/system-update/index.ts index c0ea2f49ee3..15f64186e0d 100644 --- a/app-shell-odd/src/system-update/index.ts +++ b/app-shell-odd/src/system-update/index.ts @@ -212,7 +212,9 @@ const getVersionFromZipIfValid = (path: string): Promise => const fakeReleaseNotesForMassStorage = (version: string): string => ` # Opentrons Robot Software Version ${version} -This update is from a USB mass storage device connected to your flex, and release notes cannot be shown. +This update is from a USB mass storage device connected to your Flex, and release notes cannot be shown. + +Don't remove the USB mass storage device while the update is in progress. ` export const getLatestMassStorageUpdateFiles = ( From 3ffe91b8ed18b023969d441fd1c001c0ddd3a14b Mon Sep 17 00:00:00 2001 From: Jamey H Date: Wed, 15 Nov 2023 15:59:16 -0500 Subject: [PATCH 22/46] app(fix): Connect to Wifi Network via USB (#13988) Closes RQA-1777, RQA-1781 * fix(app): update wifi connect error handling Wifi error handling must account for the status in addition to the presence of an error object in order to catch all potential errors. * fix(app): pass form data to appshellrequestor * refactor(app): add a wifi-specific placeholder error message when no error message is supplied POST requests via USB for wifi configuration due not return an error message when failed. Adds a reasonable generic error message for all failed wifi requests without a more specific error message. --- app/src/molecules/modals/ErrorModal.tsx | 4 +- .../ConnectNetwork/ResultModal.tsx | 19 +++++--- .../__tests__/ResultModal.test.tsx | 43 +++++++++++++++++-- .../Devices/RobotSettings/SelectNetwork.tsx | 2 +- .../__tests__/SelectNetwork.test.tsx | 9 ++-- app/src/redux/robot-api/http.ts | 1 + 6 files changed, 62 insertions(+), 16 deletions(-) diff --git a/app/src/molecules/modals/ErrorModal.tsx b/app/src/molecules/modals/ErrorModal.tsx index b31952b9f2a..3951eb132a9 100644 --- a/app/src/molecules/modals/ErrorModal.tsx +++ b/app/src/molecules/modals/ErrorModal.tsx @@ -11,7 +11,7 @@ interface Props { description: string close?: () => unknown closeUrl?: string - error: { message?: string; [key: string]: unknown } + error: { message?: string; [key: string]: unknown } | null } const DEFAULT_HEADING = 'Unexpected Error' @@ -37,7 +37,7 @@ export function ErrorModal(props: Props): JSX.Element {

    - {error.message ?? AN_UNKNOWN_ERROR_OCCURRED} + {error?.message ?? AN_UNKNOWN_ERROR_OCCURRED}

    {description}

    diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx index 82b35f2b04a..1a043507912 100644 --- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx @@ -1,25 +1,28 @@ import * as React from 'react' import { AlertModal, SpinnerModal } from '@opentrons/components' + +import * as Copy from './i18n' import { ErrorModal } from '../../../../molecules/modals' import { DISCONNECT } from './constants' -import * as Copy from './i18n' +import { PENDING, FAILURE } from '../../../../redux/robot-api' import type { NetworkChangeType } from './types' +import type { RequestStatus } from '../../../../redux/robot-api/types' export interface ResultModalProps { type: NetworkChangeType ssid: string | null - isPending: boolean + requestStatus: RequestStatus error: { message?: string; [key: string]: unknown } | null onClose: () => unknown } export const ResultModal = (props: ResultModalProps): JSX.Element => { - const { type, ssid, isPending, error, onClose } = props + const { type, ssid, requestStatus, error, onClose } = props const isDisconnect = type === DISCONNECT - if (isPending) { + if (requestStatus === PENDING) { const message = isDisconnect ? Copy.DISCONNECTING_FROM_NETWORK(ssid) : Copy.CONNECTING_TO_NETWORK(ssid) @@ -27,7 +30,7 @@ export const ResultModal = (props: ResultModalProps): JSX.Element => { return } - if (error) { + if (error || requestStatus === FAILURE) { const heading = isDisconnect ? Copy.UNABLE_TO_DISCONNECT : Copy.UNABLE_TO_CONNECT @@ -38,11 +41,15 @@ export const ResultModal = (props: ResultModalProps): JSX.Element => { const retryMessage = !isDisconnect ? ` ${Copy.CHECK_YOUR_CREDENTIALS}.` : '' + const placeholderError = { + message: `Likely incorrect network password. ${Copy.CHECK_YOUR_CREDENTIALS}.`, + } + return ( ) diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/ResultModal.test.tsx b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/ResultModal.test.tsx index 3cf51d805b4..00f7e9ce389 100644 --- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/ResultModal.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/ResultModal.test.tsx @@ -5,6 +5,7 @@ import { AlertModal, SpinnerModal } from '@opentrons/components' import { ErrorModal } from '../../../../../molecules/modals' import { ResultModal } from '../ResultModal' import { DISCONNECT, CONNECT, JOIN_OTHER } from '../constants' +import { PENDING, FAILURE, SUCCESS } from '../../../../../redux/robot-api' import type { ShallowWrapper } from 'enzyme' import type { ResultModalProps } from '../ResultModal' @@ -31,7 +32,7 @@ describe("SelectNetwork's ResultModal", () => { type, ssid, error: null, - isPending: true, + requestStatus: PENDING, onClose: handleClose, }} /> @@ -99,7 +100,7 @@ describe("SelectNetwork's ResultModal", () => { type, ssid, error: null, - isPending: false, + requestStatus: SUCCESS, onClose: handleClose, }} /> @@ -188,7 +189,7 @@ describe("SelectNetwork's ResultModal", () => { type, ssid, error, - isPending: false, + requestStatus: FAILURE, onClose: handleClose, }} /> @@ -249,5 +250,41 @@ describe("SelectNetwork's ResultModal", () => { expect(alert.prop('close')).toEqual(handleClose) expect(alert.prop('error')).toEqual(error) }) + + it('displays an ErrorModal with appropriate failure message if the status is failure and no error message is given', () => { + const render: ( + type: ResultModalProps['type'], + ssid?: ResultModalProps['ssid'] + ) => ShallowWrapper> = ( + type, + ssid = mockSsid + ) => { + return shallow( + + ) + } + + const wrapper = render(JOIN_OTHER, null) + const alert = wrapper.find(ErrorModal) + + expect(alert).toHaveLength(1) + expect(alert.prop('heading')).toEqual('Unable to connect to Wi-Fi') + expect(alert.prop('description')).toEqual( + expect.stringContaining('unable to connect to Wi-Fi') + ) + expect(alert.prop('close')).toEqual(handleClose) + expect(alert.prop('error')).toEqual({ + message: + 'Likely incorrect network password. Please double-check your network credentials.', + }) + }) }) }) diff --git a/app/src/organisms/Devices/RobotSettings/SelectNetwork.tsx b/app/src/organisms/Devices/RobotSettings/SelectNetwork.tsx index b3151d0418e..600b4c186d6 100644 --- a/app/src/organisms/Devices/RobotSettings/SelectNetwork.tsx +++ b/app/src/organisms/Devices/RobotSettings/SelectNetwork.tsx @@ -104,7 +104,7 @@ export const SelectNetwork = ({ ', () => { expect(resultModal.props()).toEqual({ type: Constants.CONNECT, ssid: mockConfigure.ssid, - isPending: true, + requestStatus: PENDING, error: null, onClose: expect.any(Function), }) @@ -278,7 +279,7 @@ describe('', () => { expect(resultModal.props()).toEqual({ type: Constants.CONNECT, ssid: mockConfigure.ssid, - isPending: false, + requestStatus: SUCCESS, error: null, onClose: expect.any(Function), }) @@ -307,7 +308,7 @@ describe('', () => { expect(resultModal.props()).toEqual({ type: Constants.CONNECT, ssid: mockConfigure.ssid, - isPending: false, + requestStatus: FAILURE, error: { message: 'oh no!' }, onClose: expect.any(Function), }) diff --git a/app/src/redux/robot-api/http.ts b/app/src/redux/robot-api/http.ts index 71840066a9f..7dd41b87da6 100644 --- a/app/src/redux/robot-api/http.ts +++ b/app/src/redux/robot-api/http.ts @@ -60,6 +60,7 @@ export function fetchRobotApi( headers: options.headers, method, url, + data: options.body, }) ).pipe( map(response => ({ From b0125339cac996bdde56fb8fd4c205f84954f3ce Mon Sep 17 00:00:00 2001 From: Ed Cormany Date: Wed, 15 Nov 2023 16:57:34 -0500 Subject: [PATCH 23/46] docs(api): make |apiLevel| substitution a string constant (#13991) --- api/docs/v2/conf.py | 7 ++----- api/docs/v2/new_protocol_api.rst | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/api/docs/v2/conf.py b/api/docs/v2/conf.py index 29524982522..cb896e72d89 100644 --- a/api/docs/v2/conf.py +++ b/api/docs/v2/conf.py @@ -96,13 +96,10 @@ # setup the code block substitution extension to auto-update apiLevel extensions += ['sphinx-prompt', 'sphinx_substitution_extensions'] -# get the max API level -from opentrons.protocol_api import MAX_SUPPORTED_VERSION # noqa -max_apiLevel = str(MAX_SUPPORTED_VERSION) - # use rst_prolog to hold the subsitution +# update the apiLevel value whenever a new minor version is released rst_prolog = f""" -.. |apiLevel| replace:: {max_apiLevel} +.. |apiLevel| replace:: 2.15 .. |release| replace:: {release} """ diff --git a/api/docs/v2/new_protocol_api.rst b/api/docs/v2/new_protocol_api.rst index 9ddd0ca6407..5166d53f035 100644 --- a/api/docs/v2/new_protocol_api.rst +++ b/api/docs/v2/new_protocol_api.rst @@ -17,7 +17,7 @@ Protocols and Instruments .. autoclass:: opentrons.protocol_api.InstrumentContext :members: - :exclude-members: delay + :exclude-members: delay, configure_nozzle_layout, prepare_to_aspirate .. autoclass:: opentrons.protocol_api.Liquid From c80798dca9b8d435a0d5c342a9ef7e81525a8438 Mon Sep 17 00:00:00 2001 From: Alise Au <20424172+ahiuchingau@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:48:40 -0500 Subject: [PATCH 24/46] refactor(shared-data): update 96-channel liquid handling functions (#13993) Updates from hardware for 96 channel ul/mm --------- Co-authored-by: Seth Foster --- .../ninety_six_channel/p1000/default/1_0.json | 328 +++++------------- .../ninety_six_channel/p1000/default/3_0.json | 328 +++++------------- .../ninety_six_channel/p1000/default/3_3.json | 328 +++++------------- .../ninety_six_channel/p1000/default/3_4.json | 328 +++++------------- .../ninety_six_channel/p1000/default/3_5.json | 328 +++++------------- 5 files changed, 400 insertions(+), 1240 deletions(-) diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/1_0.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/1_0.json index a3794ba520e..8ca9dc4ece4 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/1_0.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/1_0.json @@ -20,96 +20,36 @@ "aspirate": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, "dispense": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, @@ -134,94 +74,34 @@ "aspirate": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, "dispense": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, @@ -246,94 +126,46 @@ "aspirate": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, "dispense": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_0.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_0.json index a3794ba520e..8ca9dc4ece4 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_0.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_0.json @@ -20,96 +20,36 @@ "aspirate": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, "dispense": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, @@ -134,94 +74,34 @@ "aspirate": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, "dispense": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, @@ -246,94 +126,46 @@ "aspirate": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, "dispense": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json index a3794ba520e..8ca9dc4ece4 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json @@ -20,96 +20,36 @@ "aspirate": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, "dispense": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, @@ -134,94 +74,34 @@ "aspirate": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, "dispense": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, @@ -246,94 +126,46 @@ "aspirate": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, "dispense": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json index a3794ba520e..8ca9dc4ece4 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json @@ -20,96 +20,36 @@ "aspirate": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, "dispense": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, @@ -134,94 +74,34 @@ "aspirate": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, "dispense": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, @@ -246,94 +126,46 @@ "aspirate": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, "dispense": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json index a3794ba520e..8ca9dc4ece4 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json @@ -20,96 +20,36 @@ "aspirate": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, "dispense": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, @@ -134,94 +74,34 @@ "aspirate": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, "dispense": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, @@ -246,94 +126,46 @@ "aspirate": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, "dispense": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, From db5c7393113a6f66adef3b938c1235deadb8e33c Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 15 Nov 2023 17:56:08 -0500 Subject: [PATCH 25/46] fix(shared-data,api): Separate lt and ht pickup and dropoff (#13962) These configs had a couple things to change: - pickup and drop actually represent configuration for two different actions: the press fit / plunger eject sequence that the low throughput pipettes do, and the cam action sequence that the high throughput pipettes do when handling a full rack. information was actually overloaded and lost: there was only room for one speed for both separate actions. By separating the configs for each action, we get that information back - fold in the per tip current information into the pickup config so it looks a bit more natural and also can be distributed between the action kinds Now that we have separate configuration elements for picking up tips with press fit vs cam action and dropping them with plunger eject and cam action, we need to use them in a way that is nicely integrated with the nozzle configuration stuff. That means that the plan methods should check the current nozzle configuration and use the appropriate config and behavior. When we're calibrating an OT-2 multi channel, we do it with just one tip. This means we have to drop the current to have the tip attached with the appropriate amount of force. We now have a bunch of other code focused on doing exactly that tied up with the partial tip pickup mechanics, so use it instead. We had implemented partial tip configurations by doing math on the offsets of all the nozzles. This works, but has two downsides: 1. It's sort of hard to read unless you have a good mental model of the physical layouts 2. It relies on doing exact numerical comparisons In general, I think it is never a good idea to try and go _from_ numbers _to_ symbolic knowledge. It's too vulnerable to small perturbations, and it's too annoying to read. Instead, let's encode the structure of the pipette nozzles in the pipette geometry defs by writing down the rows and columns of the pipette layout. Then, we can do our partial configuration logic based on the rows and columns of the pipette. We now do not hit a bug when we request a partial configuration that doesn't include A1; and we now do not rely on array stride math; and we now technically support arbitrary axis-aligned rectangles (at least on this layer - definitely verboten above). --------- Co-authored-by: Laura Cox <31892318+Laura-Danielle@users.noreply.github.com> --- .../instruments/ot2/pipette.py | 21 +- .../instruments/ot2/pipette_handler.py | 34 +- .../instruments/ot3/pipette.py | 46 +- .../instruments/ot3/pipette_handler.py | 74 +- .../hardware_control/nozzle_manager.py | 324 +++--- api/src/opentrons/hardware_control/ot3api.py | 9 +- .../resources/pipette_data_provider.py | 5 +- .../instruments/test_nozzle_manager.py | 1002 ++++++++++++++--- .../hardware_control/test_pipette_handler.py | 68 +- .../commands/test_configure_nozzle_layout.py | 369 ++++-- .../hardware_testing/gravimetric/helpers.py | 3 +- .../opentrons_api/helpers_ot3.py | 12 +- .../robot_server/robot/calibration/util.py | 34 +- .../robot/calibration/deck/test_user_flow.py | 9 +- .../js/__tests__/pipetteSchemaV2.test.ts | 3 +- .../2/general/eight_channel/p10/1_0.json | 44 +- .../2/general/eight_channel/p10/1_3.json | 44 +- .../2/general/eight_channel/p10/1_4.json | 44 +- .../2/general/eight_channel/p10/1_5.json | 44 +- .../2/general/eight_channel/p10/1_6.json | 44 +- .../2/general/eight_channel/p1000/1_0.json | 41 +- .../2/general/eight_channel/p1000/3_0.json | 41 +- .../2/general/eight_channel/p1000/3_3.json | 41 +- .../2/general/eight_channel/p1000/3_4.json | 41 +- .../2/general/eight_channel/p1000/3_5.json | 41 +- .../2/general/eight_channel/p20/2_0.json | 44 +- .../2/general/eight_channel/p20/2_1.json | 44 +- .../2/general/eight_channel/p300/1_0.json | 44 +- .../2/general/eight_channel/p300/1_3.json | 44 +- .../2/general/eight_channel/p300/1_4.json | 44 +- .../2/general/eight_channel/p300/1_5.json | 44 +- .../2/general/eight_channel/p300/2_0.json | 44 +- .../2/general/eight_channel/p300/2_1.json | 44 +- .../2/general/eight_channel/p50/1_0.json | 44 +- .../2/general/eight_channel/p50/1_3.json | 44 +- .../2/general/eight_channel/p50/1_4.json | 44 +- .../2/general/eight_channel/p50/1_5.json | 44 +- .../2/general/eight_channel/p50/3_0.json | 41 +- .../2/general/eight_channel/p50/3_3.json | 41 +- .../2/general/eight_channel/p50/3_4.json | 41 +- .../2/general/eight_channel/p50/3_5.json | 41 +- .../general/ninety_six_channel/p1000/1_0.json | 70 +- .../general/ninety_six_channel/p1000/3_0.json | 75 +- .../general/ninety_six_channel/p1000/3_3.json | 70 +- .../general/ninety_six_channel/p1000/3_4.json | 71 +- .../general/ninety_six_channel/p1000/3_5.json | 70 +- .../2/general/single_channel/p10/1_0.json | 30 +- .../2/general/single_channel/p10/1_3.json | 30 +- .../2/general/single_channel/p10/1_4.json | 30 +- .../2/general/single_channel/p10/1_5.json | 30 +- .../2/general/single_channel/p1000/1_0.json | 30 +- .../2/general/single_channel/p1000/1_3.json | 30 +- .../2/general/single_channel/p1000/1_4.json | 30 +- .../2/general/single_channel/p1000/1_5.json | 30 +- .../2/general/single_channel/p1000/2_0.json | 30 +- .../2/general/single_channel/p1000/2_1.json | 30 +- .../2/general/single_channel/p1000/2_2.json | 30 +- .../2/general/single_channel/p1000/3_0.json | 26 +- .../2/general/single_channel/p1000/3_3.json | 26 +- .../2/general/single_channel/p1000/3_4.json | 26 +- .../2/general/single_channel/p1000/3_5.json | 26 +- .../2/general/single_channel/p20/2_0.json | 30 +- .../2/general/single_channel/p20/2_1.json | 30 +- .../2/general/single_channel/p20/2_2.json | 30 +- .../2/general/single_channel/p300/1_0.json | 30 +- .../2/general/single_channel/p300/1_3.json | 30 +- .../2/general/single_channel/p300/1_4.json | 30 +- .../2/general/single_channel/p300/1_5.json | 30 +- .../2/general/single_channel/p300/2_0.json | 30 +- .../2/general/single_channel/p300/2_1.json | 30 +- .../2/general/single_channel/p50/1_0.json | 30 +- .../2/general/single_channel/p50/1_3.json | 30 +- .../2/general/single_channel/p50/1_4.json | 30 +- .../2/general/single_channel/p50/1_5.json | 30 +- .../2/general/single_channel/p50/3_0.json | 26 +- .../2/general/single_channel/p50/3_3.json | 26 +- .../2/general/single_channel/p50/3_4.json | 26 +- .../2/general/single_channel/p50/3_5.json | 26 +- .../2/geometry/eight_channel/p10/1_0.json | 22 + .../2/geometry/eight_channel/p10/1_3.json | 22 + .../2/geometry/eight_channel/p10/1_4.json | 22 + .../2/geometry/eight_channel/p10/1_5.json | 22 + .../2/geometry/eight_channel/p10/1_6.json | 22 + .../2/geometry/eight_channel/p1000/1_0.json | 22 + .../2/geometry/eight_channel/p1000/3_0.json | 22 + .../2/geometry/eight_channel/p1000/3_3.json | 22 + .../2/geometry/eight_channel/p1000/3_4.json | 22 + .../2/geometry/eight_channel/p1000/3_5.json | 22 + .../2/geometry/eight_channel/p20/2_0.json | 22 + .../2/geometry/eight_channel/p20/2_1.json | 22 + .../2/geometry/eight_channel/p300/1_0.json | 22 + .../2/geometry/eight_channel/p300/1_3.json | 22 + .../2/geometry/eight_channel/p300/1_4.json | 22 + .../2/geometry/eight_channel/p300/1_5.json | 22 + .../2/geometry/eight_channel/p300/2_0.json | 22 + .../2/geometry/eight_channel/p300/2_1.json | 22 + .../2/geometry/eight_channel/p50/1_0.json | 22 + .../2/geometry/eight_channel/p50/1_3.json | 22 + .../2/geometry/eight_channel/p50/1_4.json | 22 + .../2/geometry/eight_channel/p50/1_5.json | 22 + .../2/geometry/eight_channel/p50/3_0.json | 22 + .../2/geometry/eight_channel/p50/3_3.json | 22 + .../2/geometry/eight_channel/p50/3_4.json | 22 + .../2/geometry/eight_channel/p50/3_5.json | 22 + .../ninety_six_channel/p1000/1_0.json | 188 ++++ .../ninety_six_channel/p1000/3_0.json | 188 ++++ .../ninety_six_channel/p1000/3_3.json | 188 ++++ .../ninety_six_channel/p1000/3_4.json | 188 ++++ .../ninety_six_channel/p1000/3_5.json | 188 ++++ .../2/geometry/single_channel/p10/1_0.json | 2 + .../2/geometry/single_channel/p10/1_3.json | 2 + .../2/geometry/single_channel/p10/1_4.json | 2 + .../2/geometry/single_channel/p10/1_5.json | 2 + .../2/geometry/single_channel/p1000/1_0.json | 2 + .../2/geometry/single_channel/p1000/1_3.json | 2 + .../2/geometry/single_channel/p1000/1_4.json | 2 + .../2/geometry/single_channel/p1000/1_5.json | 2 + .../2/geometry/single_channel/p1000/2_0.json | 2 + .../2/geometry/single_channel/p1000/2_1.json | 2 + .../2/geometry/single_channel/p1000/2_2.json | 2 + .../2/geometry/single_channel/p1000/3_0.json | 2 + .../2/geometry/single_channel/p1000/3_3.json | 2 + .../2/geometry/single_channel/p1000/3_4.json | 2 + .../2/geometry/single_channel/p1000/3_5.json | 2 + .../2/geometry/single_channel/p20/2_0.json | 2 + .../2/geometry/single_channel/p20/2_1.json | 2 + .../2/geometry/single_channel/p20/2_2.json | 2 + .../2/geometry/single_channel/p300/1_0.json | 2 + .../2/geometry/single_channel/p300/1_3.json | 2 + .../2/geometry/single_channel/p300/1_4.json | 2 + .../2/geometry/single_channel/p300/1_5.json | 2 + .../2/geometry/single_channel/p300/2_0.json | 2 + .../2/geometry/single_channel/p300/2_1.json | 2 + .../2/geometry/single_channel/p50/1_0.json | 2 + .../2/geometry/single_channel/p50/1_3.json | 2 + .../2/geometry/single_channel/p50/1_4.json | 2 + .../2/geometry/single_channel/p50/1_5.json | 2 + .../2/geometry/single_channel/p50/3_0.json | 2 + .../2/geometry/single_channel/p50/3_3.json | 2 + .../2/geometry/single_channel/p50/3_4.json | 2 + .../2/geometry/single_channel/p50/3_5.json | 2 + .../schemas/2/pipetteGeometrySchema.json | 46 + .../schemas/2/pipettePropertiesSchema.json | 162 ++- .../pipette/load_data.py | 75 +- .../pipette/model_constants.py | 76 +- .../pipette/mutable_configurations.py | 107 +- .../pipette/pipette_definition.py | 136 ++- .../pipette/scripts/build_json_script.py | 60 +- .../python/tests/pipette/test_load_data.py | 1 + .../pipette/test_mutable_configurations.py | 2 +- 150 files changed, 4712 insertions(+), 1943 deletions(-) diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py index f5b9aa3fd29..b529956e569 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py @@ -12,7 +12,8 @@ PlungerPositions, MotorConfigurations, SupportedTipsDefinition, - TipHandlingConfigurations, + PickUpTipConfigurations, + DropTipConfigurations, PipetteModelVersionType, PipetteNameType, PipetteLiquidPropertiesDefinition, @@ -109,10 +110,7 @@ def __init__( ) self._nozzle_offset = self._config.nozzle_offset self._nozzle_manager = ( - nozzle_manager.NozzleConfigurationManager.build_from_nozzlemap( - self._config.nozzle_map, - self._config.partial_tip_configurations.per_tip_pickup_current, - ) + nozzle_manager.NozzleConfigurationManager.build_from_config(self._config) ) self._current_volume = 0.0 self._working_volume = float(self._liquid_class.max_volume) @@ -230,17 +228,15 @@ def plunger_motor_current(self) -> MotorConfigurations: return self._config.plunger_motor_configurations @property - def pick_up_configurations(self) -> TipHandlingConfigurations: + def pick_up_configurations(self) -> PickUpTipConfigurations: return self._config.pick_up_tip_configurations @pick_up_configurations.setter - def pick_up_configurations( - self, pick_up_configs: TipHandlingConfigurations - ) -> None: + def pick_up_configurations(self, pick_up_configs: PickUpTipConfigurations) -> None: self._pick_up_configurations = pick_up_configs @property - def drop_configurations(self) -> TipHandlingConfigurations: + def drop_configurations(self) -> DropTipConfigurations: return self._config.drop_tip_configurations @property @@ -292,10 +288,7 @@ def reset_state(self) -> None: self._tip_overlap_lookup = self.liquid_class.tip_overlap_dictionary self._nozzle_manager = ( - nozzle_manager.NozzleConfigurationManager.build_from_nozzlemap( - self._config.nozzle_map, - self._config.partial_tip_configurations.per_tip_pickup_current, - ) + nozzle_manager.NozzleConfigurationManager.build_from_config(self._config) ) def reset_pipette_offset(self, mount: Mount, to_default: bool) -> None: diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py index 67596cea790..d2a36f19e85 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py @@ -768,16 +768,16 @@ def plan_check_pick_up_tip( # type: ignore[no-untyped-def] self._ihp_log.debug(f"Picking up tip on {mount.name}") if presses is None or presses < 0: - checked_presses = instrument.pick_up_configurations.presses + checked_presses = instrument.pick_up_configurations.press_fit.presses else: checked_presses = presses if not increment or increment < 0: - check_incr = instrument.pick_up_configurations.increment + check_incr = instrument.pick_up_configurations.press_fit.increment else: check_incr = increment - pick_up_speed = instrument.pick_up_configurations.speed + pick_up_speed = instrument.pick_up_configurations.press_fit.speed def build_presses() -> Iterator[Tuple[float, float]]: # Press the nozzle into the tip number of times, @@ -785,7 +785,7 @@ def build_presses() -> Iterator[Tuple[float, float]]: for i in range(checked_presses): # move nozzle down into the tip press_dist = ( - -1.0 * instrument.pick_up_configurations.distance + -1.0 * instrument.pick_up_configurations.press_fit.distance + -1.0 * check_incr * i ) # move nozzle back up @@ -808,7 +808,9 @@ def add_tip_to_instr() -> None: current={ Axis.by_mount( mount - ): instrument.nozzle_manager.get_tip_configuration_current() + ): instrument.pick_up_configurations.press_fit.current_by_tip_count[ + instrument.nozzle_manager.current_configuration.tip_count + ] }, speed=pick_up_speed, relative_down=top_types.Point(0, 0, press_dist), @@ -817,7 +819,7 @@ def add_tip_to_instr() -> None: for press_dist, backup_dist in build_presses() ], shake_off_list=self._build_pickup_shakes(instrument), - retract_target=instrument.pick_up_configurations.distance + retract_target=instrument.pick_up_configurations.press_fit.distance + check_incr * checked_presses + 2, ), @@ -837,7 +839,9 @@ def add_tip_to_instr() -> None: current={ Axis.by_mount( mount - ): instrument.nozzle_manager.get_tip_configuration_current() + ): instrument.pick_up_configurations.press_fit.current_by_tip_count[ + instrument.nozzle_manager.current_configuration.tip_count + ] }, speed=pick_up_speed, relative_down=top_types.Point(0, 0, press_dist), @@ -846,7 +850,7 @@ def add_tip_to_instr() -> None: for press_dist, backup_dist in build_presses() ], shake_off_list=self._build_pickup_shakes(instrument), - retract_target=instrument.pick_up_configurations.distance + retract_target=instrument.pick_up_configurations.press_fit.distance + check_incr * checked_presses + 2, ), @@ -923,9 +927,13 @@ def plan_check_drop_tip( # type: ignore[no-untyped-def] ): instrument = self.get_pipette(mount) + if not instrument.drop_configurations.plunger_eject: + raise CommandPreconditionViolated( + f"Pipette {instrument.name} on {mount.name} has no plunger eject configuration" + ) bottom = instrument.plunger_positions.bottom droptip = instrument.plunger_positions.drop_tip - speed = instrument.drop_configurations.speed + speed = instrument.drop_configurations.plunger_eject.speed shakes: List[Tuple[top_types.Point, Optional[float]]] = [] if Quirks.dropTipShake in instrument.config.quirks: diameter = instrument.current_tiprack_diameter @@ -941,7 +949,11 @@ def _remove_tips() -> None: bottom, droptip, {Axis.of_plunger(mount): instrument.plunger_motor_current.run}, - {Axis.of_plunger(mount): instrument.drop_configurations.current}, + { + Axis.of_plunger( + mount + ): instrument.drop_configurations.plunger_eject.current + }, speed, home_after, (Axis.of_plunger(mount),), @@ -971,7 +983,7 @@ def _remove_tips() -> None: { Axis.of_main_tool_actuator( mount - ): instrument.drop_configurations.current + ): instrument.drop_configurations.plunger_eject.current }, speed, home_after, diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py index 1f6dd0b4b59..a89ba96290e 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py @@ -11,7 +11,10 @@ PlungerPositions, MotorConfigurations, SupportedTipsDefinition, - TipHandlingConfigurations, + PickUpTipConfigurations, + PressFitPickUpTipConfiguration, + CamActionPickUpTipConfiguration, + DropTipConfigurations, PlungerHomingConfigurations, PipetteNameType, PipetteModelVersionType, @@ -95,10 +98,7 @@ def __init__( ) self._nozzle_offset = self._config.nozzle_offset self._nozzle_manager = ( - nozzle_manager.NozzleConfigurationManager.build_from_nozzlemap( - self._config.nozzle_map, - self._config.partial_tip_configurations.per_tip_pickup_current, - ) + nozzle_manager.NozzleConfigurationManager.build_from_config(self._config) ) self._current_volume = 0.0 self._working_volume = float(self._liquid_class.max_volume) @@ -185,13 +185,11 @@ def plunger_motor_current(self) -> MotorConfigurations: return self._plunger_motor_current @property - def pick_up_configurations(self) -> TipHandlingConfigurations: + def pick_up_configurations(self) -> PickUpTipConfigurations: return self._pick_up_configurations @pick_up_configurations.setter - def pick_up_configurations( - self, pick_up_configs: TipHandlingConfigurations - ) -> None: + def pick_up_configurations(self, pick_up_configs: PickUpTipConfigurations) -> None: self._pick_up_configurations = pick_up_configs @property @@ -199,7 +197,7 @@ def plunger_homing_configurations(self) -> PlungerHomingConfigurations: return self._plunger_homing_configurations @property - def drop_configurations(self) -> TipHandlingConfigurations: + def drop_configurations(self) -> DropTipConfigurations: return self._drop_configurations @property @@ -258,10 +256,7 @@ def reset_state(self) -> None: self._tip_overlap_lookup = self.liquid_class.tip_overlap_dictionary self._nozzle_manager = ( - nozzle_manager.NozzleConfigurationManager.build_from_nozzlemap( - self._config.nozzle_map, - self._config.partial_tip_configurations.per_tip_pickup_current, - ) + nozzle_manager.NozzleConfigurationManager.build_from_config(self._config) ) def reset_pipette_offset(self, mount: OT3Mount, to_default: bool) -> None: @@ -510,14 +505,6 @@ def tip_presence_responses(self) -> int: # TODO: put this in shared-data return 2 if self.channels > 8 else 1 - @property - def connect_tiprack_distance_mm(self) -> float: - return self._config.connect_tiprack_distance_mm - - @property - def end_tip_action_retract_distance_mm(self) -> float: - return self._config.end_tip_action_retract_distance_mm - # Cache max is chosen somewhat arbitrarily. With a float is input we don't # want this to unbounded. @functools.lru_cache(maxsize=100) @@ -672,6 +659,21 @@ def set_tip_type(self, tip_type: pip_types.PipetteTipType) -> None: self._tip_overlap_lookup = self.liquid_class.tip_overlap_dictionary self._working_volume = min(tip_type.value, self.liquid_class.max_volume) + def get_pick_up_configuration_for_tip_count( + self, count: int + ) -> Union[CamActionPickUpTipConfiguration, PressFitPickUpTipConfiguration]: + for config in ( + self._config.pick_up_tip_configurations.press_fit, + self._config.pick_up_tip_configurations.cam_action, + ): + if not config: + continue + if count in config.current_by_tip_count: + return config + raise CommandPreconditionViolated( + message=f"No pick up tip configuration for {count} tips", + ) + def _reload_and_check_skip( new_config: PipetteConfigurations, diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py index 36b41e3e816..006417484d4 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py @@ -22,6 +22,8 @@ ) from opentrons_shared_data.pipette.pipette_definition import ( liquid_class_for_volume_between_default_and_defaultlowvolume, + PressFitPickUpTipConfiguration, + CamActionPickUpTipConfiguration, ) from opentrons import types as top_types @@ -729,7 +731,7 @@ def build_one_shake() -> List[Tuple[top_types.Point, Optional[float]]]: return [] - def plan_ht_pick_up_tip(self) -> TipActionSpec: + def plan_ht_pick_up_tip(self, tip_count: int) -> TipActionSpec: # Prechecks: ready for pickup tip and press/increment are valid mount = OT3Mount.LEFT instrument = self.get_pipette(mount) @@ -737,25 +739,32 @@ def plan_ht_pick_up_tip(self) -> TipActionSpec: raise UnexpectedTipAttachError("pick_up_tip", instrument.name, mount.name) self._ihp_log.debug(f"Picking up tip on {mount.name}") + pick_up_config = instrument.get_pick_up_configuration_for_tip_count(tip_count) + if not isinstance(pick_up_config, CamActionPickUpTipConfiguration): + raise CommandPreconditionViolated( + f"Low-throughput pick up tip got wrong config for {instrument.name} on {mount.name}" + ) + tip_motor_moves = self._build_tip_motor_moves( - prep_move_dist=instrument.pick_up_configurations.prep_move_distance, - clamp_move_dist=instrument.pick_up_configurations.distance, - prep_move_speed=instrument.pick_up_configurations.prep_move_speed, - clamp_move_speed=instrument.pick_up_configurations.speed, + prep_move_dist=pick_up_config.prep_move_distance, + clamp_move_dist=pick_up_config.distance, + prep_move_speed=pick_up_config.prep_move_speed, + clamp_move_speed=pick_up_config.speed, plunger_current=instrument.plunger_motor_current.run, - tip_motor_current=instrument.nozzle_manager.get_tip_configuration_current(), + tip_motor_current=pick_up_config.current_by_tip_count[tip_count], ) return TipActionSpec( tip_action_moves=tip_motor_moves, shake_off_moves=[], - z_distance_to_tiprack=(-1 * instrument.connect_tiprack_distance_mm), - ending_z_retract_distance=instrument.end_tip_action_retract_distance_mm, + z_distance_to_tiprack=(-1 * pick_up_config.connect_tiprack_distance_mm), + ending_z_retract_distance=instrument.config.end_tip_action_retract_distance_mm, ) def plan_lt_pick_up_tip( self, mount: OT3Mount, + tip_count: int, presses: Optional[int], increment: Optional[float], ) -> TipActionSpec: @@ -765,17 +774,22 @@ def plan_lt_pick_up_tip( raise UnexpectedTipAttachError("pick_up_tip", instrument.name, mount.name) self._ihp_log.debug(f"Picking up tip on {mount.name}") + pick_up_config = instrument.get_pick_up_configuration_for_tip_count(tip_count) + if not isinstance(pick_up_config, PressFitPickUpTipConfiguration): + raise CommandPreconditionViolated( + f"Low-throughput pick up tip got wrong config for {instrument.name} on {mount.name}" + ) if presses is None or presses < 0: - checked_presses = instrument.pick_up_configurations.presses + checked_presses = pick_up_config.presses else: checked_presses = presses if not increment or increment < 0: - check_incr = instrument.pick_up_configurations.increment + check_incr = pick_up_config.increment else: check_incr = increment - pick_up_speed = instrument.pick_up_configurations.speed + pick_up_speed = pick_up_config.speed def build_presses() -> List[TipActionMoveSpec]: # Press the nozzle into the tip number of times, @@ -783,18 +797,15 @@ def build_presses() -> List[TipActionMoveSpec]: press_moves = [] for i in range(checked_presses): # move nozzle down into the tip - press_dist = ( - -1.0 * instrument.pick_up_configurations.distance - + -1.0 * check_incr * i - ) + press_dist = -1.0 * pick_up_config.distance + -1.0 * check_incr * i press_moves.append( TipActionMoveSpec( distance=press_dist, speed=pick_up_speed, currents={ - Axis.by_mount( - mount - ): instrument.nozzle_manager.get_tip_configuration_current() + Axis.by_mount(mount): pick_up_config.current_by_tip_count[ + tip_count + ] }, ) ) @@ -840,15 +851,17 @@ def plan_lt_drop_tip( mount: OT3Mount, ) -> TipActionSpec: instrument = self.get_pipette(mount) - + config = instrument.drop_configurations.plunger_eject + if not config: + raise CommandPreconditionViolated( + f"No plunger-eject drop tip configurations for {instrument.name} on {mount.name}" + ) drop_seq = [ TipActionMoveSpec( distance=instrument.plunger_positions.drop_tip, - speed=instrument.drop_configurations.speed, + speed=config.speed, currents={ - Axis.of_main_tool_actuator( - mount - ): instrument.drop_configurations.current, + Axis.of_main_tool_actuator(mount): config.current, }, ), TipActionMoveSpec( @@ -870,14 +883,19 @@ def plan_lt_drop_tip( def plan_ht_drop_tip(self) -> TipActionSpec: mount = OT3Mount.LEFT instrument = self.get_pipette(mount) + config = instrument.drop_configurations.cam_action + if not config: + raise CommandPreconditionViolated( + f"No cam-action drop tip configurations for {instrument.name} on {mount.name}" + ) drop_seq = self._build_tip_motor_moves( - prep_move_dist=instrument.drop_configurations.prep_move_distance, - clamp_move_dist=instrument.drop_configurations.distance, - prep_move_speed=instrument.drop_configurations.prep_move_speed, - clamp_move_speed=instrument.drop_configurations.speed, + prep_move_dist=config.prep_move_distance, + clamp_move_dist=config.distance, + prep_move_speed=config.prep_move_speed, + clamp_move_speed=config.speed, plunger_current=instrument.plunger_motor_current.run, - tip_motor_current=instrument.drop_configurations.current, + tip_motor_current=config.current, ) return TipActionSpec( diff --git a/api/src/opentrons/hardware_control/nozzle_manager.py b/api/src/opentrons/hardware_control/nozzle_manager.py index 781d2c55bc8..4841a1fdee8 100644 --- a/api/src/opentrons/hardware_control/nozzle_manager.py +++ b/api/src/opentrons/hardware_control/nozzle_manager.py @@ -1,17 +1,41 @@ -from typing import Dict, List, Optional, Any, Sequence -from typing_extensions import Final +from typing import Dict, List, Optional, Any, Sequence, Iterator, Tuple, cast from dataclasses import dataclass from collections import OrderedDict from enum import Enum +from itertools import chain from opentrons.hardware_control.types import CriticalPoint from opentrons.types import Point -from opentrons_shared_data.errors import ( - ErrorCodes, - GeneralError, +from opentrons_shared_data.pipette.pipette_definition import ( + PipetteGeometryDefinition, + PipetteRowDefinition, ) +from opentrons_shared_data.errors import ErrorCodes, GeneralError, PythonException -INTERNOZZLE_SPACING = 9 + +def _nozzle_names_by_row(rows: List[PipetteRowDefinition]) -> Iterator[str]: + for row in rows: + for nozzle in row.ordered_nozzles: + yield nozzle + + +def _row_or_col_index_for_nozzle( + row_or_col: "OrderedDict[str, List[str]]", nozzle: str +) -> int: + for index, row_or_col_contents in enumerate(row_or_col.values()): + if nozzle in row_or_col_contents: + return index + raise KeyError(nozzle) + + +def _row_col_indices_for_nozzle( + rows: "OrderedDict[str, List[str]]", + cols: "OrderedDict[str, List[str]]", + nozzle: str, +) -> Tuple[int, int]: + return _row_or_col_index_for_nozzle(rows, nozzle), _row_or_col_index_for_nozzle( + cols, nozzle + ) class NozzleConfigurationType(Enum): @@ -24,69 +48,93 @@ class NozzleConfigurationType(Enum): COLUMN = "COLUMN" ROW = "ROW" - QUADRANT = "QUADRANT" SINGLE = "SINGLE" FULL = "FULL" + SUBRECT = "SUBRECT" @classmethod def determine_nozzle_configuration( cls, - nozzle_difference: Point, - physical_nozzlemap_length: int, - current_nozzlemap_length: int, + physical_rows: "OrderedDict[str, List[str]]", + current_rows: "OrderedDict[str, List[str]]", + physical_cols: "OrderedDict[str, List[str]]", + current_cols: "OrderedDict[str, List[str]]", ) -> "NozzleConfigurationType": """ Determine the nozzle configuration based on the starting and ending nozzle. - - :param nozzle_difference: the difference between the back - left and front right nozzle - :param physical_nozzlemap_length: integer representing the - length of the default physical configuration of the pipette. - :param current_nozzlemap_length: integer representing the - length of the current physical configuration of the pipette. - :return : nozzle configuration type """ - if physical_nozzlemap_length == current_nozzlemap_length: + if physical_rows == current_rows and physical_cols == current_cols: return NozzleConfigurationType.FULL - - if nozzle_difference == Point(0, 0, 0): + if len(current_rows) == 1 and len(current_cols) == 1: return NozzleConfigurationType.SINGLE - elif nozzle_difference[0] == 0: - return NozzleConfigurationType.COLUMN - elif nozzle_difference[1] == 0: + if len(current_rows) == 1: return NozzleConfigurationType.ROW - else: - return NozzleConfigurationType.QUADRANT + if len(current_cols) == 1: + return NozzleConfigurationType.COLUMN + return NozzleConfigurationType.SUBRECT @dataclass class NozzleMap: """ - Nozzle Map. + A NozzleMap instance represents a specific configuration of active nozzles on a pipette. - A data store class that can build - and store nozzle configurations - based on the physical default - nozzle map of the pipette and - the requested starting/ending tips. + It exposes properties of the configuration like the configuration's front-right, front-left, + back-left and starting nozzles as well as a map of all the nozzles active in the configuration. + + Because NozzleMaps represent configurations directly, the properties of the NozzleMap may not + match the properties of the physical pipette. For instance, a NozzleMap for a single channel + configuration of an 8-channel pipette - say, A1 only - will have its front left, front right, + and active channels all be A1, while the physical configuration would have the front right + channel be H1. """ - back_left: str - front_right: str starting_nozzle: str + #: The nozzle that automated operations that count nozzles should start at + # these are really ordered dicts but you can't say that even in quotes because pydantic needs to + # evaluate them to generate serdes code so please only use ordered dicts here map_store: Dict[str, Point] + #: A map of all of the nozzles active in this configuration + rows: Dict[str, List[str]] + #: A map of all the rows active in this configuration + columns: Dict[str, List[str]] + #: A map of all the columns active in this configuration configuration: NozzleConfigurationType + #: The kind of configuration this is def __str__(self) -> str: return f"back_left_nozzle: {self.back_left} front_right_nozzle: {self.front_right} configuration: {self.configuration}" + @property + def back_left(self) -> str: + """The backest, leftest (i.e. back if it's a column, left if it's a row) nozzle of the configuration. + + Note: This is the value relevant for this particular configuration, and it may not represent the back left nozzle + of the underlying physical pipette. For instance, the back-left nozzle of a configuration representing nozzles + D7 to H12 of a 96-channel pipette is D7, which is not the back-left nozzle of the physical pipette (A1). + """ + return next(iter(self.rows.values()))[0] + + @property + def front_right(self) -> str: + """The frontest, rightest (i.e. front if it's a column, right if it's a row) nozzle of the configuration. + + Note: This is the value relevant for this configuration, not the physical pipette. See the note on back_left. + """ + return next(reversed(list(self.rows.values())))[-1] + @property def starting_nozzle_offset(self) -> Point: + """The position of the starting nozzle.""" return self.map_store[self.starting_nozzle] @property def xy_center_offset(self) -> Point: + """The position of the geometrical center of all nozzles in the configuration. + + Note: This is the value relevant fro this configuration, not the physical pipette. See the note on back_left. + """ difference = self.map_store[self.front_right] - self.map_store[self.back_left] return self.map_store[self.back_left] + Point( difference[0] / 2, difference[1] / 2, 0 @@ -94,104 +142,83 @@ def xy_center_offset(self) -> Point: @property def front_nozzle_offset(self) -> Point: + """The offset for the front_left nozzle.""" # front left-most nozzle of the 96 channel in a given configuration # and front nozzle of the 8 channel - if self.starting_nozzle == self.front_right: - return self.map_store[self.front_right] - map_store_list = list(self.map_store.values()) - starting_idx = map_store_list.index(self.map_store[self.back_left]) - difference = self.map_store[self.back_left] - self.map_store[self.front_right] - y_rows_length = int(difference[1] // INTERNOZZLE_SPACING) - return map_store_list[starting_idx + y_rows_length] + front_left = next(iter(self.columns.values()))[-1] + return self.map_store[front_left] @property def tip_count(self) -> int: + """The total number of active nozzles in the configuration, and thus the number of tips that will be picked up.""" return len(self.map_store) @classmethod def build( cls, - physical_nozzle_map: Dict[str, Point], + physical_nozzles: "OrderedDict[str, Point]", + physical_rows: "OrderedDict[str, List[str]]", + physical_columns: "OrderedDict[str, List[str]]", starting_nozzle: str, back_left_nozzle: str, front_right_nozzle: str, - origin_nozzle: Optional[str] = None, ) -> "NozzleMap": - difference = ( - physical_nozzle_map[front_right_nozzle] - - physical_nozzle_map[back_left_nozzle] - ) - x_columns_length = int(abs(difference[0] // INTERNOZZLE_SPACING)) + 1 - y_rows_length = int(abs(difference[1] // INTERNOZZLE_SPACING)) + 1 - - map_store_list = list(physical_nozzle_map.items()) - - if origin_nozzle: - origin_difference = ( - physical_nozzle_map[back_left_nozzle] - - physical_nozzle_map[origin_nozzle] + try: + back_left_row_index, back_left_column_index = _row_col_indices_for_nozzle( + physical_rows, physical_columns, back_left_nozzle ) - starting_col = int(abs(origin_difference[0] // INTERNOZZLE_SPACING)) - else: - starting_col = 0 + except KeyError as e: + raise IncompatibleNozzleConfiguration( + message=f"No entry for back left nozzle {e} in pipette", + wrapping=[PythonException(e)], + ) from e + try: + ( + front_right_row_index, + front_right_column_index, + ) = _row_col_indices_for_nozzle( + physical_rows, physical_columns, front_right_nozzle + ) + except KeyError as e: + raise IncompatibleNozzleConfiguration( + message=f"No entry for front right nozzle {e} in pipette", + wrapping=[PythonException(e)], + ) from e + + correct_rows_with_all_columns = list(physical_rows.items())[ + back_left_row_index : front_right_row_index + 1 + ] + correct_rows = [ + ( + row_name, + row_entries[back_left_column_index : front_right_column_index + 1], + ) + for row_name, row_entries in correct_rows_with_all_columns + ] + rows = OrderedDict(correct_rows) + correct_columns_with_all_rows = list(physical_columns.items())[ + back_left_column_index : front_right_column_index + 1 + ] + correct_columns = [ + (col_name, col_entries[back_left_row_index : front_right_row_index + 1]) + for col_name, col_entries in correct_columns_with_all_rows + ] + columns = OrderedDict(correct_columns) + map_store = OrderedDict( - { - k: v - for i in range(x_columns_length) - for k, v in map_store_list[ - (i + starting_col) * 8 : y_rows_length * ((i + starting_col) + 1) - ] - } + (nozzle, physical_nozzles[nozzle]) for nozzle in chain(*rows.values()) ) + return cls( - back_left=back_left_nozzle, - front_right=front_right_nozzle, starting_nozzle=starting_nozzle, map_store=map_store, + rows=rows, + columns=columns, configuration=NozzleConfigurationType.determine_nozzle_configuration( - difference, len(physical_nozzle_map), len(map_store) + physical_rows, rows, physical_columns, columns ), ) - @staticmethod - def validate_nozzle_configuration( - back_left_nozzle: str, - front_right_nozzle: str, - default_configuration: "NozzleMap", - current_configuration: Optional["NozzleMap"] = None, - ) -> None: - """ - Validate nozzle configuration. - """ - if back_left_nozzle > front_right_nozzle: - raise IncompatibleNozzleConfiguration( - message=f"Back left nozzle {back_left_nozzle} provided is not to the back or left of {front_right_nozzle}.", - detail={ - "current_nozzle_configuration": current_configuration, - "requested_back_left_nozzle": back_left_nozzle, - "requested_front_right_nozzle": front_right_nozzle, - }, - ) - if not default_configuration.map_store.get(back_left_nozzle): - raise IncompatibleNozzleConfiguration( - message=f"Starting nozzle {back_left_nozzle} does not exist in the nozzle map.", - detail={ - "current_nozzle_configuration": current_configuration, - "requested_back_left_nozzle": back_left_nozzle, - "requested_front_right_nozzle": front_right_nozzle, - }, - ) - - if not default_configuration.map_store.get(front_right_nozzle): - raise IncompatibleNozzleConfiguration( - message=f"Ending nozzle {front_right_nozzle} does not exist in the nozzle map.", - detail={ - "current_nozzle_configuration": current_configuration, - "requested_back_left_nozzle": back_left_nozzle, - "requested_front_right_nozzle": front_right_nozzle, - }, - ) - class IncompatibleNozzleConfiguration(GeneralError): """Error raised if nozzle configuration is incompatible with the currently loaded pipette.""" @@ -215,33 +242,39 @@ class NozzleConfigurationManager: def __init__( self, nozzle_map: NozzleMap, - pick_up_current_map: Dict[int, float], ) -> None: self._physical_nozzle_map = nozzle_map self._current_nozzle_configuration = nozzle_map - self._pick_up_current_map: Final[Dict[int, float]] = pick_up_current_map @classmethod - def build_from_nozzlemap( - cls, - nozzle_map: Dict[str, List[float]], - pick_up_current_map: Dict[int, float], + def build_from_config( + cls, pipette_geometry: PipetteGeometryDefinition ) -> "NozzleConfigurationManager": - - sorted_nozzlemap = list(nozzle_map.keys()) - sorted_nozzlemap.sort(key=lambda x: int(x[1::])) - nozzle_map_ordereddict: Dict[str, Point] = OrderedDict( - {k: Point(*nozzle_map[k]) for k in sorted_nozzlemap} + sorted_nozzle_map = OrderedDict( + ( + (k, Point(*pipette_geometry.nozzle_map[k])) + for k in _nozzle_names_by_row(pipette_geometry.ordered_rows) + ) ) - first_nozzle = next(iter(list(nozzle_map_ordereddict.keys()))) - last_nozzle = next(reversed(list(nozzle_map_ordereddict.keys()))) + sorted_rows = OrderedDict( + (entry.key, entry.ordered_nozzles) + for entry in pipette_geometry.ordered_rows + ) + sorted_cols = OrderedDict( + (entry.key, entry.ordered_nozzles) + for entry in pipette_geometry.ordered_columns + ) + back_left = next(iter(sorted_rows.values()))[0] + front_right = next(reversed(list(sorted_rows.values())))[-1] starting_nozzle_config = NozzleMap.build( - nozzle_map_ordereddict, - starting_nozzle=first_nozzle, - back_left_nozzle=first_nozzle, - front_right_nozzle=last_nozzle, + physical_nozzles=sorted_nozzle_map, + physical_rows=sorted_rows, + physical_columns=sorted_cols, + starting_nozzle=back_left, + back_left_nozzle=back_left, + front_right_nozzle=front_right, ) - return cls(starting_nozzle_config, pick_up_current_map) + return cls(starting_nozzle_config) @property def starting_nozzle_offset(self) -> Point: @@ -260,29 +293,24 @@ def update_nozzle_configuration( front_right_nozzle: str, starting_nozzle: Optional[str] = None, ) -> None: - if ( - back_left_nozzle == self._physical_nozzle_map.back_left - and front_right_nozzle == self._physical_nozzle_map.front_right - ): - self._current_nozzle_configuration = self._physical_nozzle_map - else: - NozzleMap.validate_nozzle_configuration( - back_left_nozzle, - front_right_nozzle, - self._physical_nozzle_map, - self._current_nozzle_configuration, - ) - - self._current_nozzle_configuration = NozzleMap.build( - self._physical_nozzle_map.map_store, - starting_nozzle=starting_nozzle or back_left_nozzle, - back_left_nozzle=back_left_nozzle, - front_right_nozzle=front_right_nozzle, - origin_nozzle=self._physical_nozzle_map.starting_nozzle, - ) + self._current_nozzle_configuration = NozzleMap.build( + # these casts are because of pydantic in the protocol engine (see above) + physical_nozzles=cast( + "OrderedDict[str, Point]", self._physical_nozzle_map.map_store + ), + physical_rows=cast( + "OrderedDict[str, List[str]]", self._physical_nozzle_map.rows + ), + physical_columns=cast( + "OrderedDict[str, List[str]]", self._physical_nozzle_map.columns + ), + starting_nozzle=starting_nozzle or back_left_nozzle, + back_left_nozzle=back_left_nozzle, + front_right_nozzle=front_right_nozzle, + ) - def get_tip_configuration_current(self) -> float: - return self._pick_up_current_map[self._current_nozzle_configuration.tip_count] + def get_tip_count(self) -> int: + return self._current_nozzle_configuration.tip_count def critical_point_with_tip_length( self, diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 1842d26152d..8a596f24e5e 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2049,7 +2049,9 @@ def add_tip_to_instr() -> None: and instrument.nozzle_manager.current_configuration.configuration == NozzleConfigurationType.FULL ): - spec = self._pipette_handler.plan_ht_pick_up_tip() + spec = self._pipette_handler.plan_ht_pick_up_tip( + instrument.nozzle_manager.current_configuration.tip_count + ) if spec.z_distance_to_tiprack: await self.move_rel( realmount, top_types.Point(z=spec.z_distance_to_tiprack) @@ -2057,7 +2059,10 @@ def add_tip_to_instr() -> None: await self._tip_motor_action(realmount, spec.tip_action_moves) else: spec = self._pipette_handler.plan_lt_pick_up_tip( - realmount, presses, increment + realmount, + instrument.nozzle_manager.current_configuration.tip_count, + presses, + increment, ) await self._force_pick_up_tip(realmount, spec) diff --git a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py index 818566e3691..a45d416a526 100644 --- a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py @@ -59,10 +59,7 @@ def configure_virtual_pipette_nozzle_layout( config = self._get_virtual_pipette_full_config_by_model_string( pipette_model_string ) - new_nozzle_manager = NozzleConfigurationManager.build_from_nozzlemap( - config.nozzle_map, - config.partial_tip_configurations.per_tip_pickup_current, - ) + new_nozzle_manager = NozzleConfigurationManager.build_from_config(config) if back_left_nozzle and front_right_nozzle and starting_nozzle: new_nozzle_manager.update_nozzle_configuration( back_left_nozzle, front_right_nozzle, starting_nozzle diff --git a/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py b/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py index 1761e59a6ec..ca963355cb2 100644 --- a/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py +++ b/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py @@ -1,199 +1,837 @@ import pytest -from typing import Dict, List, ContextManager, Tuple +from typing import Dict, List, Tuple, Union, Iterator -from contextlib import nullcontext as does_not_raise from opentrons.hardware_control import nozzle_manager from opentrons.types import Point -from opentrons.hardware_control.types import CriticalPoint - - -def build_nozzle_manger( - nozzle_map: Dict[str, List[float]] -) -> nozzle_manager.NozzleConfigurationManager: - return nozzle_manager.NozzleConfigurationManager.build_from_nozzlemap( - nozzle_map, pick_up_current_map={1: 0.1} - ) - - -NINETY_SIX_CHANNEL_MAP = { - "A1": [-36.0, -25.5, -259.15], - "A2": [-27.0, -25.5, -259.15], - "A3": [-18.0, -25.5, -259.15], - "A4": [-9.0, -25.5, -259.15], - "A5": [0.0, -25.5, -259.15], - "A6": [9.0, -25.5, -259.15], - "A7": [18.0, -25.5, -259.15], - "A8": [27.0, -25.5, -259.15], - "A9": [36.0, -25.5, -259.15], - "A10": [45.0, -25.5, -259.15], - "A11": [54.0, -25.5, -259.15], - "A12": [63.0, -25.5, -259.15], - "B1": [-36.0, -34.5, -259.15], - "B2": [-27.0, -34.5, -259.15], - "B3": [-18.0, -34.5, -259.15], - "B4": [-9.0, -34.5, -259.15], - "B5": [0.0, -34.5, -259.15], - "B6": [9.0, -34.5, -259.15], - "B7": [18.0, -34.5, -259.15], - "B8": [27.0, -34.5, -259.15], - "B9": [36.0, -34.5, -259.15], - "B10": [45.0, -34.5, -259.15], - "B11": [54.0, -34.5, -259.15], - "B12": [63.0, -34.5, -259.15], - "C1": [-36.0, -43.5, -259.15], - "C2": [-27.0, -43.5, -259.15], - "C3": [-18.0, -43.5, -259.15], - "C4": [-9.0, -43.5, -259.15], - "C5": [0.0, -43.5, -259.15], - "C6": [9.0, -43.5, -259.15], - "C7": [18.0, -43.5, -259.15], - "C8": [27.0, -43.5, -259.15], - "C9": [36.0, -43.5, -259.15], - "C10": [45.0, -43.5, -259.15], - "C11": [54.0, -43.5, -259.15], - "C12": [63.0, -43.5, -259.15], - "D1": [-36.0, -52.5, -259.15], - "D2": [-27.0, -52.5, -259.15], - "D3": [-18.0, -52.5, -259.15], - "D4": [-9.0, -52.5, -259.15], - "D5": [0.0, -52.5, -259.15], - "D6": [9.0, -52.5, -259.15], - "D7": [18.0, -52.5, -259.15], - "D8": [27.0, -52.5, -259.15], - "D9": [36.0, -52.5, -259.15], - "D10": [45.0, -52.5, -259.15], - "D11": [54.0, -52.5, -259.15], - "D12": [63.0, -52.5, -259.15], - "E1": [-36.0, -61.5, -259.15], - "E2": [-27.0, -61.5, -259.15], - "E3": [-18.0, -61.5, -259.15], - "E4": [-9.0, -61.5, -259.15], - "E5": [0.0, -61.5, -259.15], - "E6": [9.0, -61.5, -259.15], - "E7": [18.0, -61.5, -259.15], - "E8": [27.0, -61.5, -259.15], - "E9": [36.0, -61.5, -259.15], - "E10": [45.0, -61.5, -259.15], - "E11": [54.0, -61.5, -259.15], - "E12": [63.0, -61.5, -259.15], - "F1": [-36.0, -70.5, -259.15], - "F2": [-27.0, -70.5, -259.15], - "F3": [-18.0, -70.5, -259.15], - "F4": [-9.0, -70.5, -259.15], - "F5": [0.0, -70.5, -259.15], - "F6": [9.0, -70.5, -259.15], - "F7": [18.0, -70.5, -259.15], - "F8": [27.0, -70.5, -259.15], - "F9": [36.0, -70.5, -259.15], - "F10": [45.0, -70.5, -259.15], - "F11": [54.0, -70.5, -259.15], - "F12": [63.0, -70.5, -259.15], - "G1": [-36.0, -79.5, -259.15], - "G2": [-27.0, -79.5, -259.15], - "G3": [-18.0, -79.5, -259.15], - "G4": [-9.0, -79.5, -259.15], - "G5": [0.0, -79.5, -259.15], - "G6": [9.0, -79.5, -259.15], - "G7": [18.0, -79.5, -259.15], - "G8": [27.0, -79.5, -259.15], - "G9": [36.0, -79.5, -259.15], - "G10": [45.0, -79.5, -259.15], - "G11": [54.0, -79.5, -259.15], - "G12": [63.0, -79.5, -259.15], - "H1": [-36.0, -88.5, -259.15], - "H2": [-27.0, -88.5, -259.15], - "H3": [-18.0, -88.5, -259.15], - "H4": [-9.0, -88.5, -259.15], - "H5": [0.0, -88.5, -259.15], - "H6": [9.0, -88.5, -259.15], - "H7": [18.0, -88.5, -259.15], - "H8": [27.0, -88.5, -259.15], - "H9": [36.0, -88.5, -259.15], - "H10": [45.0, -88.5, -259.15], - "H11": [54.0, -88.5, -259.15], - "H12": [63.0, -88.5, -259.15], -} + +from opentrons_shared_data.pipette.load_data import load_definition +from opentrons_shared_data.pipette.types import ( + PipetteModelType, + PipetteChannelType, + PipetteVersionType, +) +from opentrons_shared_data.pipette.pipette_definition import PipetteConfigurations + + +@pytest.mark.parametrize( + "pipette_details", + [ + (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)), + (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)), + (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)), + (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)), + (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)), + ], +) +def test_single_pipettes_always_full( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.SINGLE_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + subject.update_nozzle_configuration("A1", "A1", "A1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + subject.reset_to_default_configuration() + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + +@pytest.mark.parametrize( + "pipette_details", + [ + (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)), + (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)), + (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)), + (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)), + (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)), + ], +) +def test_single_pipette_map_entries( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.SINGLE_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + def test_map_entries(nozzlemap: nozzle_manager.NozzleMap) -> None: + assert nozzlemap.back_left == "A1" + assert nozzlemap.front_right == "A1" + assert list(nozzlemap.map_store.keys()) == ["A1"] + assert list(nozzlemap.rows.keys()) == ["A"] + assert list(nozzlemap.columns.keys()) == ["1"] + assert nozzlemap.rows["A"] == ["A1"] + assert nozzlemap.columns["1"] == ["A1"] + assert nozzlemap.tip_count == 1 + + test_map_entries(subject.current_configuration) + subject.update_nozzle_configuration("A1", "A1", "A1") + test_map_entries(subject.current_configuration) + subject.reset_to_default_configuration() + test_map_entries(subject.current_configuration) @pytest.mark.parametrize( - argnames=["nozzle_map", "critical_point_configuration", "expected"], - argvalues=[ - [ - { - "A1": [-8.0, -16.0, -259.15], - "B1": [-8.0, -25.0, -259.15], - "C1": [-8.0, -34.0, -259.15], - "D1": [-8.0, -43.0, -259.15], - "E1": [-8.0, -52.0, -259.15], - "F1": [-8.0, -61.0, -259.15], - "G1": [-8.0, -70.0, -259.15], - "H1": [-8.0, -79.0, -259.15], - }, - CriticalPoint.XY_CENTER, - Point(-8.0, -47.5, -259.15), - ], - [ - NINETY_SIX_CHANNEL_MAP, - CriticalPoint.XY_CENTER, - Point(13.5, -57.0, -259.15), - ], - [ - {"A1": [1, 1, 1]}, - CriticalPoint.FRONT_NOZZLE, - Point(1, 1, 1), - ], + "pipette_details", + [ + (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)), + (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)), + (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)), + (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)), + (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)), ], ) -def test_update_nozzles_with_critical_points( - nozzle_map: Dict[str, List[float]], - critical_point_configuration: CriticalPoint, - expected: List[float], +def test_single_pipette_map_geometry( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] ) -> None: - subject = build_nozzle_manger(nozzle_map) - new_cp = subject.critical_point_with_tip_length(critical_point_configuration) - assert new_cp == expected + config = load_definition( + pipette_details[0], PipetteChannelType.SINGLE_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + def test_map_geometry(nozzlemap: nozzle_manager.NozzleMap) -> None: + assert nozzlemap.xy_center_offset == Point(*config.nozzle_map["A1"]) + assert nozzlemap.front_nozzle_offset == Point(*config.nozzle_map["A1"]) + assert nozzlemap.starting_nozzle_offset == Point(*config.nozzle_map["A1"]) + + test_map_geometry(subject.current_configuration) + subject.update_nozzle_configuration("A1", "A1", "A1") + test_map_geometry(subject.current_configuration) + subject.reset_to_default_configuration() + test_map_geometry(subject.current_configuration) @pytest.mark.parametrize( - argnames=["nozzle_map", "updated_nozzle_configuration", "exception", "expected_cp"], - argvalues=[ - [ - { - "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 22.5, 0.8], - "C1": [0.0, 13.5, 0.8], - "D1": [0.0, 4.5, 0.8], - "E1": [0.0, -4.5, 0.8], - "F1": [0.0, -13.5, 0.8], - "G1": [0.0, -22.5, 0.8], - "H1": [0.0, -31.5, 0.8], - }, - ("D1", "H1"), - does_not_raise(), - Point(0.0, 4.5, 0.8), - ], - [ - {"A1": [1, 1, 1]}, - ("A1", "D1"), - pytest.raises(nozzle_manager.IncompatibleNozzleConfiguration), - Point(1, 1, 1), - ], - [ - NINETY_SIX_CHANNEL_MAP, - ("A12", "H12"), - does_not_raise(), - Point(x=63.0, y=-25.5, z=-259.15), - ], + "pipette_details", + [ + (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)), + (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)), + (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)), + (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)), + (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)), ], ) -def test_update_nozzle_configuration( - nozzle_map: Dict[str, List[float]], - updated_nozzle_configuration: Tuple[str, str], - exception: ContextManager[None], - expected_cp: List[float], +def test_multi_config_identification( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.EIGHT_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + subject.update_nozzle_configuration("A1", "H1", "A1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + subject.reset_to_default_configuration() + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + subject.update_nozzle_configuration("A1", "D1", "A1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.COLUMN + ) + + subject.update_nozzle_configuration("A1", "A1", "A1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SINGLE + ) + + subject.update_nozzle_configuration("H1", "H1", "H1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SINGLE + ) + + subject.update_nozzle_configuration("C1", "F1", "C1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.COLUMN + ) + + subject.reset_to_default_configuration() + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + +@pytest.mark.parametrize( + "pipette_details", + [ + (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)), + (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)), + (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)), + (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)), + (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)), + ], +) +def test_multi_config_map_entries( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.EIGHT_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + def test_map_entries( + nozzlemap: nozzle_manager.NozzleMap, nozzles: List[str] + ) -> None: + assert nozzlemap.back_left == nozzles[0] + assert nozzlemap.front_right == nozzles[-1] + assert list(nozzlemap.map_store.keys()) == nozzles + assert list(nozzlemap.rows.keys()) == [nozzle[0] for nozzle in nozzles] + assert list(nozzlemap.columns.keys()) == ["1"] + for rowname, row_elements in nozzlemap.rows.items(): + assert row_elements == [f"{rowname}1"] + + assert nozzlemap.columns["1"] == nozzles + assert nozzlemap.tip_count == len(nozzles) + + test_map_entries( + subject.current_configuration, ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + ) + subject.update_nozzle_configuration("A1", "H1", "A1") + test_map_entries( + subject.current_configuration, ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + ) + subject.update_nozzle_configuration("A1", "D1", "A1") + test_map_entries(subject.current_configuration, ["A1", "B1", "C1", "D1"]) + subject.update_nozzle_configuration("A1", "A1", "A1") + test_map_entries(subject.current_configuration, ["A1"]) + subject.update_nozzle_configuration("H1", "H1", "H1") + test_map_entries(subject.current_configuration, ["H1"]) + subject.update_nozzle_configuration("C1", "F1", "C1") + test_map_entries(subject.current_configuration, ["C1", "D1", "E1", "F1"]) + + +@pytest.mark.parametrize( + "pipette_details", + [ + (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)), + (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)), + (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)), + (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)), + (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)), + ], +) +def test_multi_config_geometry( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.EIGHT_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + def test_map_geometry( + nozzlemap: nozzle_manager.NozzleMap, + front_nozzle: str, + starting_nozzle: str, + xy_center_in_center_of: Union[Tuple[str, str], str], + ) -> None: + if isinstance(xy_center_in_center_of, str): + assert nozzlemap.xy_center_offset == Point( + *config.nozzle_map[xy_center_in_center_of] + ) + else: + assert nozzlemap.xy_center_offset == ( + ( + Point(*config.nozzle_map[xy_center_in_center_of[0]]) + + Point(*config.nozzle_map[xy_center_in_center_of[1]]) + ) + * 0.5 + ) + assert nozzlemap.front_nozzle_offset == Point(*config.nozzle_map[front_nozzle]) + assert nozzlemap.starting_nozzle_offset == Point( + *config.nozzle_map[starting_nozzle] + ) + + test_map_geometry(subject.current_configuration, "H1", "A1", ("A1", "H1")) + + subject.update_nozzle_configuration("A1", "A1", "A1") + test_map_geometry(subject.current_configuration, "A1", "A1", "A1") + + subject.update_nozzle_configuration("D1", "D1", "D1") + test_map_geometry(subject.current_configuration, "D1", "D1", "D1") + + subject.update_nozzle_configuration("C1", "G1", "C1") + test_map_geometry(subject.current_configuration, "G1", "C1", "E1") + + subject.update_nozzle_configuration("E1", "H1", "E1") + test_map_geometry(subject.current_configuration, "H1", "E1", ("E1", "H1")) + + subject.reset_to_default_configuration() + test_map_geometry(subject.current_configuration, "H1", "A1", ("A1", "H1")) + + +@pytest.mark.parametrize( + "pipette_details", [(PipetteModelType.p1000, PipetteVersionType(major=3, minor=5))] +) +def test_96_config_identification( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.NINETY_SIX_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + subject.update_nozzle_configuration("A1", "H12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + subject.update_nozzle_configuration("A1", "H1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.COLUMN + ) + subject.update_nozzle_configuration("A12", "H12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.COLUMN + ) + subject.update_nozzle_configuration("A8", "H8") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.COLUMN + ) + + subject.update_nozzle_configuration("A1", "A12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.ROW + ) + subject.update_nozzle_configuration("H1", "H12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.ROW + ) + subject.update_nozzle_configuration("D1", "D12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.ROW + ) + + subject.update_nozzle_configuration("E1", "H6") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + subject.update_nozzle_configuration("E7", "H12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + + subject.update_nozzle_configuration("C4", "F9") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + subject.update_nozzle_configuration("A1", "D12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + subject.update_nozzle_configuration("E1", "H12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + subject.update_nozzle_configuration("A1", "H6") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + subject.update_nozzle_configuration("A7", "H12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + + +@pytest.mark.parametrize( + "pipette_details", [(PipetteModelType.p1000, PipetteVersionType(major=3, minor=5))] +) +def test_96_config_map_entries( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] ) -> None: - subject = build_nozzle_manger(nozzle_map) - with exception: - subject.update_nozzle_configuration(*updated_nozzle_configuration) - assert subject.starting_nozzle_offset == expected_cp + config = load_definition( + pipette_details[0], PipetteChannelType.NINETY_SIX_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + def test_map_entries( + nozzlemap: nozzle_manager.NozzleMap, + rows: Dict[str, List[str]], + cols: Dict[str, List[str]], + ) -> None: + assert nozzlemap.back_left == next(iter(rows.values()))[0] + assert nozzlemap.front_right == next(reversed(list(rows.values())))[-1] + + def _nozzles() -> Iterator[str]: + for row in rows.values(): + for nozzle in row: + yield nozzle + + assert list(nozzlemap.map_store.keys()) == list(_nozzles()) + assert nozzlemap.rows == rows + assert nozzlemap.columns == cols + assert nozzlemap.tip_count == sum(len(row) for row in rows.values()) + + test_map_entries( + subject.current_configuration, + { + "A": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12", + ], + "B": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12", + ], + "C": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12", + ], + "D": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12", + ], + "E": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12", + ], + "F": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", + ], + "G": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12", + ], + "H": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12", + ], + }, + { + "1": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + "2": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + "3": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + "4": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + "5": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + "6": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + "7": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + "8": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + "9": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + "10": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + "11": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + "12": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"], + }, + ) + + subject.update_nozzle_configuration("A1", "H1") + test_map_entries( + subject.current_configuration, + { + "A": ["A1"], + "B": ["B1"], + "C": ["C1"], + "D": ["D1"], + "E": ["E1"], + "F": ["F1"], + "G": ["G1"], + "H": ["H1"], + }, + {"1": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"]}, + ) + + subject.update_nozzle_configuration("A12", "H12") + test_map_entries( + subject.current_configuration, + { + "A": ["A12"], + "B": ["B12"], + "C": ["C12"], + "D": ["D12"], + "E": ["E12"], + "F": ["F12"], + "G": ["G12"], + "H": ["H12"], + }, + {"12": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"]}, + ) + + subject.update_nozzle_configuration("A8", "H8") + test_map_entries( + subject.current_configuration, + { + "A": ["A8"], + "B": ["B8"], + "C": ["C8"], + "D": ["D8"], + "E": ["E8"], + "F": ["F8"], + "G": ["G8"], + "H": ["H8"], + }, + {"8": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"]}, + ) + + subject.update_nozzle_configuration("A1", "A12") + test_map_entries( + subject.current_configuration, + { + "A": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12", + ] + }, + { + "1": ["A1"], + "2": ["A2"], + "3": ["A3"], + "4": ["A4"], + "5": ["A5"], + "6": ["A6"], + "7": ["A7"], + "8": ["A8"], + "9": ["A9"], + "10": ["A10"], + "11": ["A11"], + "12": ["A12"], + }, + ) + + subject.update_nozzle_configuration("H1", "H12") + test_map_entries( + subject.current_configuration, + { + "H": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12", + ] + }, + { + "1": ["H1"], + "2": ["H2"], + "3": ["H3"], + "4": ["H4"], + "5": ["H5"], + "6": ["H6"], + "7": ["H7"], + "8": ["H8"], + "9": ["H9"], + "10": ["H10"], + "11": ["H11"], + "12": ["H12"], + }, + ) + subject.update_nozzle_configuration("D1", "D12") + test_map_entries( + subject.current_configuration, + { + "D": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12", + ] + }, + { + "1": ["D1"], + "2": ["D2"], + "3": ["D3"], + "4": ["D4"], + "5": ["D5"], + "6": ["D6"], + "7": ["D7"], + "8": ["D8"], + "9": ["D9"], + "10": ["D10"], + "11": ["D11"], + "12": ["D12"], + }, + ) + + subject.update_nozzle_configuration("A1", "D6") + test_map_entries( + subject.current_configuration, + { + "A": ["A1", "A2", "A3", "A4", "A5", "A6"], + "B": ["B1", "B2", "B3", "B4", "B5", "B6"], + "C": ["C1", "C2", "C3", "C4", "C5", "C6"], + "D": ["D1", "D2", "D3", "D4", "D5", "D6"], + }, + { + "1": ["A1", "B1", "C1", "D1"], + "2": ["A2", "B2", "C2", "D2"], + "3": ["A3", "B3", "C3", "D3"], + "4": ["A4", "B4", "C4", "D4"], + "5": ["A5", "B5", "C5", "D5"], + "6": ["A6", "B6", "C6", "D6"], + }, + ) + + subject.update_nozzle_configuration("A7", "D12") + test_map_entries( + subject.current_configuration, + { + "A": ["A7", "A8", "A9", "A10", "A11", "A12"], + "B": ["B7", "B8", "B9", "B10", "B11", "B12"], + "C": ["C7", "C8", "C9", "C10", "C11", "C12"], + "D": ["D7", "D8", "D9", "D10", "D11", "D12"], + }, + { + "7": ["A7", "B7", "C7", "D7"], + "8": ["A8", "B8", "C8", "D8"], + "9": ["A9", "B9", "C9", "D9"], + "10": ["A10", "B10", "C10", "D10"], + "11": ["A11", "B11", "C11", "D11"], + "12": ["A12", "B12", "C12", "D12"], + }, + ) + + subject.update_nozzle_configuration("E1", "H6") + test_map_entries( + subject.current_configuration, + { + "E": ["E1", "E2", "E3", "E4", "E5", "E6"], + "F": ["F1", "F2", "F3", "F4", "F5", "F6"], + "G": ["G1", "G2", "G3", "G4", "G5", "G6"], + "H": ["H1", "H2", "H3", "H4", "H5", "H6"], + }, + { + "1": ["E1", "F1", "G1", "H1"], + "2": ["E2", "F2", "G2", "H2"], + "3": ["E3", "F3", "G3", "H3"], + "4": ["E4", "F4", "G4", "H4"], + "5": ["E5", "F5", "G5", "H5"], + "6": ["E6", "F6", "G6", "H6"], + }, + ) + + subject.update_nozzle_configuration("E7", "H12") + test_map_entries( + subject.current_configuration, + { + "E": ["E7", "E8", "E9", "E10", "E11", "E12"], + "F": ["F7", "F8", "F9", "F10", "F11", "F12"], + "G": ["G7", "G8", "G9", "G10", "G11", "G12"], + "H": ["H7", "H8", "H9", "H10", "H11", "H12"], + }, + { + "7": ["E7", "F7", "G7", "H7"], + "8": ["E8", "F8", "G8", "H8"], + "9": ["E9", "F9", "G9", "H9"], + "10": ["E10", "F10", "G10", "H10"], + "11": ["E11", "F11", "G11", "H11"], + "12": ["E12", "F12", "G12", "H12"], + }, + ) + + subject.update_nozzle_configuration("C4", "D5") + test_map_entries( + subject.current_configuration, + {"C": ["C4", "C5"], "D": ["D4", "D5"]}, + {"4": ["C4", "D4"], "5": ["C5", "D5"]}, + ) + + +@pytest.mark.parametrize( + "pipette_details", [(PipetteModelType.p1000, PipetteVersionType(major=3, minor=5))] +) +def test_96_config_geometry( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.NINETY_SIX_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + def test_map_geometry( + config: PipetteConfigurations, + nozzlemap: nozzle_manager.NozzleMap, + starting_nozzle: str, + front_nozzle: str, + center_between: Union[str, Tuple[str, str]], + ) -> None: + if isinstance(center_between, str): + assert nozzlemap.xy_center_offset == Point( + *config.nozzle_map[center_between] + ) + else: + assert ( + nozzlemap.xy_center_offset + == ( + Point(*config.nozzle_map[center_between[0]]) + + Point(*config.nozzle_map[center_between[1]]) + ) + * 0.5 + ) + + assert nozzlemap.front_nozzle_offset == Point(*config.nozzle_map[front_nozzle]) + assert nozzlemap.starting_nozzle_offset == Point( + *config.nozzle_map[starting_nozzle] + ) + + test_map_geometry(config, subject.current_configuration, "A1", "H1", ("A1", "H12")) + + subject.update_nozzle_configuration("A1", "H1") + test_map_geometry(config, subject.current_configuration, "A1", "H1", ("A1", "H1")) + + subject.update_nozzle_configuration("A12", "H12") + test_map_geometry( + config, subject.current_configuration, "A12", "H12", ("A12", "H12") + ) + + subject.update_nozzle_configuration("A1", "A12") + test_map_geometry(config, subject.current_configuration, "A1", "A1", ("A1", "A12")) + + subject.update_nozzle_configuration("H1", "H12") + test_map_geometry(config, subject.current_configuration, "H1", "H1", ("H1", "H12")) + + subject.update_nozzle_configuration("A1", "D6") + test_map_geometry(config, subject.current_configuration, "A1", "D1", ("A1", "D6")) + + subject.update_nozzle_configuration("E7", "H12") + test_map_geometry(config, subject.current_configuration, "E7", "H7", ("E7", "H12")) + + subject.update_nozzle_configuration("C4", "D5") + test_map_geometry(config, subject.current_configuration, "C4", "D4", ("C4", "D5")) diff --git a/api/tests/opentrons/hardware_control/test_pipette_handler.py b/api/tests/opentrons/hardware_control/test_pipette_handler.py index c962fc592c5..3bd855024f6 100644 --- a/api/tests/opentrons/hardware_control/test_pipette_handler.py +++ b/api/tests/opentrons/hardware_control/test_pipette_handler.py @@ -16,6 +16,11 @@ TipActionMoveSpec, ) +from opentrons_shared_data.pipette.pipette_definition import ( + PressFitPickUpTipConfiguration, + CamActionPickUpTipConfiguration, +) + @pytest.fixture def mock_pipette(decoy: Decoy) -> Pipette: @@ -106,15 +111,19 @@ def test_plan_check_pick_up_tip_with_presses_argument( decoy.when(mock_pipette.has_tip).then_return(False) decoy.when(mock_pipette.config.quirks).then_return([]) - decoy.when(mock_pipette.pick_up_configurations.distance).then_return(0) - decoy.when(mock_pipette.pick_up_configurations.increment).then_return(0) - decoy.when(mock_pipette.connect_tiprack_distance_mm).then_return(8) - decoy.when(mock_pipette.end_tip_action_retract_distance_mm).then_return(2) - - if presses_input is None: - decoy.when(mock_pipette.pick_up_configurations.presses).then_return( - expected_array_length - ) + decoy.when(mock_pipette.pick_up_configurations.press_fit.presses).then_return( + expected_array_length + ) + decoy.when(mock_pipette.pick_up_configurations.press_fit.distance).then_return(5) + decoy.when(mock_pipette.pick_up_configurations.press_fit.increment).then_return(0) + decoy.when(mock_pipette.pick_up_configurations.press_fit.speed).then_return(10) + decoy.when(mock_pipette.config.end_tip_action_retract_distance_mm).then_return(0) + decoy.when( + mock_pipette.pick_up_configurations.press_fit.current_by_tip_count + ).then_return({1: 1.0}) + decoy.when(mock_pipette.nozzle_manager.current_configuration.tip_count).then_return( + 1 + ) spec, _add_tip_to_instrs = subject.plan_check_pick_up_tip( mount, tip_length, presses, increment @@ -147,32 +156,37 @@ def test_plan_check_pick_up_tip_with_presses_argument_ot3( increment = 1 decoy.when(mock_pipette_ot3.has_tip).then_return(False) - decoy.when(mock_pipette_ot3.pick_up_configurations.presses).then_return(2) - decoy.when(mock_pipette_ot3.pick_up_configurations.increment).then_return(increment) - decoy.when(mock_pipette_ot3.pick_up_configurations.speed).then_return(5.5) - decoy.when(mock_pipette_ot3.pick_up_configurations.distance).then_return(10) decoy.when( - mock_pipette_ot3.nozzle_manager.get_tip_configuration_current() - ).then_return(1) + mock_pipette_ot3.get_pick_up_configuration_for_tip_count(channels) + ).then_return( + CamActionPickUpTipConfiguration( + distance=10, + speed=5.5, + prep_move_distance=19.0, + prep_move_speed=10, + currentByTipCount={96: 1.0}, + connectTiprackDistanceMM=8, + ) + if channels == 96 + else PressFitPickUpTipConfiguration( + presses=2, + increment=increment, + distance=10, + speed=5.5, + currentByTipCount={channels: 1.0}, + ) + ) decoy.when(mock_pipette_ot3.plunger_motor_current.run).then_return(1) decoy.when(mock_pipette_ot3.config.quirks).then_return([]) decoy.when(mock_pipette_ot3.channels).then_return(channels) - decoy.when(mock_pipette_ot3.pick_up_configurations.prep_move_distance).then_return( - 19.0 + decoy.when(mock_pipette_ot3.config.end_tip_action_retract_distance_mm).then_return( + 2 ) - decoy.when(mock_pipette_ot3.pick_up_configurations.prep_move_speed).then_return(10) - decoy.when(mock_pipette_ot3.connect_tiprack_distance_mm).then_return(8) - decoy.when(mock_pipette_ot3.end_tip_action_retract_distance_mm).then_return(2) - - if presses_input is None: - decoy.when(mock_pipette_ot3.config.pick_up_presses).then_return( - expected_array_length - ) if channels == 96: - spec = subject_ot3.plan_ht_pick_up_tip() + spec = subject_ot3.plan_ht_pick_up_tip(96) else: - spec = subject_ot3.plan_lt_pick_up_tip(mount, presses, increment) + spec = subject_ot3.plan_lt_pick_up_tip(mount, channels, presses, increment) assert len(spec.tip_action_moves) == expected_array_length assert spec.tip_action_moves == request.getfixturevalue( expected_pick_up_motor_actions diff --git a/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py b/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py index 44fc10530e5..5635a40897b 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py +++ b/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py @@ -2,6 +2,7 @@ import pytest from decoy import Decoy from typing import Union, Optional, Dict +from collections import OrderedDict from opentrons.protocol_engine.execution import ( EquipmentHandler, @@ -25,105 +26,265 @@ SingleNozzleLayoutConfiguration, ) +NINETY_SIX_ROWS = OrderedDict( + ( + ( + "A", + [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12", + ], + ), + ( + "B", + [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12", + ], + ), + ( + "C", + [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12", + ], + ), + ( + "D", + [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12", + ], + ), + ( + "E", + [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12", + ], + ), + ( + "F", + [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", + ], + ), + ( + "G", + [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12", + ], + ), + ( + "H", + [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12", + ], + ), + ) +) + + +NINETY_SIX_COLS = OrderedDict( + ( + ("1", ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"]), + ("2", ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"]), + ("3", ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"]), + ("4", ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"]), + ("5", ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"]), + ("6", ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"]), + ("7", ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"]), + ("8", ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"]), + ("9", ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"]), + ("10", ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"]), + ("11", ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"]), + ("12", ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"]), + ) +) -NINETY_SIX_MAP = { - "A1": Point(-36.0, -25.5, -259.15), - "A2": Point(-27.0, -25.5, -259.15), - "A3": Point(-18.0, -25.5, -259.15), - "A4": Point(-9.0, -25.5, -259.15), - "A5": Point(0.0, -25.5, -259.15), - "A6": Point(9.0, -25.5, -259.15), - "A7": Point(18.0, -25.5, -259.15), - "A8": Point(27.0, -25.5, -259.15), - "A9": Point(36.0, -25.5, -259.15), - "A10": Point(45.0, -25.5, -259.15), - "A11": Point(54.0, -25.5, -259.15), - "A12": Point(63.0, -25.5, -259.15), - "B1": Point(-36.0, -34.5, -259.15), - "B2": Point(-27.0, -34.5, -259.15), - "B3": Point(-18.0, -34.5, -259.15), - "B4": Point(-9.0, -34.5, -259.15), - "B5": Point(0.0, -34.5, -259.15), - "B6": Point(9.0, -34.5, -259.15), - "B7": Point(18.0, -34.5, -259.15), - "B8": Point(27.0, -34.5, -259.15), - "B9": Point(36.0, -34.5, -259.15), - "B10": Point(45.0, -34.5, -259.15), - "B11": Point(54.0, -34.5, -259.15), - "B12": Point(63.0, -34.5, -259.15), - "C1": Point(-36.0, -43.5, -259.15), - "C2": Point(-27.0, -43.5, -259.15), - "C3": Point(-18.0, -43.5, -259.15), - "C4": Point(-9.0, -43.5, -259.15), - "C5": Point(0.0, -43.5, -259.15), - "C6": Point(9.0, -43.5, -259.15), - "C7": Point(18.0, -43.5, -259.15), - "C8": Point(27.0, -43.5, -259.15), - "C9": Point(36.0, -43.5, -259.15), - "C10": Point(45.0, -43.5, -259.15), - "C11": Point(54.0, -43.5, -259.15), - "C12": Point(63.0, -43.5, -259.15), - "D1": Point(-36.0, -52.5, -259.15), - "D2": Point(-27.0, -52.5, -259.15), - "D3": Point(-18.0, -52.5, -259.15), - "D4": Point(-9.0, -52.5, -259.15), - "D5": Point(0.0, -52.5, -259.15), - "D6": Point(9.0, -52.5, -259.15), - "D7": Point(18.0, -52.5, -259.15), - "D8": Point(27.0, -52.5, -259.15), - "D9": Point(36.0, -52.5, -259.15), - "D10": Point(45.0, -52.5, -259.15), - "D11": Point(54.0, -52.5, -259.15), - "D12": Point(63.0, -52.5, -259.15), - "E1": Point(-36.0, -61.5, -259.15), - "E2": Point(-27.0, -61.5, -259.15), - "E3": Point(-18.0, -61.5, -259.15), - "E4": Point(-9.0, -61.5, -259.15), - "E5": Point(0.0, -61.5, -259.15), - "E6": Point(9.0, -61.5, -259.15), - "E7": Point(18.0, -61.5, -259.15), - "E8": Point(27.0, -61.5, -259.15), - "E9": Point(36.0, -61.5, -259.15), - "E10": Point(45.0, -61.5, -259.15), - "E11": Point(54.0, -61.5, -259.15), - "E12": Point(63.0, -61.5, -259.15), - "F1": Point(-36.0, -70.5, -259.15), - "F2": Point(-27.0, -70.5, -259.15), - "F3": Point(-18.0, -70.5, -259.15), - "F4": Point(-9.0, -70.5, -259.15), - "F5": Point(0.0, -70.5, -259.15), - "F6": Point(9.0, -70.5, -259.15), - "F7": Point(18.0, -70.5, -259.15), - "F8": Point(27.0, -70.5, -259.15), - "F9": Point(36.0, -70.5, -259.15), - "F10": Point(45.0, -70.5, -259.15), - "F11": Point(54.0, -70.5, -259.15), - "F12": Point(63.0, -70.5, -259.15), - "G1": Point(-36.0, -79.5, -259.15), - "G2": Point(-27.0, -79.5, -259.15), - "G3": Point(-18.0, -79.5, -259.15), - "G4": Point(-9.0, -79.5, -259.15), - "G5": Point(0.0, -79.5, -259.15), - "G6": Point(9.0, -79.5, -259.15), - "G7": Point(18.0, -79.5, -259.15), - "G8": Point(27.0, -79.5, -259.15), - "G9": Point(36.0, -79.5, -259.15), - "G10": Point(45.0, -79.5, -259.15), - "G11": Point(54.0, -79.5, -259.15), - "G12": Point(63.0, -79.5, -259.15), - "H1": Point(-36.0, -88.5, -259.15), - "H2": Point(-27.0, -88.5, -259.15), - "H3": Point(-18.0, -88.5, -259.15), - "H4": Point(-9.0, -88.5, -259.15), - "H5": Point(0.0, -88.5, -259.15), - "H6": Point(9.0, -88.5, -259.15), - "H7": Point(18.0, -88.5, -259.15), - "H8": Point(27.0, -88.5, -259.15), - "H9": Point(36.0, -88.5, -259.15), - "H10": Point(45.0, -88.5, -259.15), - "H11": Point(54.0, -88.5, -259.15), - "H12": Point(63.0, -88.5, -259.15), -} +NINETY_SIX_MAP = OrderedDict( + ( + ("A1", Point(-36.0, -25.5, -259.15)), + ("A2", Point(-27.0, -25.5, -259.15)), + ("A3", Point(-18.0, -25.5, -259.15)), + ("A4", Point(-9.0, -25.5, -259.15)), + ("A5", Point(0.0, -25.5, -259.15)), + ("A6", Point(9.0, -25.5, -259.15)), + ("A7", Point(18.0, -25.5, -259.15)), + ("A8", Point(27.0, -25.5, -259.15)), + ("A9", Point(36.0, -25.5, -259.15)), + ("A10", Point(45.0, -25.5, -259.15)), + ("A11", Point(54.0, -25.5, -259.15)), + ("A12", Point(63.0, -25.5, -259.15)), + ("B1", Point(-36.0, -34.5, -259.15)), + ("B2", Point(-27.0, -34.5, -259.15)), + ("B3", Point(-18.0, -34.5, -259.15)), + ("B4", Point(-9.0, -34.5, -259.15)), + ("B5", Point(0.0, -34.5, -259.15)), + ("B6", Point(9.0, -34.5, -259.15)), + ("B7", Point(18.0, -34.5, -259.15)), + ("B8", Point(27.0, -34.5, -259.15)), + ("B9", Point(36.0, -34.5, -259.15)), + ("B10", Point(45.0, -34.5, -259.15)), + ("B11", Point(54.0, -34.5, -259.15)), + ("B12", Point(63.0, -34.5, -259.15)), + ("C1", Point(-36.0, -43.5, -259.15)), + ("C2", Point(-27.0, -43.5, -259.15)), + ("C3", Point(-18.0, -43.5, -259.15)), + ("C4", Point(-9.0, -43.5, -259.15)), + ("C5", Point(0.0, -43.5, -259.15)), + ("C6", Point(9.0, -43.5, -259.15)), + ("C7", Point(18.0, -43.5, -259.15)), + ("C8", Point(27.0, -43.5, -259.15)), + ("C9", Point(36.0, -43.5, -259.15)), + ("C10", Point(45.0, -43.5, -259.15)), + ("C11", Point(54.0, -43.5, -259.15)), + ("C12", Point(63.0, -43.5, -259.15)), + ("D1", Point(-36.0, -52.5, -259.15)), + ("D2", Point(-27.0, -52.5, -259.15)), + ("D3", Point(-18.0, -52.5, -259.15)), + ("D4", Point(-9.0, -52.5, -259.15)), + ("D5", Point(0.0, -52.5, -259.15)), + ("D6", Point(9.0, -52.5, -259.15)), + ("D7", Point(18.0, -52.5, -259.15)), + ("D8", Point(27.0, -52.5, -259.15)), + ("D9", Point(36.0, -52.5, -259.15)), + ("D10", Point(45.0, -52.5, -259.15)), + ("D11", Point(54.0, -52.5, -259.15)), + ("D12", Point(63.0, -52.5, -259.15)), + ("E1", Point(-36.0, -61.5, -259.15)), + ("E2", Point(-27.0, -61.5, -259.15)), + ("E3", Point(-18.0, -61.5, -259.15)), + ("E4", Point(-9.0, -61.5, -259.15)), + ("E5", Point(0.0, -61.5, -259.15)), + ("E6", Point(9.0, -61.5, -259.15)), + ("E7", Point(18.0, -61.5, -259.15)), + ("E8", Point(27.0, -61.5, -259.15)), + ("E9", Point(36.0, -61.5, -259.15)), + ("E10", Point(45.0, -61.5, -259.15)), + ("E11", Point(54.0, -61.5, -259.15)), + ("E12", Point(63.0, -61.5, -259.15)), + ("F1", Point(-36.0, -70.5, -259.15)), + ("F2", Point(-27.0, -70.5, -259.15)), + ("F3", Point(-18.0, -70.5, -259.15)), + ("F4", Point(-9.0, -70.5, -259.15)), + ("F5", Point(0.0, -70.5, -259.15)), + ("F6", Point(9.0, -70.5, -259.15)), + ("F7", Point(18.0, -70.5, -259.15)), + ("F8", Point(27.0, -70.5, -259.15)), + ("F9", Point(36.0, -70.5, -259.15)), + ("F10", Point(45.0, -70.5, -259.15)), + ("F11", Point(54.0, -70.5, -259.15)), + ("F12", Point(63.0, -70.5, -259.15)), + ("G1", Point(-36.0, -79.5, -259.15)), + ("G2", Point(-27.0, -79.5, -259.15)), + ("G3", Point(-18.0, -79.5, -259.15)), + ("G4", Point(-9.0, -79.5, -259.15)), + ("G5", Point(0.0, -79.5, -259.15)), + ("G6", Point(9.0, -79.5, -259.15)), + ("G7", Point(18.0, -79.5, -259.15)), + ("G8", Point(27.0, -79.5, -259.15)), + ("G9", Point(36.0, -79.5, -259.15)), + ("G10", Point(45.0, -79.5, -259.15)), + ("G11", Point(54.0, -79.5, -259.15)), + ("G12", Point(63.0, -79.5, -259.15)), + ("H1", Point(-36.0, -88.5, -259.15)), + ("H2", Point(-27.0, -88.5, -259.15)), + ("H3", Point(-18.0, -88.5, -259.15)), + ("H4", Point(-9.0, -88.5, -259.15)), + ("H5", Point(0.0, -88.5, -259.15)), + ("H6", Point(9.0, -88.5, -259.15)), + ("H7", Point(18.0, -88.5, -259.15)), + ("H8", Point(27.0, -88.5, -259.15)), + ("H9", Point(36.0, -88.5, -259.15)), + ("H10", Point(45.0, -88.5, -259.15)), + ("H11", Point(54.0, -88.5, -259.15)), + ("H12", Point(63.0, -88.5, -259.15)), + ) +) @pytest.mark.parametrize( @@ -132,7 +293,9 @@ [ SingleNozzleLayoutConfiguration(primary_nozzle="A1"), NozzleMap.build( - physical_nozzle_map={"A1": Point(0, 0, 0)}, + physical_nozzles=OrderedDict({"A1": Point(0, 0, 0)}), + physical_rows=OrderedDict({"A": ["A1"]}), + physical_columns=OrderedDict({"1": ["A1"]}), starting_nozzle="A1", back_left_nozzle="A1", front_right_nozzle="A1", @@ -142,7 +305,9 @@ [ ColumnNozzleLayoutConfiguration(primary_nozzle="A1"), NozzleMap.build( - physical_nozzle_map=NINETY_SIX_MAP, + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, starting_nozzle="A1", back_left_nozzle="A1", front_right_nozzle="H1", @@ -154,7 +319,9 @@ primary_nozzle="A1", front_right_nozzle="E1" ), NozzleMap.build( - physical_nozzle_map=NINETY_SIX_MAP, + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, starting_nozzle="A1", back_left_nozzle="A1", front_right_nozzle="E1", diff --git a/hardware-testing/hardware_testing/gravimetric/helpers.py b/hardware-testing/hardware_testing/gravimetric/helpers.py index 88d9586a50e..ddaced5ad0c 100644 --- a/hardware-testing/hardware_testing/gravimetric/helpers.py +++ b/hardware-testing/hardware_testing/gravimetric/helpers.py @@ -376,8 +376,7 @@ def _load_pipette( if pipette.channels == 8 and not increment and not photometric: hwapi = get_sync_hw_api(ctx) mnt = OT3Mount.LEFT if pipette_mount == "left" else OT3Mount.RIGHT - hwpipette: Pipette = hwapi.hardware_pipettes[mnt.to_mount()] - hwpipette.pick_up_configurations.current = 0.2 + hwapi.update_nozzle_configuration_for_mount(mnt, "H1", "H1", "H1") return pipette diff --git a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py index 9f7d0215af4..6652a174a73 100644 --- a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py +++ b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py @@ -546,9 +546,11 @@ async def update_pick_up_current( ) -> None: """Update pick-up-tip current.""" pipette = _get_pipette_from_mount(api, mount) - config_model = pipette.pick_up_configurations - config_model.current = current - pipette.pick_up_configurations = config_model + config_model = pipette.pick_up_configurations.press_fit + config_model.current_by_tip_count = { + k: current for k in config_model.current_by_tip_count.keys() + } + pipette.pick_up_configurations.press_fit = config_model async def update_pick_up_distance( @@ -556,9 +558,9 @@ async def update_pick_up_distance( ) -> None: """Update pick-up-tip current.""" pipette = _get_pipette_from_mount(api, mount) - config_model = pipette.pick_up_configurations + config_model = pipette.pick_up_configurations.press_fit config_model.distance = distance - pipette.pick_up_configurations = config_model + pipette.pick_up_configurations.press_fit = config_model async def move_plunger_absolute_ot3( diff --git a/robot-server/robot_server/robot/calibration/util.py b/robot-server/robot_server/robot/calibration/util.py index d7a1d2a7845..f07f6852e68 100644 --- a/robot-server/robot_server/robot/calibration/util.py +++ b/robot-server/robot_server/robot/calibration/util.py @@ -1,8 +1,6 @@ import logging -import contextlib from typing import Set, Dict, Any, Union, List, Optional, TYPE_CHECKING -from opentrons.hardware_control.instruments.ot2.pipette import Pipette from opentrons.hardware_control.util import plan_arc from opentrons.hardware_control.types import CriticalPoint from opentrons.protocol_api import labware @@ -108,33 +106,26 @@ def get_next_state(self, from_state, command): async def invalidate_tip(user_flow: CalibrationUserFlow): await user_flow.return_tip() user_flow.reset_tip_origin() + await user_flow.hardware.update_nozzle_configuration_for_mount( + user_flow.mount, None, None + ) await user_flow.move_to_tip_rack() -@contextlib.contextmanager -def save_default_pick_up_current(instr: Pipette): - # reduce pick up current for multichannel pipette picking up 1 tip - saved_default = instr.pick_up_configurations.current - instr.update_config_item({"pick_up_current": 0.1}) - - try: - yield - finally: - instr.update_config_item({"pick_up_current": saved_default}) - - async def pick_up_tip(user_flow: CalibrationUserFlow, tip_length: float): # grab position of active nozzle for ref when returning tip later cp = user_flow.critical_point_override user_flow.tip_origin = await user_flow.hardware.gantry_position( user_flow.mount, critical_point=cp ) - - with contextlib.ExitStack() as stack: - if user_flow.hw_pipette.config.channels > 1: - stack.enter_context(save_default_pick_up_current(user_flow.hw_pipette)) - - await user_flow.hardware.pick_up_tip(user_flow.mount, tip_length) + if user_flow.hw_pipette.config.channels > 1: + await user_flow.hardware.update_nozzle_configuration_for_mount( + user_flow.mount, + back_left_nozzle="H1", + front_right_nozzle="H1", + starting_nozzle="H1", + ) + await user_flow.hardware.pick_up_tip(user_flow.mount, tip_length) async def return_tip(user_flow: CalibrationUserFlow, tip_length: float): @@ -154,6 +145,9 @@ async def return_tip(user_flow: CalibrationUserFlow, tip_length: float): ) await user_flow.hardware.drop_tip(user_flow.mount) user_flow.reset_tip_origin() + await user_flow.hardware.update_nozzle_configuration_for_mount( + user_flow.mount, None, None + ) async def move( diff --git a/robot-server/tests/robot/calibration/deck/test_user_flow.py b/robot-server/tests/robot/calibration/deck/test_user_flow.py index f162a291e75..8b6bf90c43e 100644 --- a/robot-server/tests/robot/calibration/deck/test_user_flow.py +++ b/robot-server/tests/robot/calibration/deck/test_user_flow.py @@ -1,6 +1,6 @@ import pytest import datetime -from mock import MagicMock, call +from mock import call from typing import List, Tuple from pathlib import Path from opentrons.calibration_storage import types as cal_types @@ -151,14 +151,7 @@ async def test_save_default_pick_up_current(mock_hw): def mock_update_config_item(*args): pass - uf._hw_pipette.update_config_item = MagicMock(side_effect=mock_update_config_item) - default_current = pip.pick_up_configurations.current - update_config_calls = [ - call({"pick_up_current": 0.1}), - call({"pick_up_current": default_current}), - ] await uf.pick_up_tip() - uf._hw_pipette.update_config_item.assert_has_calls(update_config_calls) async def test_return_tip(mock_user_flow): diff --git a/shared-data/js/__tests__/pipetteSchemaV2.test.ts b/shared-data/js/__tests__/pipetteSchemaV2.test.ts index 66845b9cab6..1dce5ef754b 100644 --- a/shared-data/js/__tests__/pipetteSchemaV2.test.ts +++ b/shared-data/js/__tests__/pipetteSchemaV2.test.ts @@ -90,9 +90,8 @@ describe('test schema against all general specs definitions', () => { expect(generalPaths.length).toBeGreaterThan(0) generalPaths.forEach(generalPath => { - const generalDef = require(generalPath) - it(`${generalPath} validates against schema`, () => { + const generalDef = require(generalPath) const valid = validateGeneralSpecs(generalDef) const validationErrors = validateGeneralSpecs.errors expect(validationErrors).toBe(null) diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_0.json index a3a7a9a8fc4..61bb1a9b8e4 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_0.json @@ -4,17 +4,28 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.1, + "3": 0.15, + "4": 0.2, + "5": 0.25, + "6": 0.3, + "7": 0.35, + "8": 0.4 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.1, - "3": 0.15, - "4": 0.2, - "5": 0.25, - "6": 0.3, - "7": 0.35, - "8": 0.4 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 1.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_3.json index 3f6700dc816..c8e07ba071b 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_3.json @@ -4,17 +4,28 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.1, + "3": 0.15, + "4": 0.2, + "5": 0.25, + "6": 0.3, + "7": 0.35, + "8": 0.4 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.1, - "3": 0.15, - "4": 0.2, - "5": 0.25, - "6": 0.3, - "7": 0.35, - "8": 0.4 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 1.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_4.json index f183e7aab87..d673992b6d2 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_4.json @@ -4,17 +4,28 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.1, + "3": 0.15, + "4": 0.2, + "5": 0.25, + "6": 0.3, + "7": 0.35, + "8": 0.4 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.1, - "3": 0.15, - "4": 0.2, - "5": 0.25, - "6": 0.3, - "7": 0.35, - "8": 0.4 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 1.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_5.json index ef35a1587c8..dbcb7f8dbc7 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_5.json @@ -4,17 +4,28 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 3.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 3.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.14, + "3": 0.21, + "4": 0.28, + "5": 0.34, + "6": 0.41, + "7": 0.48, + "8": 0.55 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.14, - "3": 0.21, - "4": 0.28, - "5": 0.34, - "6": 0.41, - "7": 0.48, - "8": 0.55 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 1.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_6.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_6.json index ef35a1587c8..dbcb7f8dbc7 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_6.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_6.json @@ -4,17 +4,28 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 3.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 3.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.14, + "3": 0.21, + "4": 0.28, + "5": 0.34, + "6": 0.41, + "7": 0.48, + "8": 0.55 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.14, - "3": 0.21, - "4": 0.28, - "5": 0.34, - "6": 0.41, - "7": 0.48, - "8": 0.55 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 1.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/1_0.json index 1d17191c93f..e557a84a71e 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/1_0.json @@ -4,14 +4,28 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15, + "2": 0.13, + "3": 0.19, + "4": 0.25, + "5": 0.31, + "6": 0.38, + "7": 0.44, + "8": 0.5 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -39,17 +53,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.15, - "2": 0.13, - "3": 0.19, - "4": 0.25, - "5": 0.31, - "6": 0.38, - "7": 0.44, - "8": 0.5 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "backCompatNames": [], "channels": 8, @@ -60,6 +64,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json index 8bd59075ccc..16a0931ec7c 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json @@ -4,14 +4,28 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15, + "2": 0.13, + "3": 0.19, + "4": 0.25, + "5": 0.31, + "6": 0.38, + "7": 0.44, + "8": 0.5 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -39,17 +53,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.15, - "2": 0.13, - "3": 0.19, - "4": 0.25, - "5": 0.31, - "6": 0.38, - "7": 0.44, - "8": 0.5 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "backCompatNames": [], "channels": 8, @@ -60,6 +64,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json index 8bd59075ccc..16a0931ec7c 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json @@ -4,14 +4,28 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15, + "2": 0.13, + "3": 0.19, + "4": 0.25, + "5": 0.31, + "6": 0.38, + "7": 0.44, + "8": 0.5 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -39,17 +53,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.15, - "2": 0.13, - "3": 0.19, - "4": 0.25, - "5": 0.31, - "6": 0.38, - "7": 0.44, - "8": 0.5 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "backCompatNames": [], "channels": 8, @@ -60,6 +64,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json index 862266f22fe..47ace6a4e52 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json @@ -4,14 +4,28 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.14, + "3": 0.21, + "4": 0.28, + "5": 0.34, + "6": 0.41, + "7": 0.48, + "8": 0.55 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -39,17 +53,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.2, - "2": 0.14, - "3": 0.21, - "4": 0.28, - "5": 0.34, - "6": 0.41, - "7": 0.48, - "8": 0.55 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "backCompatNames": [], "channels": 8, @@ -60,6 +64,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json index 862266f22fe..47ace6a4e52 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json @@ -4,14 +4,28 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.14, + "3": 0.21, + "4": 0.28, + "5": 0.34, + "6": 0.41, + "7": 0.48, + "8": 0.55 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -39,17 +53,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.2, - "2": 0.14, - "3": 0.21, - "4": 0.28, - "5": 0.34, - "6": 0.41, - "7": 0.48, - "8": 0.55 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "backCompatNames": [], "channels": 8, @@ -60,6 +64,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p20/2_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p20/2_0.json index 9530ac8428c..b204977b556 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p20/2_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p20/2_0.json @@ -4,17 +4,28 @@ "model": "p20", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 11.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 11.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.28, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 15.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 15.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.15, - "3": 0.23, - "4": 0.28, - "5": 0.38, - "6": 0.45, - "7": 0.53, - "8": 0.6 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 1.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p20/2_1.json b/shared-data/pipette/definitions/2/general/eight_channel/p20/2_1.json index 9530ac8428c..b204977b556 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p20/2_1.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p20/2_1.json @@ -4,17 +4,28 @@ "model": "p20", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 11.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 11.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.28, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 15.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 15.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.15, - "3": 0.23, - "4": 0.28, - "5": 0.38, - "6": 0.45, - "7": 0.53, - "8": 0.6 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 1.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_0.json index 0d1ed8808cc..8a0560e150a 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_0.json @@ -4,17 +4,28 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.3, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.15, - "3": 0.23, - "4": 0.3, - "5": 0.38, - "6": 0.45, - "7": 0.53, - "8": 0.6 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 5.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_3.json index 602bf5df703..ce299da8595 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_3.json @@ -4,17 +4,28 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.3, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.15, - "3": 0.23, - "4": 0.3, - "5": 0.38, - "6": 0.45, - "7": 0.53, - "8": 0.6 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 5.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_4.json index 602bf5df703..ce299da8595 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_4.json @@ -4,17 +4,28 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.3, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.15, - "3": 0.23, - "4": 0.3, - "5": 0.38, - "6": 0.45, - "7": 0.53, - "8": 0.6 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 5.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_5.json index 972cc766d78..c420477d1cb 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_5.json @@ -4,17 +4,28 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 3.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 3.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.23, + "3": 0.34, + "4": 0.45, + "5": 0.56, + "6": 0.68, + "7": 0.79, + "8": 0.9 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.23, - "3": 0.34, - "4": 0.45, - "5": 0.56, - "6": 0.68, - "7": 0.79, - "8": 0.9 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 5.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/2_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/2_0.json index 412f0b9c90d..3096fd46333 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/2_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/2_0.json @@ -4,17 +4,28 @@ "model": "p300", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 11.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 11.0, + "currentByTipCount": { + "1": 0.13, + "2": 0.2, + "3": 0.3, + "4": 0.4, + "5": 0.5, + "6": 0.6, + "7": 0.7, + "8": 0.8 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.5, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.5 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.13, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 3.5, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/2_1.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/2_1.json index 412f0b9c90d..3096fd46333 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/2_1.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/2_1.json @@ -4,17 +4,28 @@ "model": "p300", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 11.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 11.0, + "currentByTipCount": { + "1": 0.13, + "2": 0.2, + "3": 0.3, + "4": 0.4, + "5": 0.5, + "6": 0.6, + "7": 0.7, + "8": 0.8 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.5, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.5 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.13, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 3.5, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_0.json index 489a8a97191..472de5d3a2e 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_0.json @@ -4,17 +4,28 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.3, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.15, - "3": 0.23, - "4": 0.3, - "5": 0.38, - "6": 0.45, - "7": 0.53, - "8": 0.6 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 2.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_3.json index 2931fbd0351..f6542bbe5ae 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_3.json @@ -4,17 +4,28 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.3, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.15, - "3": 0.23, - "4": 0.3, - "5": 0.38, - "6": 0.45, - "7": 0.53, - "8": 0.6 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 2.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_4.json index d05487517e8..dd7d9415c45 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_4.json @@ -4,17 +4,28 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.3, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.15, - "3": 0.23, - "4": 0.3, - "5": 0.38, - "6": 0.45, - "7": 0.53, - "8": 0.6 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 2.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_5.json index 6204217a21f..5e35bb44b29 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_5.json @@ -4,17 +4,28 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 3.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 3.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.2, + "3": 0.3, + "4": 0.4, + "5": 0.5, + "6": 0.6, + "7": 0.7, + "8": 0.8 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,17 +44,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.1, - "2": 0.2, - "3": 0.3, - "4": 0.4, - "5": 0.5, - "6": 0.6, - "7": 0.7, - "8": 0.8 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "channels": 8, "shaftDiameter": 2.0, @@ -54,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json index 4ea113546b3..9a22b968ebc 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json @@ -4,14 +4,28 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15, + "2": 0.13, + "3": 0.19, + "4": 0.25, + "5": 0.31, + "6": 0.38, + "7": 0.44, + "8": 0.5 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,17 +59,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.15, - "2": 0.13, - "3": 0.19, - "4": 0.25, - "5": 0.31, - "6": 0.38, - "7": 0.44, - "8": 0.5 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "backCompatNames": [], "channels": 8, @@ -66,6 +70,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json index 4ea113546b3..9a22b968ebc 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json @@ -4,14 +4,28 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15, + "2": 0.13, + "3": 0.19, + "4": 0.25, + "5": 0.31, + "6": 0.38, + "7": 0.44, + "8": 0.5 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,17 +59,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.15, - "2": 0.13, - "3": 0.19, - "4": 0.25, - "5": 0.31, - "6": 0.38, - "7": 0.44, - "8": 0.5 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "backCompatNames": [], "channels": 8, @@ -66,6 +70,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json index cffcc3d65e9..fef2c2cb87e 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json @@ -4,14 +4,28 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.14, + "3": 0.2, + "4": 0.28, + "5": 0.34, + "6": 0.41, + "7": 0.48, + "8": 0.55 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,17 +59,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.2, - "2": 0.14, - "3": 0.2, - "4": 0.28, - "5": 0.34, - "6": 0.41, - "7": 0.48, - "8": 0.55 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "backCompatNames": [], "channels": 8, @@ -66,6 +70,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json index cffcc3d65e9..fef2c2cb87e 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json @@ -4,14 +4,28 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.14, + "3": 0.2, + "4": 0.28, + "5": 0.34, + "6": 0.41, + "7": 0.48, + "8": 0.55 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,17 +59,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8], - "perTipPickupCurrent": { - "1": 0.2, - "2": 0.14, - "3": 0.2, - "4": 0.28, - "5": 0.34, - "6": 0.41, - "7": 0.48, - "8": 0.55 - } + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] }, "backCompatNames": [], "channels": 8, @@ -66,6 +70,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json index 2fa4e3e803a..e5ae64d98cc 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json @@ -4,19 +4,45 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 0.0, - "speed": 5.5, - "increment": 0.0, - "distance": 10.0, - "prep_move_distance": 9.0, - "prep_move_speed": 10.0 + "pressFit": { + "presses": 1, + "increment": 0.0, + "speed": 10.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.25, + "3": 0.3, + "4": 0.35, + "5": 0.4, + "6": 0.45, + "7": 0.5, + "8": 0.55, + "12": 0.19, + "16": 0.25, + "24": 0.38, + "48": 0.75 + } + }, + "camAction": { + "prep_move_distance": 9.0, + "prep_move_speed": 10.0, + "speed": 5.5, + "distance": 10.0, + "connectTiprackDistanceMM": 7.0, + "currentByTipCount": { + "96": 1.5 + } + } }, "dropTipConfigurations": { - "current": 1.5, - "speed": 5.5, - "distance": 26.5, - "prep_move_distance": 16.0, - "prep_move_speed": 10.5 + "camAction": { + "current": 1.5, + "speed": 5.5, + "distance": 26.5, + "prep_move_distance": 16.0, + "prep_move_speed": 10.5 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -44,22 +70,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 8, 12, 96], - "perTipPickupCurrent": { - "1": 0.02, - "2": 0.03, - "3": 0.05, - "4": 0.06, - "5": 0.08, - "6": 0.09, - "7": 0.11, - "8": 0.13, - "12": 0.19, - "16": 0.25, - "24": 0.38, - "48": 0.75, - "96": 1.5 - } + "availableConfigurations": [1, 8, 12, 96] }, "backCompatNames": [], "channels": 96, @@ -71,7 +82,6 @@ "current": 2.0, "speed": 5 }, - "tipPresenceCheckDistanceMM": "8.0", - "connectTiprackDistanceMM": "7.0", - "endTipActionRetractDistanceMM": "2.0" + "tipPresenceCheckDistanceMM": 8.0, + "endTipActionRetractDistanceMM": 2.0 } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json index bef06d53c03..3db9976b4e9 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json @@ -4,19 +4,50 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 0.0, - "speed": 5.5, - "increment": 0.0, - "distance": 10.0, - "prep_move_distance": 9.0, - "prep_move_speed": 10.0 + "pressFit": { + "presses": 1, + "increment": 0.0, + "speed": 10.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.25, + "3": 0.3, + "4": 0.35, + "5": 0.4, + "6": 0.45, + "7": 0.5, + "8": 0.55, + "12": 0.19, + "16": 0.25, + "24": 0.38, + "48": 0.75 + } + }, + "camAction": { + "speed": 5.5, + "distance": 10.0, + "prep_move_distance": 9.0, + "prep_move_speed": 10.0, + "connectTiprackDistanceMM": 7.0, + "currentByTipCount": { + "96": 1.5 + } + } }, "dropTipConfigurations": { - "current": 1.5, - "speed": 5.5, - "distance": 10.5, - "prep_move_distance": 16.0, - "prep_move_speed": 10.0 + "plungerEject": { + "current": 1.5, + "speed": 5.5, + "distance": 10.5 + }, + "camAction": { + "current": 1.5, + "speed": 5.5, + "distance": 10.5, + "prep_move_distance": 16.0, + "prep_move_speed": 10.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -44,22 +75,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 8, 12, 96], - "perTipPickupCurrent": { - "1": 0.02, - "2": 0.03, - "3": 0.05, - "4": 0.06, - "5": 0.08, - "6": 0.09, - "7": 0.11, - "8": 0.13, - "12": 0.19, - "16": 0.25, - "24": 0.38, - "48": 0.75, - "96": 1.5 - } + "availableConfigurations": [1, 8, 12, 16, 24, 48, 96] }, "backCompatNames": [], "channels": 96, @@ -71,7 +87,6 @@ "current": 2.0, "speed": 5 }, - "tipPresenceCheckDistanceMM": "8.0", - "connectTiprackDistanceMM": "7.0", - "endTipActionRetractDistanceMM": "2.0" + "tipPresenceCheckDistanceMM": 8.0, + "endTipActionRetractDistanceMM": 2.0 } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json index bef06d53c03..f5108478b2d 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json @@ -4,19 +4,45 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 0.0, - "speed": 5.5, - "increment": 0.0, - "distance": 10.0, - "prep_move_distance": 9.0, - "prep_move_speed": 10.0 + "pressFit": { + "presses": 1, + "increment": 0.0, + "speed": 10.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.25, + "3": 0.3, + "4": 0.35, + "5": 0.4, + "6": 0.45, + "7": 0.5, + "8": 0.55, + "12": 0.19, + "16": 0.25, + "24": 0.38, + "48": 0.75 + } + }, + "camAction": { + "speed": 5.5, + "distance": 10.0, + "prep_move_distance": 9.0, + "prep_move_speed": 10.0, + "connectTiprackDistanceMM": 7.0, + "currentByTipCount": { + "96": 1.5 + } + } }, "dropTipConfigurations": { - "current": 1.5, - "speed": 5.5, - "distance": 10.5, - "prep_move_distance": 16.0, - "prep_move_speed": 10.0 + "camAction": { + "current": 1.5, + "speed": 5.5, + "distance": 10.5, + "prep_move_distance": 16.0, + "prep_move_speed": 10.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -44,22 +70,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 8, 12, 96], - "perTipPickupCurrent": { - "1": 0.02, - "2": 0.03, - "3": 0.05, - "4": 0.06, - "5": 0.08, - "6": 0.09, - "7": 0.11, - "8": 0.13, - "12": 0.19, - "16": 0.25, - "24": 0.38, - "48": 0.75, - "96": 1.5 - } + "availableConfigurations": [1, 8, 12, 16, 24, 48, 96] }, "backCompatNames": [], "channels": 96, @@ -71,7 +82,6 @@ "current": 2.0, "speed": 5 }, - "tipPresenceCheckDistanceMM": "8.0", - "connectTiprackDistanceMM": "7.0", - "endTipActionRetractDistanceMM": "2.0" + "tipPresenceCheckDistanceMM": 8.0, + "endTipActionRetractDistanceMM": 2.0 } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json index 4d7eaff5487..c3bb411ab2a 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json @@ -4,19 +4,46 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 0.0, - "speed": 5.5, - "increment": 0.0, - "distance": 10.0, - "prep_move_distance": 9.0, - "prep_move_speed": 10.0 + "pressFit": { + "presses": 1, + "increment": 0.0, + "speed": 10.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.25, + "3": 0.3, + "4": 0.35, + "5": 0.4, + "6": 0.45, + "7": 0.5, + "8": 0.55, + "12": 0.19, + "16": 0.25, + "24": 0.38, + "48": 0.75 + } + }, + "camAction": { + "speed": 5.5, + "distance": 10.0, + "prep_move_distance": 9.0, + "prep_move_speed": 10.0, + "connectTiprackDistanceMM": 7.0, + + "currentByTipCount": { + "96": 1.5 + } + } }, "dropTipConfigurations": { - "current": 1.5, - "speed": 5.5, - "distance": 10.8, - "prep_move_distance": 19.0, - "prep_move_speed": 10.0 + "camAction": { + "current": 1.5, + "speed": 5.5, + "distance": 10.8, + "prep_move_distance": 19.0, + "prep_move_speed": 10.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -44,22 +71,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 8, 12, 96], - "perTipPickupCurrent": { - "1": 0.02, - "2": 0.03, - "3": 0.05, - "4": 0.06, - "5": 0.08, - "6": 0.09, - "7": 0.11, - "8": 0.13, - "12": 0.19, - "16": 0.25, - "24": 0.38, - "48": 0.75, - "96": 1.5 - } + "availableConfigurations": [1, 8, 12, 16, 24, 48, 96] }, "backCompatNames": [], "channels": 96, @@ -71,7 +83,6 @@ "current": 0.8, "speed": 5 }, - "tipPresenceCheckDistanceMM": "8.0", - "connectTiprackDistanceMM": "7.0", - "endTipActionRetractDistanceMM": "2.0" + "tipPresenceCheckDistanceMM": 8.0, + "endTipActionRetractDistanceMM": 2.0 } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json index 2494dfeccca..264c351d0b5 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json @@ -4,19 +4,45 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 0.0, - "speed": 5.5, - "increment": 0.0, - "distance": 10.0, - "prep_move_distance": 8.25, - "prep_move_speed": 10.0 + "pressFit": { + "presses": 1, + "increment": 0.0, + "speed": 10.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.25, + "3": 0.3, + "4": 0.35, + "5": 0.4, + "6": 0.45, + "7": 0.5, + "8": 0.55, + "12": 0.19, + "16": 0.25, + "24": 0.38, + "48": 0.75 + } + }, + "camAction": { + "speed": 5.5, + "distance": 10.0, + "prep_move_distance": 8.25, + "prep_move_speed": 10.0, + "connectTiprackDistanceMM": 7.0, + "currentByTipCount": { + "96": 1.5 + } + } }, "dropTipConfigurations": { - "current": 1.5, - "speed": 5.5, - "distance": 10.8, - "prep_move_distance": 19.0, - "prep_move_speed": 10.0 + "camAction": { + "current": 1.5, + "speed": 5.5, + "distance": 10.8, + "prep_move_distance": 19.0, + "prep_move_speed": 10.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -44,22 +70,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 8, 12, 16, 24, 48, 96], - "perTipPickupCurrent": { - "1": 0.02, - "2": 0.03, - "3": 0.05, - "4": 0.06, - "5": 0.08, - "6": 0.09, - "7": 0.11, - "8": 0.13, - "12": 0.19, - "16": 0.25, - "24": 0.38, - "48": 0.75, - "96": 1.5 - } + "availableConfigurations": [1, 8, 12, 16, 24, 48, 96] }, "backCompatNames": [], "channels": 96, @@ -71,7 +82,6 @@ "current": 0.8, "speed": 5 }, - "tipPresenceCheckDistanceMM": "8.0", - "connectTiprackDistanceMM": "7.0", - "endTipActionRetractDistanceMM": "2.0" + "tipPresenceCheckDistanceMM": 8.0, + "endTipActionRetractDistanceMM": 2.0 } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p10/1_0.json b/shared-data/pipette/definitions/2/general/single_channel/p10/1_0.json index 1d66d202d5f..802c8c7b89b 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p10/1_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p10/1_0.json @@ -4,17 +4,21 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 1.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p10/1_3.json b/shared-data/pipette/definitions/2/general/single_channel/p10/1_3.json index 93bc4d1e0a2..d202a50bc01 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p10/1_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p10/1_3.json @@ -4,17 +4,21 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 1.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p10/1_4.json b/shared-data/pipette/definitions/2/general/single_channel/p10/1_4.json index 44fba8ac981..e1668578f56 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p10/1_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p10/1_4.json @@ -4,17 +4,21 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 1.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p10/1_5.json b/shared-data/pipette/definitions/2/general/single_channel/p10/1_5.json index 44fba8ac981..e1668578f56 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p10/1_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p10/1_5.json @@ -4,17 +4,21 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 1.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_0.json index e2145b88696..771b4ca2e80 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_0.json @@ -4,17 +4,21 @@ "model": "p1000", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 15.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 15.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 9.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_3.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_3.json index 2cfca4bd684..4aa2f4c192f 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_3.json @@ -4,17 +4,21 @@ "model": "p1000", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 15.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 15.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.7, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.7, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 9.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_4.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_4.json index 2cfca4bd684..4aa2f4c192f 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_4.json @@ -4,17 +4,21 @@ "model": "p1000", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 15.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 15.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.7, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.7, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 9.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_5.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_5.json index 4199255baf5..d4244fd5d61 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_5.json @@ -4,17 +4,21 @@ "model": "p1000", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 15.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 15.0, + "currentByTipCount": { + "1": 0.15 + } + } }, "dropTipConfigurations": { - "current": 0.7, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.7, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.15 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 9.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_0.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_0.json index 90dca46ec79..f7478e116ec 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_0.json @@ -4,17 +4,21 @@ "model": "p1000", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 17.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 17.0, + "currentByTipCount": { + "1": 0.17 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.17 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 6.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_1.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_1.json index 9ecf3e0909b..52e697b7dc5 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_1.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_1.json @@ -4,17 +4,21 @@ "model": "p1000", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 17.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 17.0, + "currentByTipCount": { + "1": 0.17 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.17 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 6.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_2.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_2.json index 9ecf3e0909b..52e697b7dc5 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_2.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_2.json @@ -4,17 +4,21 @@ "model": "p1000", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 17.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 17.0, + "currentByTipCount": { + "1": 0.17 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.17 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 6.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json index 3475cb66bd4..320387e75b2 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json @@ -4,14 +4,21 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 5, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 5, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -39,9 +46,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "perTipPickupCurrent": { - "1": 0.15 - } + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -52,6 +57,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json index 3475cb66bd4..320387e75b2 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json @@ -4,14 +4,21 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 5, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 5, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -39,9 +46,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "perTipPickupCurrent": { - "1": 0.15 - } + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -52,6 +57,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json index 1298625e578..209ff556963 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json @@ -4,14 +4,21 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15 + "plungerEject": { + "current": 1.0, + "speed": 15 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -39,9 +46,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "perTipPickupCurrent": { - "1": 0.2 - } + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -52,6 +57,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json index 1298625e578..209ff556963 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json @@ -4,14 +4,21 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15 + "plungerEject": { + "current": 1.0, + "speed": 15 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -39,9 +46,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "perTipPickupCurrent": { - "1": 0.2 - } + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -52,6 +57,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p20/2_0.json b/shared-data/pipette/definitions/2/general/single_channel/p20/2_0.json index 1b5a8b392ae..105d5e8b24b 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p20/2_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p20/2_0.json @@ -4,17 +4,21 @@ "model": "p20", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 14.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 14.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.0, + "speed": 15.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 1.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p20/2_1.json b/shared-data/pipette/definitions/2/general/single_channel/p20/2_1.json index fd041576730..78c0f57cb88 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p20/2_1.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p20/2_1.json @@ -4,17 +4,21 @@ "model": "p20", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 14.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 14.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.0, + "speed": 15.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 1.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p20/2_2.json b/shared-data/pipette/definitions/2/general/single_channel/p20/2_2.json index fd041576730..78c0f57cb88 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p20/2_2.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p20/2_2.json @@ -4,17 +4,21 @@ "model": "p20", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 14.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 14.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.0, + "speed": 15.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 1.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/1_0.json b/shared-data/pipette/definitions/2/general/single_channel/p300/1_0.json index ebd868f0a03..889cd38633f 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/1_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/1_0.json @@ -4,17 +4,21 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 5.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/1_3.json b/shared-data/pipette/definitions/2/general/single_channel/p300/1_3.json index 426e391d012..ff662bdb79e 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/1_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/1_3.json @@ -4,17 +4,21 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 5.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/1_4.json b/shared-data/pipette/definitions/2/general/single_channel/p300/1_4.json index 6d469c910ba..6c36696faa2 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/1_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/1_4.json @@ -4,17 +4,21 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 5.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/1_5.json b/shared-data/pipette/definitions/2/general/single_channel/p300/1_5.json index 6d469c910ba..6c36696faa2 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/1_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/1_5.json @@ -4,17 +4,21 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 5.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/2_0.json b/shared-data/pipette/definitions/2/general/single_channel/p300/2_0.json index d5aea0fa6cf..8166c7dbaeb 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/2_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/2_0.json @@ -4,17 +4,21 @@ "model": "p300", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 17.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 17.0, + "currentByTipCount": { + "1": 0.125 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.125 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 3.5, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/2_1.json b/shared-data/pipette/definitions/2/general/single_channel/p300/2_1.json index d8c36634655..4342b7cf7c9 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/2_1.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/2_1.json @@ -4,17 +4,21 @@ "model": "p300", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 17.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 17.0, + "currentByTipCount": { + "1": 0.125 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.125 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 3.5, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/1_0.json b/shared-data/pipette/definitions/2/general/single_channel/p50/1_0.json index f07934753bb..ef647527024 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/1_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/1_0.json @@ -4,17 +4,21 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 2.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/1_3.json b/shared-data/pipette/definitions/2/general/single_channel/p50/1_3.json index 01bce6c8c90..4820cd9271e 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/1_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/1_3.json @@ -4,17 +4,21 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 2.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/1_4.json b/shared-data/pipette/definitions/2/general/single_channel/p50/1_4.json index a205e79cbc8..1ba3ca439f6 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/1_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/1_4.json @@ -4,17 +4,21 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 2.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/1_5.json b/shared-data/pipette/definitions/2/general/single_channel/p50/1_5.json index a205e79cbc8..1ba3ca439f6 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/1_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/1_5.json @@ -4,17 +4,21 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -33,10 +37,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "availableConfigurations": null, - "perTipPickupCurrent": { - "1": 0.1 - } + "availableConfigurations": null }, "channels": 1, "shaftDiameter": 2.0, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json index 04aaedc093a..1c81a8b8a2f 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json @@ -4,14 +4,21 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 5, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 5, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,9 +52,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "perTipPickupCurrent": { - "1": 0.15 - } + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -58,6 +63,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json index 04aaedc093a..1c81a8b8a2f 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json @@ -4,14 +4,21 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 5, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 5, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,9 +52,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "perTipPickupCurrent": { - "1": 0.15 - } + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -58,6 +63,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json index b46c3773f3c..43961d8b22a 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json @@ -4,14 +4,21 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15 + "plungerEject": { + "current": 1.0, + "speed": 15 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,9 +52,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "perTipPickupCurrent": { - "1": 0.2 - } + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -58,6 +63,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json index b46c3773f3c..43961d8b22a 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json @@ -4,14 +4,21 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15 + "plungerEject": { + "current": 1.0, + "speed": 15 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,9 +52,7 @@ }, "partialTipConfigurations": { "partialTipSupported": false, - "perTipPickupCurrent": { - "1": 0.2 - } + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -58,6 +63,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_0.json index 62ab42193de..d214a493141 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_0.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p10/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], "B1": [0.0, 22.5, 0.8], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_3.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_3.json index 62ab42193de..d214a493141 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_3.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p10/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], "B1": [0.0, 22.5, 0.8], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_4.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_4.json index 62ab42193de..d214a493141 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_4.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p10/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], "B1": [0.0, 22.5, 0.8], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_5.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_5.json index 62ab42193de..d214a493141 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_5.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p10/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], "B1": [0.0, 22.5, 0.8], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_6.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_6.json index 62ab42193de..d214a493141 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_6.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_6.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p10/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], "B1": [0.0, 22.5, 0.8], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/1_0.json index 280d149c350..85dd2ff91e3 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/1_0.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], "B1": [-8.0, -25.0, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_0.json index 280d149c350..85dd2ff91e3 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_0.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], "B1": [-8.0, -25.0, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_3.json index 280d149c350..85dd2ff91e3 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_3.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], "B1": [-8.0, -25.0, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_4.json index 280d149c350..85dd2ff91e3 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_4.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], "B1": [-8.0, -25.0, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_5.json index 280d149c350..85dd2ff91e3 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_5.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], "B1": [-8.0, -25.0, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_0.json index 67fdef7b76b..330b04d4bb8 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_0.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 19.4], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p20/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 19.4], "B1": [0.0, 22.5, 19.4], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_1.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_1.json index 67fdef7b76b..330b04d4bb8 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_1.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_1.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 19.4], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p20/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 19.4], "B1": [0.0, 22.5, 19.4], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_0.json index 5a9430100c8..0df1381c03e 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_0.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p300/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], "B1": [0.0, 22.5, 0.8], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_3.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_3.json index 5a9430100c8..0df1381c03e 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_3.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p300/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], "B1": [0.0, 22.5, 0.8], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_4.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_4.json index 5a9430100c8..0df1381c03e 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_4.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p300/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], "B1": [0.0, 22.5, 0.8], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_5.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_5.json index 5a9430100c8..0df1381c03e 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_5.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p300/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], "B1": [0.0, 22.5, 0.8], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_0.json index f79ca279af5..afc2468175b 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_0.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 35.52], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p300/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 35.52], "B1": [0.0, 22.5, 35.52], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_1.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_1.json index f79ca279af5..afc2468175b 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_1.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_1.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 35.52], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p300/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 35.52], "B1": [0.0, 22.5, 35.52], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_0.json index 167a0e0cf79..aa8e5807bb4 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_0.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], "B1": [0.0, 22.5, 0.8], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_3.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_3.json index 167a0e0cf79..aa8e5807bb4 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_3.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], "B1": [0.0, 22.5, 0.8], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_4.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_4.json index 167a0e0cf79..aa8e5807bb4 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_4.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], "B1": [0.0, 22.5, 0.8], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_5.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_5.json index 167a0e0cf79..aa8e5807bb4 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_5.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], "B1": [0.0, 22.5, 0.8], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_0.json index 7fba327c671..cea157936fc 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_0.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], "B1": [-8.0, -25.0, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_3.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_3.json index 7fba327c671..cea157936fc 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_3.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], "B1": [-8.0, -25.0, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_4.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_4.json index 7fba327c671..cea157936fc 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_4.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], "B1": [-8.0, -25.0, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_5.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_5.json index 7fba327c671..cea157936fc 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_5.json @@ -2,6 +2,28 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], "B1": [-8.0, -25.0, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/1_0.json index 8924881092c..770e7a9af58 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/1_0.json @@ -2,6 +2,194 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/ninety_six_channel/p1000/placeholder.gltf", "nozzleOffset": [-36.0, -25.5, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ] + }, + { + "key": "B", + "orderedNozzles": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12" + ] + }, + { + "key": "C", + "orderedNozzles": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12" + ] + }, + { + "key": "D", + "orderedNozzles": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12" + ] + }, + { + "key": "E", + "orderedNozzles": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12" + ] + }, + { + "key": "F", + "orderedNozzles": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12" + ] + }, + { + "key": "G", + "orderedNozzles": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12" + ] + }, + { + "key": "H", + "orderedNozzles": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ] + } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + }, + { + "key": "2", + "orderedNozzles": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"] + }, + { + "key": "3", + "orderedNozzles": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"] + }, + { + "key": "4", + "orderedNozzles": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"] + }, + { + "key": "5", + "orderedNozzles": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"] + }, + { + "key": "6", + "orderedNozzles": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"] + }, + { + "key": "7", + "orderedNozzles": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"] + }, + { + "key": "8", + "orderedNozzles": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"] + }, + { + "key": "9", + "orderedNozzles": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"] + }, + { + "key": "10", + "orderedNozzles": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"] + }, + { + "key": "11", + "orderedNozzles": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"] + }, + { + "key": "12", + "orderedNozzles": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + } + ], "nozzleMap": { "A1": [-36.0, -25.5, -259.15], "A2": [-27.0, -25.5, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_0.json index 8924881092c..770e7a9af58 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_0.json @@ -2,6 +2,194 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/ninety_six_channel/p1000/placeholder.gltf", "nozzleOffset": [-36.0, -25.5, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ] + }, + { + "key": "B", + "orderedNozzles": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12" + ] + }, + { + "key": "C", + "orderedNozzles": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12" + ] + }, + { + "key": "D", + "orderedNozzles": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12" + ] + }, + { + "key": "E", + "orderedNozzles": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12" + ] + }, + { + "key": "F", + "orderedNozzles": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12" + ] + }, + { + "key": "G", + "orderedNozzles": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12" + ] + }, + { + "key": "H", + "orderedNozzles": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ] + } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + }, + { + "key": "2", + "orderedNozzles": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"] + }, + { + "key": "3", + "orderedNozzles": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"] + }, + { + "key": "4", + "orderedNozzles": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"] + }, + { + "key": "5", + "orderedNozzles": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"] + }, + { + "key": "6", + "orderedNozzles": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"] + }, + { + "key": "7", + "orderedNozzles": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"] + }, + { + "key": "8", + "orderedNozzles": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"] + }, + { + "key": "9", + "orderedNozzles": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"] + }, + { + "key": "10", + "orderedNozzles": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"] + }, + { + "key": "11", + "orderedNozzles": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"] + }, + { + "key": "12", + "orderedNozzles": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + } + ], "nozzleMap": { "A1": [-36.0, -25.5, -259.15], "A2": [-27.0, -25.5, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_3.json index 8924881092c..770e7a9af58 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_3.json @@ -2,6 +2,194 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/ninety_six_channel/p1000/placeholder.gltf", "nozzleOffset": [-36.0, -25.5, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ] + }, + { + "key": "B", + "orderedNozzles": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12" + ] + }, + { + "key": "C", + "orderedNozzles": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12" + ] + }, + { + "key": "D", + "orderedNozzles": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12" + ] + }, + { + "key": "E", + "orderedNozzles": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12" + ] + }, + { + "key": "F", + "orderedNozzles": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12" + ] + }, + { + "key": "G", + "orderedNozzles": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12" + ] + }, + { + "key": "H", + "orderedNozzles": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ] + } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + }, + { + "key": "2", + "orderedNozzles": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"] + }, + { + "key": "3", + "orderedNozzles": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"] + }, + { + "key": "4", + "orderedNozzles": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"] + }, + { + "key": "5", + "orderedNozzles": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"] + }, + { + "key": "6", + "orderedNozzles": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"] + }, + { + "key": "7", + "orderedNozzles": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"] + }, + { + "key": "8", + "orderedNozzles": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"] + }, + { + "key": "9", + "orderedNozzles": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"] + }, + { + "key": "10", + "orderedNozzles": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"] + }, + { + "key": "11", + "orderedNozzles": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"] + }, + { + "key": "12", + "orderedNozzles": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + } + ], "nozzleMap": { "A1": [-36.0, -25.5, -259.15], "A2": [-27.0, -25.5, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_4.json index 8924881092c..770e7a9af58 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_4.json @@ -2,6 +2,194 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/ninety_six_channel/p1000/placeholder.gltf", "nozzleOffset": [-36.0, -25.5, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ] + }, + { + "key": "B", + "orderedNozzles": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12" + ] + }, + { + "key": "C", + "orderedNozzles": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12" + ] + }, + { + "key": "D", + "orderedNozzles": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12" + ] + }, + { + "key": "E", + "orderedNozzles": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12" + ] + }, + { + "key": "F", + "orderedNozzles": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12" + ] + }, + { + "key": "G", + "orderedNozzles": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12" + ] + }, + { + "key": "H", + "orderedNozzles": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ] + } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + }, + { + "key": "2", + "orderedNozzles": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"] + }, + { + "key": "3", + "orderedNozzles": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"] + }, + { + "key": "4", + "orderedNozzles": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"] + }, + { + "key": "5", + "orderedNozzles": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"] + }, + { + "key": "6", + "orderedNozzles": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"] + }, + { + "key": "7", + "orderedNozzles": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"] + }, + { + "key": "8", + "orderedNozzles": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"] + }, + { + "key": "9", + "orderedNozzles": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"] + }, + { + "key": "10", + "orderedNozzles": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"] + }, + { + "key": "11", + "orderedNozzles": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"] + }, + { + "key": "12", + "orderedNozzles": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + } + ], "nozzleMap": { "A1": [-36.0, -25.5, -259.15], "A2": [-27.0, -25.5, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_5.json index 8924881092c..770e7a9af58 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_5.json @@ -2,6 +2,194 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/ninety_six_channel/p1000/placeholder.gltf", "nozzleOffset": [-36.0, -25.5, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ] + }, + { + "key": "B", + "orderedNozzles": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12" + ] + }, + { + "key": "C", + "orderedNozzles": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12" + ] + }, + { + "key": "D", + "orderedNozzles": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12" + ] + }, + { + "key": "E", + "orderedNozzles": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12" + ] + }, + { + "key": "F", + "orderedNozzles": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12" + ] + }, + { + "key": "G", + "orderedNozzles": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12" + ] + }, + { + "key": "H", + "orderedNozzles": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ] + } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + }, + { + "key": "2", + "orderedNozzles": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"] + }, + { + "key": "3", + "orderedNozzles": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"] + }, + { + "key": "4", + "orderedNozzles": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"] + }, + { + "key": "5", + "orderedNozzles": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"] + }, + { + "key": "6", + "orderedNozzles": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"] + }, + { + "key": "7", + "orderedNozzles": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"] + }, + { + "key": "8", + "orderedNozzles": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"] + }, + { + "key": "9", + "orderedNozzles": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"] + }, + { + "key": "10", + "orderedNozzles": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"] + }, + { + "key": "11", + "orderedNozzles": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"] + }, + { + "key": "12", + "orderedNozzles": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + } + ], "nozzleMap": { "A1": [-36.0, -25.5, -259.15], "A2": [-27.0, -25.5, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_0.json index 8411e4e44dd..46813106020 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 12.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p10/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 12.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_3.json index 8411e4e44dd..46813106020 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_3.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 12.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p10/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 12.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_4.json index 8411e4e44dd..46813106020 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_4.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 12.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p10/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 12.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_5.json index 8411e4e44dd..46813106020 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_5.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 12.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p10/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 12.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_0.json index 64e0cde09c9..6e0f777ff79 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 45.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 45.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_3.json index 64e0cde09c9..6e0f777ff79 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_3.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 45.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 45.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_4.json index 64e0cde09c9..6e0f777ff79 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_4.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 45.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 45.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_5.json index 64e0cde09c9..6e0f777ff79 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_5.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 45.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 45.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_0.json index a69cd078a60..03e44b6b76d 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 50.14], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 50.14] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_1.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_1.json index a69cd078a60..03e44b6b76d 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_1.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_1.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 50.14], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 50.14] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_2.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_2.json index a69cd078a60..03e44b6b76d 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_2.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_2.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 50.14], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 50.14] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_0.json index e2a461538c3..50baf715160 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_3.json index e2a461538c3..50baf715160 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_3.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_4.json index e2a461538c3..50baf715160 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_4.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_5.json index e2a461538c3..50baf715160 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_5.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_0.json index 30c3eb29e71..db8fa79ea87 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 10.45], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p20/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 10.45] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_1.json b/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_1.json index 30c3eb29e71..db8fa79ea87 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_1.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_1.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 10.45], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p20/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 10.45] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_2.json b/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_2.json index 30c3eb29e71..db8fa79ea87 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_2.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_2.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 10.45], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p20/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 10.45] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_0.json index 3d97efe13fe..636b42cd596 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p300/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_3.json index 3d97efe13fe..636b42cd596 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_3.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p300/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_4.json index 3d97efe13fe..636b42cd596 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_4.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p300/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_5.json index 3d97efe13fe..636b42cd596 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_5.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p300/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_0.json index 1b56f8b667a..c43a1fd1f10 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 29.45], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p300/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 29.45] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_1.json b/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_1.json index 1b56f8b667a..c43a1fd1f10 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_1.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_1.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 29.45], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p300/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 29.45] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_0.json index 1d4a8b299bc..a67e53f92f5 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_3.json index 1d4a8b299bc..a67e53f92f5 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_3.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_4.json index 1d4a8b299bc..a67e53f92f5 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_4.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_5.json index 1d4a8b299bc..a67e53f92f5 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_5.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_0.json index 30252f1ef03..96414e8ec41 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_3.json index 30252f1ef03..96414e8ec41 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_3.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_4.json index 30252f1ef03..96414e8ec41 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_4.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_5.json index 30252f1ef03..96414e8ec41 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_5.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/schemas/2/pipetteGeometrySchema.json b/shared-data/pipette/schemas/2/pipetteGeometrySchema.json index df75f98ef08..4dd243304f9 100644 --- a/shared-data/pipette/schemas/2/pipetteGeometrySchema.json +++ b/shared-data/pipette/schemas/2/pipetteGeometrySchema.json @@ -7,6 +7,20 @@ "items": { "type": "number" }, "minItems": 3, "maxItems": 3 + }, + "nozzleName": { + "type": "string", + "pattern": "[A-Z]+[0-9]+" + }, + "rowName": { + "type": "string", + "pattern": "[A-Z]+", + "description": "The name of a row of nozzles" + }, + "columnName": { + "type": "string", + "pattern": "[0-9]+", + "description": "The name of a column of nozzles" } }, "type": "object", @@ -23,6 +37,38 @@ "type": "string", "pattern": "^pipette/definitions/[2]/geometry/([a-z]*_[a-z]*)+/p[0-9]{2,4}/[a-z]*[.]gltf" }, + "orderedRows": { + "type": "array", + "items": { + "type": "object", + "description": "A row of nozzle keys", + "required": ["key", "orderedNozzles"], + "properties": { + "key": { "$ref": "#/definitions/rowName" }, + "orderedNozzles": { + "type": "array", + "description": "The list of nozzle names in this row", + "items": { "$ref": "#/definitions/nozzleName" } + } + } + } + }, + "orderedColumns": { + "type": "array", + "items": { + "type": "object", + "description": "A column of nozzle keys", + "required": ["key", "orderedNozzles"], + "properties": { + "key": { "$ref": "#/definitions/columnName" }, + "orderedNozzles": { + "type": "array", + "description": "The list of nozzle names in this column", + "items": { "$ref": "#/definitions/nozzleName" } + } + } + } + }, "nozzleMap": { "type": "object", "description": "Unordered object of well objects with position and dimensional information", diff --git a/shared-data/pipette/schemas/2/pipettePropertiesSchema.json b/shared-data/pipette/schemas/2/pipettePropertiesSchema.json index d2b2a7c78fa..b45870167c5 100644 --- a/shared-data/pipette/schemas/2/pipettePropertiesSchema.json +++ b/shared-data/pipette/schemas/2/pipettePropertiesSchema.json @@ -30,6 +30,16 @@ "type": { "type": "string" }, "displayName": { "type": "string" } } + }, + "partialTipCount": { + "type": "number", + "enum": [1, 2, 3, 4, 5, 6, 7, 8, 12, 96, 384] + }, + "currentByTipCount": { + "type": "object", + "patternProperties": { + "\\d+": { "$ref": "#/definitions/currentRange" } + } } }, "description": "Version-level pipette specifications, which may vary across different versions of the same pipette", @@ -66,38 +76,24 @@ "channels": { "$ref": "#/definitions/channels" }, "partialTipConfigurations": { "description": "Object containing information on partial tip configurations", - "$oneof": [ + "oneof": [ { "type": "object", "required": ["partialTipSupported"], "properties": { - "partialTipSupported": { "type": "boolean", "value": true } + "partialTipSupported": { "const": false }, + "availableConfigurations": null } }, { "type": "object", - "required": [ - "partialTipSupported", - "availableConfigurations", - "perTipPickupCurrent" - ], + "required": ["partialTipSupported", "availableConfigurations"], "properties": { - "partialTipSupported": { "type": "boolean", "value": false }, + "partialTipSupported": { "const": true }, "availableConfigurations": { "type": "array", "description": "Array of available configurations", - "items": { - "type": "number", - "enum": [1, 2, 3, 4, 5, 6, 7, 8, 12, 96, 384] - }, - "perTipPickupCurrent": { - "type": "object", - "patternProperties": { - "\\d+": { - "$ref": "#/definitions/positiveNumber" - } - } - } + "items": { "$ref": "#/definitions/partialTipCount" } } } } @@ -162,42 +158,124 @@ } }, "pickUpTipConfigurations": { - "type": "object", "description": "Object containing configurations for picking up tips common to all partial configurations", - "required": ["speed"], - "properties": { - "presses": { "$ref": "#/definitions/positiveNumber" }, - "speed": { "$ref": "#/definitions/positiveNumber" }, - "increment": { "$ref": "#/definitions/positiveNumber" }, - "distance": { "$ref": "#/definitions/positiveNumber" } - } + "anyOf": [ + { + "type": "object", + "required": ["pressFit"], + "properties": { + "pressFit": { + "type": "object", + "required": [ + "presses", + "speed", + "increment", + "distance", + "currentByTipCount" + ], + "additionalProperties": false, + "properties": { + "presses": { "$ref": "#/definitions/positiveNumber" }, + "speed": { "$ref": "#/definitions/positiveNumber" }, + "increment": { "$ref": "#/definitions/positiveNumber" }, + "distance": { "$ref": "#/definitions/positiveNumber" }, + "currentByTipCount": { + "$ref": "#/definitions/currentByTipCount" + } + } + } + } + }, + { + "type": "object", + "required": ["camAction"], + "properties": { + "camAction": { + "type": "object", + "required": [ + "prep_move_distance", + "prep_move_speed", + "speed", + "distance", + "currentByTipCount" + ], + "additionalProperties": false, + "properties": { + "prep_move_distance": { + "$ref": "#/definitions/positiveNumber" + }, + "prep_move_speed": { "$ref": "#/definitions/positiveNumber" }, + "speed": { "$ref": "#/definitions/positiveNumber" }, + "distance": { "$ref": "#/definitions/positiveNumber" }, + "currentByTipCount": { + "$ref": "#/definitions/currentByTipCount" + }, + "connectTiprackDistanceMM": { + "$ref": "#/definitions/positiveNumber" + } + } + } + } + } + ] }, "dropTipConfigurations": { "type": "object", "description": "Object containing configurations specific to dropping tips", - "required": ["current", "speed"], - "properties": { - "current": { "$ref": "#/definitions/currentRange" }, - "presses": {}, - "speed": { "$ref": "#/definitions/positiveNumber" }, - "increment": {}, - "distance": {} - } + "anyOf": [ + { + "type": "object", + "required": ["plungerEject"], + "properties": { + "plungerEject": { + "type": "object", + "required": ["current", "speed"], + "additionalProperties": false, + "properties": { + "current": { "$ref": "#/definitions/currentRange" }, + "speed": { "$ref": "#/definitions/positiveNumber" } + } + } + } + }, + { + "type": "object", + "required": ["camAction"], + "properties": { + "camAction": { + "type": "object", + "required": [ + "current", + "prep_move_distance", + "prep_move_speed", + "distance", + "speed" + ], + "additionalProperties": false, + "properties": { + "current": { "$ref": "#/definitions/currentRange" }, + "prep_move_distance": { + "$ref": "#/definitions/positiveNumber" + }, + "prep_move_speed": { "$ref": "#/definitions/positiveNumber" }, + "distance": { "$ref": "#/definitions/positiveNumber" }, + "speed": { "$ref": "#/definitions/positiveNumber" } + } + } + } + } + ] }, "displayName": { "type": "string", "description": "Display name of the pipette include model and generation number in readable format." }, "tipPresenceCheckDistanceMM": { - "type": "string", + "$ref": "#/definitions/positiveNumber", "description": "The distance to move the gear motors down to check tip presence status." }, - "connectTiprackDistanceMM": { - "type": "string", - "description": "The distance to move the z stage down to connect with the tiprack before clamping." - }, "endTipActionRetractDistanceMM": { - "type": "string", + "$ref": "#/definitions/positiveNumber", "description": "The distance to move the z stage up after a tip pickup or dropoff." }, "model": { diff --git a/shared-data/python/opentrons_shared_data/pipette/load_data.py b/shared-data/python/opentrons_shared_data/pipette/load_data.py index abf5fa1c2b0..4dc6b200574 100644 --- a/shared-data/python/opentrons_shared_data/pipette/load_data.py +++ b/shared-data/python/opentrons_shared_data/pipette/load_data.py @@ -1,7 +1,7 @@ import json import os -from typing import Dict, Any, Union, Optional +from typing import Dict, Any, Union, Optional, List from typing_extensions import Literal from functools import lru_cache @@ -152,7 +152,41 @@ def _change_to_camel_case(c: str) -> str: return f"{config_name[0]}" + "".join(s.capitalize() for s in config_name[1::]) -def update_pipette_configuration( # noqa: C901 +def _edit_non_quirk_with_lc_override( + mutable_config_key: str, + new_mutable_value: Any, + base_dict: Dict[str, Any], + liquid_class: Optional[LiquidClasses], +) -> None: + def _do_edit_non_quirk( + new_value: Any, existing: Dict[Any, Any], keypath: List[Any] + ) -> None: + thiskey: Any = keypath[0] + if thiskey in [lc.name for lc in LiquidClasses]: + if liquid_class: + thiskey = liquid_class + else: + thiskey = LiquidClasses[thiskey] + if len(keypath) > 1: + restkeys = keypath[1:] + if thiskey == "##EACHTIP##": + for key in existing.keys(): + _do_edit_non_quirk(new_value, existing[key], restkeys) + else: + _do_edit_non_quirk(new_value, existing[thiskey], restkeys) + else: + # This was the last key + if thiskey == "##EACHTIP##": + for key in existing.keys(): + existing[key] = new_value + else: + existing[thiskey] = new_value + + new_names = _MAP_KEY_TO_V2[mutable_config_key] + _do_edit_non_quirk(new_mutable_value, base_dict, new_names) + + +def update_pipette_configuration( base_configurations: PipetteConfigurations, v1_configuration_changes: Dict[str, Any], liquid_class: Optional[LiquidClasses] = None, @@ -169,40 +203,11 @@ def update_pipette_configuration( # noqa: C901 lookup_key = _change_to_camel_case(c) if c == "quirks" and isinstance(v, dict): quirks_list.extend([b.name for b in v.values() if b.value]) - elif liquid_class: - if lookup_key == "tipLength": - new_names = _MAP_KEY_TO_V2[lookup_key] - top_name = new_names["top_level_name"] - nested_name = new_names["nested_name"] - # This is only a concern for OT-2 configs and I think we can - # be less smart about handling multiple tip types by updating - # all tips. - for k in dict_of_base_model["liquid_properties"][liquid_class][ - new_names["top_level_name"] - ].keys(): - dict_of_base_model["liquid_properties"][liquid_class][top_name][k][ - nested_name - ] = v - else: - dict_of_base_model["liquid_properties"][liquid_class].pop(lookup_key) - dict_of_base_model["liquid_properties"][liquid_class][lookup_key] = v else: - try: - dict_of_base_model.pop(lookup_key) - dict_of_base_model[lookup_key] = v - except KeyError: - # The name is not the same format as previous so - # we need to look it up from the V2 key map - new_names = _MAP_KEY_TO_V2[lookup_key] - top_name = new_names["top_level_name"] - nested_name = new_names["nested_name"] - if new_names.get("liquid_class"): - # isinstances are needed for type checking. - liquid_class = LiquidClasses[new_names["liquid_class"]] - dict_of_base_model[top_name][liquid_class][nested_name] = v - else: - # isinstances are needed for type checking. - dict_of_base_model[top_name][nested_name] = v + _edit_non_quirk_with_lc_override( + lookup_key, v, dict_of_base_model, liquid_class + ) + dict_of_base_model["quirks"] = list( set(dict_of_base_model["quirks"]) - set(quirks_list) ) diff --git a/shared-data/python/opentrons_shared_data/pipette/model_constants.py b/shared-data/python/opentrons_shared_data/pipette/model_constants.py index 00c577823d2..ea79ecd1154 100644 --- a/shared-data/python/opentrons_shared_data/pipette/model_constants.py +++ b/shared-data/python/opentrons_shared_data/pipette/model_constants.py @@ -1,4 +1,4 @@ -from typing import Dict, Union +from typing import Dict, Union, List from .types import ( Quirks, @@ -54,57 +54,29 @@ RESTRICTED_MUTABLE_CONFIG_KEYS = [*VALID_QUIRKS, "model"] -_MAP_KEY_TO_V2: Dict[str, Dict[str, str]] = { - "top": { - "top_level_name": "plungerPositionsConfigurations", - "nested_name": "top", - "liquid_class": "default", - }, - "bottom": { - "top_level_name": "plungerPositionsConfigurations", - "nested_name": "bottom", - "liquid_class": "default", - }, - "blowout": { - "top_level_name": "plungerPositionsConfigurations", - "nested_name": "blowout", - "liquid_class": "default", - }, - "dropTip": { - "top_level_name": "plungerPositionsConfigurations", - "nested_name": "drop", - "liquid_class": "default", - }, - "pickUpCurrent": { - "top_level_name": "partialTipConfigurations", - "nested_name": "perTipPickupCurrent", - }, - "pickUpDistance": { - "top_level_name": "pickUpTipConfigurations", - "nested_name": "distance", - }, - "pickUpIncrement": { - "top_level_name": "pickUpTipConfigurations", - "nested_name": "increment", - }, - "pickUpPresses": { - "top_level_name": "pickUpTipConfigurations", - "nested_name": "presses", - }, - "pickUpSpeed": { - "top_level_name": "pickUpTipConfigurations", - "nested_name": "speed", - }, - "plungerCurrent": { - "top_level_name": "plungerMotorConfigurations", - "nested_name": "run", - }, - "dropTipCurrent": { - "top_level_name": "dropTipConfigurations", - "nested_name": "current", - }, - "dropTipSpeed": {"top_level_name": "dropTipConfigurations", "nested_name": "speed"}, - "tipLength": {"top_level_name": "supportedTips", "nested_name": "defaultTipLength"}, + +_MAP_KEY_TO_V2: Dict[str, List[str]] = { + "top": ["plungerPositionsConfigurations", "default", "top"], + "bottom": ["plungerPositionsConfigurations", "default", "bottom"], + "blowout": ["plungerPositionsConfigurations", "default", "blowout"], + "dropTip": ["plungerPositionsConfigurations", "default", "drop"], + "pickUpCurrent": ["pickUpTipConfigurations", "pressFit", "currentByTipCount"], + "pickUpDistance": ["pickUpTipConfigurations", "pressFit", "distance"], + "pickUpIncrement": ["pickUpTipConfigurations", "pressFit", "increment"], + "pickUpPresses": ["pickUpTipConfigurations", "pressFit", "presses"], + "pickUpSpeed": ["pickUpTipConfigurations", "pressFit", "speed"], + "plungerCurrent": ["plungerMotorConfigurations", "run"], + "dropTipCurrent": ["dropTipConfigurations", "plungerEject", "current"], + "dropTipSpeed": ["dropTipConfigurations", "plungerEject", "speed"], + "maxVolume": ["liquid_properties", "default", "maxVolume"], + "minVolume": ["liquid_properties", "default", "minVolume"], + "tipLength": [ + "liquid_properties", + "default", + "supportedTips", + "##EACHTIP##", + "defaultTipLength", + ], } diff --git a/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py b/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py index 2475353e12b..93d4b4c7d53 100644 --- a/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py +++ b/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py @@ -40,6 +40,34 @@ LIQUID_CLASS = LiquidClasses.default +def _edit_non_quirk( + mutable_config_key: str, new_mutable_value: MutableConfig, base_dict: Dict[str, Any] +) -> None: + def _do_edit_non_quirk( + new_value: MutableConfig, existing: Dict[Any, Any], keypath: List[Any] + ) -> None: + thiskey: Any = keypath[0] + if thiskey in [lc.name for lc in LiquidClasses]: + thiskey = LiquidClasses[thiskey] + if len(keypath) > 1: + restkeys = keypath[1:] + if thiskey == "##EACHTIP##": + for key in existing.keys(): + _do_edit_non_quirk(new_value, existing[key], restkeys) + else: + _do_edit_non_quirk(new_value, existing[thiskey], restkeys) + else: + # This was the last key + if thiskey == "##EACHTIP##": + for key in existing.keys(): + existing[key] = new_value.value + else: + existing[thiskey] = new_value.value + + new_names = _MAP_KEY_TO_V2[mutable_config_key] + _do_edit_non_quirk(new_mutable_value, base_dict, new_names) + + def _migrate_to_v2_configurations( base_configurations: PipetteConfigurations, v1_mutable_configs: OverrideType, @@ -59,26 +87,9 @@ def _migrate_to_v2_configurations( continue if c == "quirks" and isinstance(v, dict): quirks_list.extend([b.name for b in v.values() if b.value]) - else: - new_names = _MAP_KEY_TO_V2[c] - top_name = new_names["top_level_name"] - nested_name = new_names["nested_name"] - if c == "tipLength" and isinstance(v, MutableConfig): - # This is only a concern for OT-2 configs and I think we can - # be less smart about handling multiple tip types by updating - # all tips. - for k in dict_of_base_model["liquid_properties"][LIQUID_CLASS][ - new_names["top_level_name"] - ].keys(): - dict_of_base_model["liquid_properties"][LIQUID_CLASS][top_name][k][ - nested_name - ] = v - elif new_names.get("liquid_class") and isinstance(v, MutableConfig): - _class = LiquidClasses[new_names["liquid_class"]] - dict_of_base_model[top_name][_class][nested_name] = v.value - elif isinstance(v, MutableConfig): - # isinstances are needed for type checking. - dict_of_base_model[top_name][nested_name] = v.value + elif isinstance(v, MutableConfig): + _edit_non_quirk(c, v, dict_of_base_model) + dict_of_base_model["quirks"] = list( set(dict_of_base_model["quirks"]).union(set(quirks_list)) ) @@ -143,10 +154,40 @@ def _list_all_mutable_configs( return default_configurations +def _get_default_value_for(config: Dict[str, Any], keypath: List[str]) -> Any: + def _do_get_default_value_for( + remaining_config: Dict[Any, Any], keypath: List[str] + ) -> Any: + first: Any = keypath[0] + if first in [lc.name for lc in LiquidClasses]: + first = LiquidClasses[first] + if len(keypath) > 1: + rest = keypath[1:] + if first == "##EACHTIP##": + tip_list = list(remaining_config.keys()) + tip_list.sort(key=lambda o: o.value) + return _do_get_default_value_for(remaining_config[tip_list[-1]], rest) + else: + return _do_get_default_value_for(remaining_config[first], rest) + else: + if first == "###EACHTIP##": + tip_list = list(remaining_config.keys()) + tip_list.sort(key=lambda o: o.value) + return remaining_config[tip_list[-1]] + elif first == "currentByTipCount": + # return the value for the most tips at a time + cbt = remaining_config[first] + return cbt[next(reversed(sorted(cbt.keys())))] + else: + return remaining_config[first] + + return _do_get_default_value_for(config, keypath) + + def _find_default(name: str, configs: Dict[str, Any]) -> MutableConfig: """Find the default value from the configs and return it as a mutable config.""" - lookup_dict = _MAP_KEY_TO_V2[name] - nested_name = lookup_dict["nested_name"] + keypath = _MAP_KEY_TO_V2[name] + nested_name = keypath[-1] if name == "pickUpCurrent": min_max_dict = _MIN_MAX_LOOKUP["current"] @@ -156,27 +197,7 @@ def _find_default(name: str, configs: Dict[str, Any]) -> MutableConfig: min_max_dict = _MIN_MAX_LOOKUP[nested_name] type_lookup = _TYPE_LOOKUP[nested_name] units_lookup = _UNITS_LOOKUP[nested_name] - if name == "tipLength": - # This is only a concern for OT-2 configs and I think we can - # be less smart about handling multiple tip types. Instead, just - # get the max tip type. - tip_list = list( - configs["liquid_properties"][LIQUID_CLASS][ - lookup_dict["top_level_name"] - ].keys() - ) - tip_list.sort(key=lambda o: o.value) - default_value = configs["liquid_properties"][LIQUID_CLASS][ - lookup_dict["top_level_name"] - ][tip_list[-1]][nested_name] - elif name == "pickUpCurrent": - default_value_dict = configs[lookup_dict["top_level_name"]][nested_name] - default_value = default_value_dict[configs["channels"].value] - elif lookup_dict.get("liquid_class"): - _class = LiquidClasses[lookup_dict["liquid_class"]] - default_value = configs[lookup_dict["top_level_name"]][_class][nested_name] - else: - default_value = configs[lookup_dict["top_level_name"]][nested_name] + default_value = _get_default_value_for(configs, keypath) return MutableConfig( value=default_value, default=default_value, diff --git a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py index 7a58e1b61b2..7374972b800 100644 --- a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py +++ b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py @@ -1,5 +1,5 @@ import re -from typing import List, Dict, Tuple +from typing import List, Dict, Tuple, Optional from pydantic import BaseModel, Field, validator from typing_extensions import Literal from dataclasses import dataclass @@ -9,7 +9,9 @@ PLUNGER_CURRENT_MINIMUM = 0.1 PLUNGER_CURRENT_MAXIMUM = 1.5 -NOZZLE_MAP_NAMES = re.compile(r"[A-Z]{1}[0-9]{1,2}") +NOZZLE_MAP_NAMES = re.compile(r"(?P[A-Z]+)(?P[0-9]+)") +COLUMN_NAMES = re.compile(r"[0-9]+") +ROW_NAMES = re.compile(r"[A-Z]+") # TODO (lc 12-5-2022) Ideally we can deprecate this @@ -150,31 +152,92 @@ class PlungerHomingConfigurations(BaseModel): ) -class TipHandlingConfigurations(BaseModel): +class PressFitPickUpTipConfiguration(BaseModel): presses: int = Field( - default=0.0, description="The number of tries required to force pick up a tip." + ..., + description="The number of times to force pickup (incrementally more each time by increment)", ) - current: float = Field( - default=0.0, - description="The current to use for tip drop-off.", + increment: float = Field( + ..., + description="The increment to move the pipette down on each force tip pickup press", + ) + distance: float = Field( + ..., description="The starting distance to begin a pick up tip from" ) speed: float = Field( + ..., description="The speed to move the Z axis for each force pickup" + ) + current_by_tip_count: Dict[int, float] = Field( ..., - description="The speed to move the z or plunger axis for tip pickup or drop off.", + description="A current dictionary look-up by partial tip configuration.", + alias="currentByTipCount", ) - increment: float = Field( - default=0.0, - description="The increment to move the pipette down for force tip pickup retries.", + + +class CamActionPickUpTipConfiguration(BaseModel): + distance: float = Field(..., description="How far to move the cams once engaged") + speed: float = Field(..., description="How fast to move the cams when engaged") + prep_move_distance: float = Field( + ..., description="How far to move the cams to engage the rack" + ) + prep_move_speed: float = Field( + ..., description="How fast to move the cams when moving to the rack" + ) + current_by_tip_count: Dict[int, float] = Field( + ..., + description="A current dictionary look-up by partial tip configuration.", + alias="currentByTipCount", + ) + connect_tiprack_distance_mm: float = Field( + description="The distance to move the head down to connect with the tiprack before clamping.", + alias="connectTiprackDistanceMM", + ) + + +class PlungerEjectDropTipConfiguration(BaseModel): + current: float = Field( + ..., description="The current to use on the plunger motor when dropping a tip" + ) + speed: float = Field( + ..., description="How fast to move the plunger motor when dropping a tip" + ) + + +class CamActionDropTipConfiguration(BaseModel): + current: float = Field( + ..., description="The current to use on the cam motors when dropping tips" ) distance: float = Field( - default=0.0, description="The distance to begin a pick up tip from." + ..., description="The distance to move the cams when dropping tips" + ) + speed: float = Field( + ..., description="How fast to move the cams when dropping tips" ) prep_move_distance: float = Field( - default=0.0, - description="The distance to move downward before tip pickup or drop-off.", + ..., description="How far to move the cams after disengaging" ) prep_move_speed: float = Field( - default=0.0, description="The speed for the optional preparatory move." + ..., description="How fast to move the cams after disengaging" + ) + + +class DropTipConfigurations(BaseModel): + plunger_eject: Optional[PlungerEjectDropTipConfiguration] = Field( + description="Configuration for tip drop via plunger eject", alias="plungerEject" + ) + cam_action: Optional[CamActionDropTipConfiguration] = Field( + description="Configuration for tip drop via cam action", alias="camAction" + ) + + +class PickUpTipConfigurations(BaseModel): + press_fit: PressFitPickUpTipConfiguration = Field( + description="Configuration for tip pickup via press fit", alias="pressFit" + ) + cam_action: Optional[CamActionPickUpTipConfiguration] = Field( + default=None, + description="Configuration for tip pickup via cam action", + alias="camAction", ) @@ -195,11 +258,6 @@ class PartialTipDefinition(BaseModel): description="A list of the types of partial tip configurations supported, listed by channel ints", alias="availableConfigurations", ) - per_tip_pickup_current: Dict[int, float] = Field( - ..., - description="A current dictionary look-up by partial tip configuration.", - alias="perTipPickupCurrent", - ) class PipettePhysicalPropertiesDefinition(BaseModel): @@ -223,10 +281,10 @@ class PipettePhysicalPropertiesDefinition(BaseModel): display_category: pip_types.PipetteGenerationType = Field( ..., description="The product model of the pipette.", alias="displayCategory" ) - pick_up_tip_configurations: TipHandlingConfigurations = Field( + pick_up_tip_configurations: PickUpTipConfigurations = Field( ..., alias="pickUpTipConfigurations" ) - drop_tip_configurations: TipHandlingConfigurations = Field( + drop_tip_configurations: DropTipConfigurations = Field( ..., alias="dropTipConfigurations" ) plunger_homing_configurations: PlungerHomingConfigurations = Field( @@ -266,14 +324,10 @@ class PipettePhysicalPropertiesDefinition(BaseModel): description="The distance the high throughput tip motors will travel to check tip status.", alias="tipPresenceCheckDistanceMM", ) - connect_tiprack_distance_mm: float = Field( - default=0, - description="The distance to move the head down to connect with the tiprack before clamping.", - alias="connectTiprackDistanceMM", - ) + end_tip_action_retract_distance_mm: float = Field( - default=0, - description="The distance to move the head up after a tip pickup or dropoff.", + default=0.0, + description="The distance to move the head up after a tip drop or pickup.", alias="endTipActionRetractDistanceMM", ) @@ -310,6 +364,28 @@ class Config: } +class PipetteRowDefinition(BaseModel): + key: str + ordered_nozzles: List[str] = Field(..., alias="orderedNozzles") + + @validator("key") + def check_key_is_row(cls, v: str) -> str: + if not ROW_NAMES.search(v): + raise ValueError(f"{v} is not a valid row name") + return v + + +class PipetteColumnDefinition(BaseModel): + key: str + ordered_nozzles: List[str] = Field(..., alias="orderedNozzles") + + @validator("key") + def check_key_is_column(cls, v: str) -> str: + if not COLUMN_NAMES.search(v): + raise ValueError(f"{v} is not a valid column name") + return v + + class PipetteGeometryDefinition(BaseModel): """The geometry properties definition of a pipette.""" @@ -320,6 +396,8 @@ class PipetteGeometryDefinition(BaseModel): alias="pathTo3D", ) nozzle_map: Dict[str, List[float]] = Field(..., alias="nozzleMap") + ordered_columns: List[PipetteColumnDefinition] = Field(..., alias="orderedColumns") + ordered_rows: List[PipetteRowDefinition] = Field(..., alias="orderedRows") @validator("nozzle_map", pre=True) def check_nonempty_strings( diff --git a/shared-data/python/opentrons_shared_data/pipette/scripts/build_json_script.py b/shared-data/python/opentrons_shared_data/pipette/scripts/build_json_script.py index fa0c4956e30..a7af2e30911 100644 --- a/shared-data/python/opentrons_shared_data/pipette/scripts/build_json_script.py +++ b/shared-data/python/opentrons_shared_data/pipette/scripts/build_json_script.py @@ -12,12 +12,15 @@ PipetteGeometryDefinition, PipetteLiquidPropertiesDefinition, PipettePhysicalPropertiesDefinition, - TipHandlingConfigurations, PlungerPositions, SupportedTipsDefinition, MotorConfigurations, PartialTipDefinition, AvailableSensorDefinition, + PickUpTipConfigurations, + PressFitPickUpTipConfiguration, + DropTipConfigurations, + PlungerEjectDropTipConfiguration, ) from ..dev_types import PipetteModelSpec @@ -33,18 +36,19 @@ GEOMETRY_SCHEMA = "#/pipette/schemas/2/pipetteGeometryPropertiesSchema.json" -def _build_tip_handling_configurations( - tip_handling_type: str, model_configurations: Optional[PipetteModelSpec] = None -) -> TipHandlingConfigurations: +def _build_pickup_tip_data( + model_configurations: Optional[PipetteModelSpec] = None, +) -> PickUpTipConfigurations: presses = 0 increment = 0 distance = 0.0 - if tip_handling_type == "pickup" and model_configurations: + if model_configurations: + current = model_configurations["pickUpCurrent"]["value"] speed = model_configurations["pickUpSpeed"]["value"] presses = model_configurations["pickUpPresses"]["value"] increment = int(model_configurations["pickUpIncrement"]["value"]) distance = model_configurations["pickUpDistance"]["value"] - elif tip_handling_type == "pickup": + else: print("Handling pick up tip configurations\n") speed = float(input("please provide the speed\n")) presses = int(input("please provide the number of presses for force pick up\n")) @@ -56,16 +60,32 @@ def _build_tip_handling_configurations( distance = float( input("please provide the starting distance for pick up tip\n") ) - elif tip_handling_type == "drop" and model_configurations: + print(f"TODO: Current {current} is not used yet") + return PickUpTipConfigurations( + pressFit=PressFitPickUpTipConfiguration( + speed=speed, + presses=presses, + increment=increment, + distance=distance, + currentByTipCount={}, + ) + ) + + +def _build_drop_tip_data( + model_configurations: Optional[PipetteModelSpec] = None, +) -> DropTipConfigurations: + if model_configurations: + current = model_configurations["dropTipCurrent"]["value"] speed = model_configurations["dropTipSpeed"]["value"] - elif tip_handling_type == "drop": + else: print("Handling drop tip configurations\n") speed = float(input("please provide the speed\n")) - return TipHandlingConfigurations( - speed=speed, - presses=presses, - increment=increment, - distance=distance, + return DropTipConfigurations( + plungerEject=PlungerEjectDropTipConfiguration( + current=current, + speed=speed, + ) ) @@ -106,18 +126,14 @@ def _build_motor_configurations( def _build_partial_tip_configurations(channels: int) -> PartialTipDefinition: if channels == 8: return PartialTipDefinition( - partialTipSupported=True, - availableConfigurations=[1, 2, 3, 4, 5, 6, 7, 8], - perTipPickupCurrent={}, + partialTipSupported=True, availableConfigurations=[1, 2, 3, 4, 5, 6, 7, 8] ) elif channels == 96: return PartialTipDefinition( - partialTipSupported=True, - availableConfigurations=[1, 8, 12, 96], - perTipPickupCurrent={}, + partialTipSupported=True, availableConfigurations=[1, 8, 12, 96] ) else: - return PartialTipDefinition(partialTipSupported=False, perTipPickupCurrent={}) + return PartialTipDefinition(partialTipSupported=False) def build_geometry_model_v2( @@ -185,8 +201,8 @@ def build_physical_model_v2( shaft_ul_per_mm = float( input(f"Please provide the uL to mm conversion for {pipette_type}\n") ) - pick_up_tip_configurations = _build_tip_handling_configurations("pickup") - drop_tip_configurations = _build_tip_handling_configurations("drop") + pick_up_tip_configurations = _build_pickup_tip_data() + drop_tip_configurations = _build_drop_tip_data() plunger_positions = _build_plunger_positions() plunger_motor_configurations = _build_motor_configurations() partial_tip_configurations = _build_partial_tip_configurations(int(channels)) diff --git a/shared-data/python/tests/pipette/test_load_data.py b/shared-data/python/tests/pipette/test_load_data.py index 84a6344ad1b..1b9e9775c16 100644 --- a/shared-data/python/tests/pipette/test_load_data.py +++ b/shared-data/python/tests/pipette/test_load_data.py @@ -85,6 +85,7 @@ def test_update_pipette_configuration( base_configurations = load_data.load_definition( model_name.pipette_type, model_name.pipette_channels, model_name.pipette_version ) + updated_configurations = load_data.update_pipette_configuration( base_configurations, v1_configuration_changes, liquid_class ) diff --git a/shared-data/python/tests/pipette/test_mutable_configurations.py b/shared-data/python/tests/pipette/test_mutable_configurations.py index e70520fb05f..3aabfd40434 100644 --- a/shared-data/python/tests/pipette/test_mutable_configurations.py +++ b/shared-data/python/tests/pipette/test_mutable_configurations.py @@ -241,7 +241,7 @@ def test_load_with_overrides( if serial_number == TEST_SERIAL_NUMBER: dict_loaded_configs = loaded_base_configurations.dict(by_alias=True) - dict_loaded_configs["pickUpTipConfigurations"]["speed"] = 5.0 + dict_loaded_configs["pickUpTipConfigurations"]["pressFit"]["speed"] = 5.0 updated_configurations_dict = updated_configurations.dict(by_alias=True) assert set(dict_loaded_configs.pop("quirks")) == set( updated_configurations_dict.pop("quirks") From c1aa067150792aacccf89d76d712097957793f8a Mon Sep 17 00:00:00 2001 From: Brent Hagen Date: Wed, 15 Nov 2023 17:56:28 -0500 Subject: [PATCH 26/46] refactor(components): add waste chute staging area to base deck (#13994) adds the waste chute staging area fixture component to BaseDeck, with some styling tweaks. updates positioning of FlexTrash. re RAUT-861 --- .../MoveLabwareInterventionContent.tsx | 2 +- .../src/hardware-sim/BaseDeck/BaseDeck.tsx | 61 +++++++++++-------- .../BaseDeck/WasteChuteFixture.tsx | 29 ++++++--- .../src/hardware-sim/Deck/FlexTrash.tsx | 30 +++++---- .../hardware-sim/Deck/MoveLabwareOnDeck.tsx | 8 +-- .../src/hardware-sim/Deck/RobotWorkSpace.tsx | 8 +-- .../src/hardware-sim/Deck/StyledDeck.tsx | 14 ++--- .../src/components/DeckSetup/index.tsx | 4 +- shared-data/js/constants.ts | 10 +++ 9 files changed, 100 insertions(+), 66 deletions(-) diff --git a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx index 19659b7a537..db4b5b883ee 100644 --- a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx +++ b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx @@ -197,7 +197,7 @@ export function MoveLabwareInterventionContent({ loadedModules={run.modules} loadedLabware={run.labware} // TODO(bh, 2023-07-19): remove when StyledDeck removed from MoveLabwareOnDeck - trashLocation={ + trashCutoutId={ robotType === 'OT-3 Standard' ? 'cutoutA3' : undefined } backgroundItems={ diff --git a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx index ac565632877..30a2d8d37ac 100644 --- a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx +++ b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx @@ -10,7 +10,8 @@ import { STAGING_AREA_RIGHT_SLOT_FIXTURE, TRASH_BIN_ADAPTER_FIXTURE, WASTE_CHUTE_CUTOUT, - WASTE_CHUTE_FIXTURES, + WASTE_CHUTE_ONLY_FIXTURES, + WASTE_CHUTE_STAGING_AREA_FIXTURES, } from '@opentrons/shared-data' import { RobotCoordinateSpace } from '../RobotCoordinateSpace' @@ -21,14 +22,10 @@ import { DeckFromLayers } from '../Deck/DeckFromLayers' import { SlotLabels } from '../Deck' import { COLORS } from '../../ui-style-constants' -import { - // EXTENDED_DECK_CONFIG_FIXTURE, - STANDARD_SLOT_DECK_CONFIG_FIXTURE, -} from './__fixtures__' import { SingleSlotFixture } from './SingleSlotFixture' import { StagingAreaFixture } from './StagingAreaFixture' import { WasteChuteFixture } from './WasteChuteFixture' -// import { WasteChuteStagingAreaFixture } from './WasteChuteStagingAreaFixture' +import { WasteChuteStagingAreaFixture } from './WasteChuteStagingAreaFixture' import type { DeckConfiguration, @@ -37,14 +34,13 @@ import type { ModuleLocation, ModuleModel, RobotType, - SingleSlotCutoutFixtureId, - WasteChuteCutoutFixtureId, } from '@opentrons/shared-data' -import type { TrashLocation } from '../Deck/FlexTrash' +import type { TrashCutoutId } from '../Deck/FlexTrash' import type { StagingAreaLocation } from './StagingAreaFixture' import type { WellFill } from '../Labware' interface BaseDeckProps { + deckConfig: DeckConfiguration robotType: RobotType labwareLocations?: Array<{ labwareLocation: LabwareLocation @@ -64,7 +60,6 @@ interface BaseDeckProps { moduleChildren?: React.ReactNode onLabwareClick?: () => void }> - deckConfig?: DeckConfiguration deckLayerBlocklist?: string[] showExpansion?: boolean lightFill?: string @@ -82,9 +77,7 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { lightFill = COLORS.light1, darkFill = COLORS.darkGreyEnabled, deckLayerBlocklist = [], - // TODO(bh, 2023-10-09): remove deck config fixture for Flex after migration to v4 - // deckConfig = EXTENDED_DECK_CONFIG_FIXTURE, - deckConfig = STANDARD_SLOT_DECK_CONFIG_FIXTURE, + deckConfig, showExpansion = true, children, showSlotLabels = true, @@ -92,10 +85,10 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { } = props const deckDef = getDeckDefFromRobotType(robotType) - const singleSlotFixtures = deckConfig.filter(fixture => - SINGLE_SLOT_FIXTURES.includes( - fixture.cutoutFixtureId as SingleSlotCutoutFixtureId - ) + const singleSlotFixtures = deckConfig.filter( + fixture => + fixture.cutoutFixtureId != null && + SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) ) const stagingAreaFixtures = deckConfig.filter( fixture => fixture.cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE @@ -103,11 +96,17 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { const trashBinFixtures = deckConfig.filter( fixture => fixture.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE ) - const wasteChuteFixtures = deckConfig.filter( + const wasteChuteOnlyFixtures = deckConfig.filter( + fixture => + fixture.cutoutFixtureId != null && + WASTE_CHUTE_ONLY_FIXTURES.includes(fixture.cutoutFixtureId) && + fixture.cutoutId === WASTE_CHUTE_CUTOUT + ) + const wasteChuteStagingAreaFixtures = deckConfig.filter( fixture => - WASTE_CHUTE_FIXTURES.includes( - fixture.cutoutFixtureId as WasteChuteCutoutFixtureId - ) && fixture.cutoutId === WASTE_CHUTE_CUTOUT + fixture.cutoutFixtureId != null && + WASTE_CHUTE_STAGING_AREA_FIXTURES.includes(fixture.cutoutFixtureId) && + fixture.cutoutId === WASTE_CHUTE_CUTOUT ) return ( @@ -121,6 +120,9 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { /> ) : ( <> + {showSlotLabels ? ( + + ) : null} {singleSlotFixtures.map(fixture => ( ))} - {wasteChuteFixtures.map(fixture => ( + {wasteChuteOnlyFixtures.map(fixture => ( ))} + {wasteChuteStagingAreaFixtures.map(fixture => ( + + ))} )} <> @@ -248,9 +260,6 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { ) : null } )} - {showSlotLabels ? ( - - ) : null} {children} diff --git a/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx b/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx index b698dfdbd54..9f562731b72 100644 --- a/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx +++ b/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx @@ -4,8 +4,13 @@ import { WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data' import { Icon } from '../../icons' import { Flex, Text } from '../../primitives' -import { ALIGN_CENTER, DIRECTION_COLUMN, JUSTIFY_CENTER } from '../../styles' -import { BORDERS, COLORS, TYPOGRAPHY } from '../../ui-style-constants' +import { + ALIGN_CENTER, + DIRECTION_COLUMN, + JUSTIFY_CENTER, + TEXT_ALIGN_CENTER, +} from '../../styles' +import { COLORS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' import { SlotBase } from './SlotBase' @@ -69,32 +74,38 @@ interface WasteChuteProps { /** * a deck map foreign object representing the physical location of the waste chute connected to the deck - * based on preliminary designs - * TODO(bh, 2023-10-11): when designs and definitions settled, resolve position details etc */ export function WasteChute(props: WasteChuteProps): JSX.Element { const { wasteIconColor, backgroundColor } = props return ( - Waste chute + + Waste chute + ) diff --git a/components/src/hardware-sim/Deck/FlexTrash.tsx b/components/src/hardware-sim/Deck/FlexTrash.tsx index 6fcd3b0a057..e114e6cb443 100644 --- a/components/src/hardware-sim/Deck/FlexTrash.tsx +++ b/components/src/hardware-sim/Deck/FlexTrash.tsx @@ -16,7 +16,7 @@ import trashDef from '@opentrons/shared-data/labware/definitions/2/opentrons_1_t import type { RobotType } from '@opentrons/shared-data' // only allow edge cutout locations (columns 1 and 3) -export type TrashLocation = +export type TrashCutoutId = | 'cutoutA1' | 'cutoutB1' | 'cutoutC1' @@ -30,7 +30,7 @@ interface FlexTrashProps { robotType: RobotType trashIconColor: string backgroundColor: string - trashLocation?: TrashLocation + trashCutoutId?: TrashCutoutId } /** @@ -41,21 +41,25 @@ export const FlexTrash = ({ robotType, trashIconColor, backgroundColor, - trashLocation, + trashCutoutId, }: FlexTrashProps): JSX.Element | null => { // be sure we don't try to render for an OT-2 if (robotType !== FLEX_ROBOT_TYPE) return null const deckDefinition = getDeckDefFromRobotType(robotType) - const trashSlot = deckDefinition.locations.cutouts.find( - slot => slot.id === trashLocation + const trashCutout = deckDefinition.locations.cutouts.find( + cutout => cutout.id === trashCutoutId ) - // retrieve slot x,y positions and dimensions from deck definition for the given trash slot - // TODO(bh, 2023-10-09): refactor position, offsets, and rotation after v4 migration - const [x = 0, y = 0] = trashSlot?.position ?? [] - const [slotXDimension = 0, slotYDimension = 0] = trashSlot?.position ?? [] + // retrieve slot x,y positions and dimensions from deck definition for the given trash cutout location + const [x = 0, y = 0] = trashCutout?.position ?? [] + + // a standard addressable area slot bounding box dimension + const { + xDimension: slotXDimension = 0, + yDimension: slotYDimension = 0, + } = deckDefinition.locations.addressableAreas[0].boundingBox // adjust for dimensions from trash definition const { x: xAdjustment, y: yAdjustment } = trashDef.cornerOffsetFromSlot @@ -63,10 +67,10 @@ export const FlexTrash = ({ // rotate trash 180 degrees in column 1 const rotateDegrees = - trashLocation === 'cutoutA1' || - trashLocation === 'cutoutB1' || - trashLocation === 'cutoutC1' || - trashLocation === 'cutoutD1' + trashCutoutId === 'cutoutA1' || + trashCutoutId === 'cutoutB1' || + trashCutoutId === 'cutoutC1' || + trashCutoutId === 'cutoutD1' ? '180' : '0' diff --git a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx index b1926a0467e..11af7d2ba65 100644 --- a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx +++ b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx @@ -23,7 +23,7 @@ import type { DeckDefinition, } from '@opentrons/shared-data' import type { StyleProps } from '../../primitives' -import type { TrashLocation } from './FlexTrash' +import type { TrashCutoutId } from './FlexTrash' const getModulePosition = ( deckDef: DeckDefinition, @@ -139,7 +139,7 @@ interface MoveLabwareOnDeckProps extends StyleProps { loadedLabware: LoadedLabware[] backgroundItems?: React.ReactNode deckFill?: string - trashLocation?: TrashLocation + trashCutoutId?: TrashCutoutId } export function MoveLabwareOnDeck( props: MoveLabwareOnDeckProps @@ -153,7 +153,7 @@ export function MoveLabwareOnDeck( loadedModules, backgroundItems = null, deckFill = '#e6e6e6', - trashLocation, + trashCutoutId, ...styleProps } = props const deckDef = React.useMemo(() => getDeckDefFromRobotType(robotType), [ @@ -233,7 +233,7 @@ export function MoveLabwareOnDeck( deckFill={deckFill} layerBlocklist={[]} robotType={robotType} - trashLocation={trashLocation} + trashCutoutId={trashCutoutId} /> )} {backgroundItems} diff --git a/components/src/hardware-sim/Deck/RobotWorkSpace.tsx b/components/src/hardware-sim/Deck/RobotWorkSpace.tsx index cf3e24e578d..3678d1acfea 100644 --- a/components/src/hardware-sim/Deck/RobotWorkSpace.tsx +++ b/components/src/hardware-sim/Deck/RobotWorkSpace.tsx @@ -4,7 +4,7 @@ import { StyleProps, Svg } from '../../primitives' import { StyledDeck } from './StyledDeck' import type { DeckDefinition, DeckSlot } from '@opentrons/shared-data' -import type { TrashLocation } from './FlexTrash' +import type { TrashCutoutId } from './FlexTrash' export interface RobotWorkSpaceRenderProps { deckSlotsById: { [slotId: string]: DeckSlot } @@ -23,7 +23,7 @@ export interface RobotWorkSpaceProps extends StyleProps { // optional boolean to show the OT-2 deck from deck defintion layers showDeckLayers?: boolean // TODO(bh, 2023-10-09): remove - trashSlotName?: TrashLocation + trashCutoutId?: TrashCutoutId trashColor?: string id?: string } @@ -37,7 +37,7 @@ export function RobotWorkSpace(props: RobotWorkSpaceProps): JSX.Element | null { deckFill = '#CCCCCC', deckLayerBlocklist = [], showDeckLayers = false, - trashSlotName, + trashCutoutId, viewBox, trashColor, id, @@ -89,7 +89,7 @@ export function RobotWorkSpace(props: RobotWorkSpaceProps): JSX.Element | null { deckFill={deckFill} layerBlocklist={deckLayerBlocklist} robotType={OT2_ROBOT_TYPE} - trashLocation={trashSlotName} + trashCutoutId={trashCutoutId} trashColor={trashColor} /> ) : null} diff --git a/components/src/hardware-sim/Deck/StyledDeck.tsx b/components/src/hardware-sim/Deck/StyledDeck.tsx index de990cebe3a..7ac0130fbc0 100644 --- a/components/src/hardware-sim/Deck/StyledDeck.tsx +++ b/components/src/hardware-sim/Deck/StyledDeck.tsx @@ -6,13 +6,13 @@ import { FlexTrash } from './FlexTrash' import type { RobotType } from '@opentrons/shared-data' import type { DeckFromLayersProps } from './DeckFromLayers' -import type { TrashLocation } from './FlexTrash' +import type { TrashCutoutId } from './FlexTrash' interface StyledDeckProps { deckFill: string robotType: RobotType trashColor?: string - trashLocation?: TrashLocation + trashCutoutId?: TrashCutoutId } // apply fill to .SLOT_BASE class from ot3_standard deck definition @@ -28,12 +28,12 @@ export function StyledDeck( const { deckFill, robotType, - trashLocation, + trashCutoutId, trashColor = '#757070', ...DeckFromLayersProps } = props const trashSlotClipId = - trashLocation != null ? `SLOT_CLIPS_${trashLocation}` : null + trashCutoutId != null ? `SLOT_CLIPS_${trashCutoutId}` : null const trashLayerBlocklist = trashSlotClipId != null @@ -47,13 +47,13 @@ export function StyledDeck( layerBlocklist={trashLayerBlocklist} robotType={robotType} /> - {/* TODO(bh, 2023-11-06): remove trash and trashLocation prop when StyledDeck removed from MoveLabwareOnDeck */} - {trashLocation != null ? ( + {/* TODO(bh, 2023-11-06): remove trash and trashCutoutId prop when StyledDeck removed from MoveLabwareOnDeck */} + {trashCutoutId != null ? ( ) : null} diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx index 3db220a1858..e1b76301fdf 100644 --- a/protocol-designer/src/components/DeckSetup/index.tsx +++ b/protocol-designer/src/components/DeckSetup/index.tsx @@ -12,7 +12,7 @@ import { SingleSlotFixture, StagingAreaFixture, StagingAreaLocation, - TrashLocation, + TrashCutoutId, useOnClickOutside, WasteChuteFixture, } from '@opentrons/components' @@ -610,7 +610,7 @@ export const DeckSetup = (): JSX.Element => { diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index 368f23c0a59..c944c7943dc 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -324,3 +324,13 @@ export const WASTE_CHUTE_FIXTURES: CutoutFixtureId[] = [ STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, ] + +export const WASTE_CHUTE_ONLY_FIXTURES: CutoutFixtureId[] = [ + WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, +] + +export const WASTE_CHUTE_STAGING_AREA_FIXTURES: CutoutFixtureId[] = [ + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, +] From 8cc5df9307102f3ab6f64e5ede6a0a93e9326285 Mon Sep 17 00:00:00 2001 From: Brian Arthur Cooper Date: Wed, 15 Nov 2023 18:12:43 -0500 Subject: [PATCH 27/46] refactor(app): read robotType from protocol analysis with fallbacks (#13992) Remove all instances of the utility function that gleans the robotType from a list of loaded labware based on soon to be deprecated fixed trash labware loadnames. --- .../__tests__/DeckThumbnail.test.tsx | 18 ++---- app/src/molecules/DeckThumbnail/index.tsx | 5 +- .../ProtocolRun/ProtocolRunModuleControls.tsx | 1 - .../Devices/ProtocolRun/ProtocolRunSetup.tsx | 29 +++++---- .../SetupLabware/SetupLabwareMap.tsx | 3 +- .../__tests__/SetupLabwareMap.test.tsx | 4 +- .../ProtocolRun/SetupLabware/index.tsx | 5 +- .../SetupLiquids/SetupLiquidsMap.tsx | 4 +- .../__tests__/SetupLiquidsMap.test.tsx | 47 ++++++++------ .../SetupModuleAndDeck/SetupModulesList.tsx | 1 - .../SetupModuleAndDeck/SetupModulesMap.tsx | 4 +- .../__tests__/SetupModulesList.test.tsx | 4 +- .../__tests__/SetupModulesMap.test.tsx | 2 + .../ProtocolRunModuleControls.test.tsx | 11 ++-- .../ProtocolRun/utils/getLabwareRenderInfo.ts | 13 ++-- .../useModuleCalibrationStatus.test.tsx | 8 +-- ...seModuleRenderInfoForProtocolById.test.tsx | 19 ++---- .../useProtocolDetailsForRun.test.tsx | 8 ++- .../useUnmatchedModulesForProtocol.test.tsx | 10 +-- .../hooks/useModuleCalibrationStatus.ts | 2 +- .../useModuleRenderInfoForProtocolById.ts | 14 ++-- .../Devices/hooks/useProtocolDetailsForRun.ts | 10 +-- .../hooks/useUnmatchedModulesForProtocol.ts | 5 +- .../MoveLabwareInterventionContent.tsx | 4 +- .../__tests__/InterventionModal.test.tsx | 6 ++ app/src/organisms/InterventionModal/index.tsx | 4 +- .../useMostRecentCompletedAnalysis.ts | 8 ++- .../ModuleCard/__tests__/hooks.test.tsx | 64 +++++++------------ app/src/organisms/ModuleCard/hooks.tsx | 13 +--- .../Devices/ProtocolRunDetails/index.tsx | 1 - .../deck/definitions/4/ot3_standard.json | 4 ++ .../getRobotTypeFromLoadedLabware.test.ts | 31 --------- shared-data/js/helpers/index.ts | 14 +--- shared-data/js/types.ts | 1 + 34 files changed, 155 insertions(+), 222 deletions(-) delete mode 100644 shared-data/js/helpers/__tests__/getRobotTypeFromLoadedLabware.test.ts diff --git a/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx b/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx index 16b2555fda2..027160db13f 100644 --- a/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx +++ b/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx @@ -3,7 +3,6 @@ import { when, resetAllWhenMocks } from 'jest-when' import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, OT2_ROBOT_TYPE, } from '@opentrons/shared-data' import ot2StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot2_standard.json' @@ -33,7 +32,6 @@ import { DeckThumbnail } from '../' import type { LabwareDefinition2, - LoadedLabware, ModuleModel, ModuleType, RunTimeCommand, @@ -49,10 +47,6 @@ jest.mock('../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo') jest.mock('../../../organisms/Devices/hooks') jest.mock('../../../organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo') -const mockGetRobotTypeFromLoadedLabware = getRobotTypeFromLoadedLabware as jest.MockedFunction< - typeof getRobotTypeFromLoadedLabware -> - const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction< typeof getDeckDefFromRobotType > @@ -79,9 +73,11 @@ const mockGetAttachedProtocolModuleMatches = getAttachedProtocolModuleMatches as > const mockBaseDeck = BaseDeck as jest.MockedFunction -const protocolAnalysis = simpleAnalysisFileFixture as any +const protocolAnalysis = { + ...simpleAnalysisFileFixture, + robotType: OT2_ROBOT_TYPE, +} as any const commands: RunTimeCommand[] = simpleAnalysisFileFixture.commands as any -const labware: LoadedLabware[] = simpleAnalysisFileFixture.labware as any const MOCK_300_UL_TIPRACK_ID = '300_ul_tiprack_id' const MOCK_MAGNETIC_MODULE_COORDS = [10, 20, 0] const MOCK_SECOND_MAGNETIC_MODULE_COORDS = [100, 200, 0] @@ -111,9 +107,6 @@ const render = (props: React.ComponentProps) => { describe('DeckThumbnail', () => { beforeEach(() => { - when(mockGetRobotTypeFromLoadedLabware) - .calledWith(labware) - .mockReturnValue(OT2_ROBOT_TYPE) when(mockGetDeckDefFromRobotType) .calledWith(OT2_ROBOT_TYPE) .mockReturnValue(ot2StandardDeckDef as any) @@ -228,9 +221,6 @@ describe('DeckThumbnail', () => { // nestedLabwareDef: null, // }, // ] - when(mockGetRobotTypeFromLoadedLabware) - .calledWith(labware) - .mockReturnValue(FLEX_ROBOT_TYPE) when(mockGetDeckDefFromRobotType) .calledWith(FLEX_ROBOT_TYPE) .mockReturnValue(ot3StandardDeckDef as any) diff --git a/app/src/molecules/DeckThumbnail/index.tsx b/app/src/molecules/DeckThumbnail/index.tsx index eefe2a3c1c6..9b143d81565 100644 --- a/app/src/molecules/DeckThumbnail/index.tsx +++ b/app/src/molecules/DeckThumbnail/index.tsx @@ -3,8 +3,8 @@ import map from 'lodash/map' import { BaseDeck } from '@opentrons/components' import { + FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, THERMOCYCLER_MODULE_V1, } from '@opentrons/shared-data' import { @@ -42,8 +42,7 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element | null { const attachedModules = useAttachedModules() if (protocolAnalysis == null || protocolAnalysis.errors.length) return null - - const robotType = getRobotTypeFromLoadedLabware(protocolAnalysis.labware) + const robotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE const deckDef = getDeckDefFromRobotType(robotType) const initialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter( protocolAnalysis.commands diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx index 8e2d67fd238..f8dcee3d93b 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx @@ -70,7 +70,6 @@ export const ProtocolRunModuleControls = ({ const { attachPipetteRequired, updatePipetteFWRequired } = usePipetteIsReady() const moduleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById( - robotName, runId ) const attachedModules = Object.values(moduleRenderInfoForProtocolById).filter( diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx index aa45ca4191d..34241b35f04 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -66,8 +66,8 @@ export function ProtocolRunSetup({ const { t, i18n } = useTranslation('protocol_setup') const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) - const protocolData = robotProtocolAnalysis ?? storedProtocolAnalysis - const modules = parseAllRequiredModuleModels(protocolData?.commands ?? []) + const protocolAnalysis = robotProtocolAnalysis ?? storedProtocolAnalysis + const modules = parseAllRequiredModuleModels(protocolAnalysis?.commands ?? []) const robot = useRobot(robotName) const calibrationStatusRobot = useRunCalibrationStatus(robotName, runId) @@ -80,7 +80,7 @@ export function ProtocolRunSetup({ ) const stepsKeysInOrder = - protocolData != null + protocolAnalysis != null ? [ ROBOT_CALIBRATION_STEP_KEY, MODULE_SETUP_KEY, @@ -91,33 +91,34 @@ export function ProtocolRunSetup({ : [ROBOT_CALIBRATION_STEP_KEY, LPC_KEY, LABWARE_SETUP_KEY] const targetStepKeyInOrder = stepsKeysInOrder.filter((stepKey: StepKey) => { - if (protocolData == null) { + if (protocolAnalysis == null) { return stepKey !== MODULE_SETUP_KEY && stepKey !== LIQUID_SETUP_KEY } if ( - protocolData.modules.length === 0 && - protocolData.liquids.length === 0 + protocolAnalysis.modules.length === 0 && + protocolAnalysis.liquids.length === 0 ) { return stepKey !== MODULE_SETUP_KEY && stepKey !== LIQUID_SETUP_KEY } - if (protocolData.modules.length === 0) { + if (protocolAnalysis.modules.length === 0) { return stepKey !== MODULE_SETUP_KEY } - if (protocolData.liquids.length === 0) { + if (protocolAnalysis.liquids.length === 0) { return stepKey !== LIQUID_SETUP_KEY } return true }) if (robot == null) return null - const hasLiquids = protocolData != null && protocolData.liquids?.length > 0 - const hasModules = protocolData != null && modules.length > 0 + const hasLiquids = + protocolAnalysis != null && protocolAnalysis.liquids?.length > 0 + const hasModules = protocolAnalysis != null && modules.length > 0 const protocolDeckConfig = getSimplestDeckConfigForProtocolCommands( - protocolData?.commands ?? [] + protocolAnalysis?.commands ?? [] ) const hasFixtures = protocolDeckConfig.length > 0 @@ -165,7 +166,7 @@ export function ProtocolRunSetup({ robotName={robotName} runId={runId} hasModules={hasModules} - commands={protocolData?.commands ?? []} + commands={protocolAnalysis?.commands ?? []} /> ), description: moduleDescription, @@ -202,7 +203,7 @@ export function ProtocolRunSetup({ protocolRunHeaderRef={protocolRunHeaderRef} robotName={robotName} runId={runId} - protocolAnalysis={protocolData} + protocolAnalysis={protocolAnalysis} /> ), description: hasLiquids @@ -217,7 +218,7 @@ export function ProtocolRunSetup({ gridGap={SPACING.spacing16} margin={SPACING.spacing16} > - {protocolData != null ? ( + {protocolAnalysis != null ? ( <> {runHasStarted ? ( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx index cdaaddce611..528b293a390 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx @@ -12,7 +12,6 @@ import { import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, THERMOCYCLER_MODULE_V1, } from '@opentrons/shared-data' @@ -52,7 +51,7 @@ export function SetupLabwareMap({ const commands = protocolAnalysis.commands - const robotType = getRobotTypeFromLoadedLabware(protocolAnalysis.labware) + const robotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE const deckDef = getDeckDefFromRobotType(robotType) const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx index 02fae05cf58..e6fabcac8ad 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx @@ -7,7 +7,7 @@ import { LabwareRender, Module, } from '@opentrons/components' -import { getModuleDef2 } from '@opentrons/shared-data' +import { OT2_ROBOT_TYPE, getModuleDef2 } from '@opentrons/shared-data' import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' import { i18n } from '../../../../../i18n' @@ -159,6 +159,7 @@ describe('SetupLabwareMap', () => { protocolAnalysis: ({ commands: [], labware: [], + robotType: OT2_ROBOT_TYPE, } as unknown) as CompletedProtocolAnalysis, }) @@ -236,6 +237,7 @@ describe('SetupLabwareMap', () => { protocolAnalysis: ({ commands: [], labware: [], + robotType: OT2_ROBOT_TYPE, } as unknown) as CompletedProtocolAnalysis, }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/index.tsx index 6ee3657de84..82dba6c6cc6 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/index.tsx @@ -42,10 +42,7 @@ export function SetupLabware(props: SetupLabwareProps): JSX.Element { ) const isFlex = useIsFlex(robotName) - const moduleRenderInfoById = useModuleRenderInfoForProtocolById( - robotName, - runId - ) + const moduleRenderInfoById = useModuleRenderInfoForProtocolById(runId) const moduleModels = map( moduleRenderInfoById, ({ moduleDef }) => moduleDef.model diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx index 0c6a4ac80b4..2bb6c16f327 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx @@ -15,8 +15,8 @@ import { LabwareRender, } from '@opentrons/components' import { + FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, THERMOCYCLER_MODULE_V1, } from '@opentrons/shared-data' @@ -64,7 +64,7 @@ export function SetupLiquidsMap( const initialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter( protocolAnalysis.commands ?? [] ) - const robotType = getRobotTypeFromLoadedLabware(protocolAnalysis.labware) + const robotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE const deckDef = getDeckDefFromRobotType(robotType) const labwareRenderInfo = getLabwareRenderInfo(protocolAnalysis, deckDef) const labwareByLiquidId = parseLabwareInfoByLiquidId( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx index e4786c8d522..8d17c44f7b1 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx @@ -12,7 +12,6 @@ import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fi import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, OT2_ROBOT_TYPE, } from '@opentrons/shared-data' import { @@ -75,9 +74,6 @@ const mockBaseDeck = BaseDeck as jest.MockedFunction const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction< typeof getDeckDefFromRobotType > -const mockGetRobotTypeFromLoadedLabware = getRobotTypeFromLoadedLabware as jest.MockedFunction< - typeof getRobotTypeFromLoadedLabware -> const mockParseInitialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter as jest.MockedFunction< typeof parseInitialLoadedLabwareByAdapter > @@ -131,13 +127,17 @@ const render = (props: React.ComponentProps) => { i18nInstance: i18n, }) } +const mockProtocolAnalysis = { + ...simpleAnalysisFileFixture, + robotType: OT2_ROBOT_TYPE, +} as any describe('SetupLiquidsMap', () => { let props: React.ComponentProps beforeEach(() => { props = { runId: RUN_ID, - protocolAnalysis: simpleAnalysisFileFixture as any, + protocolAnalysis: mockProtocolAnalysis, } when(mockLabwareRender) .mockReturnValue(

    ) // this (default) empty div will be returned when LabwareRender isn't called with expected labware definition @@ -161,23 +161,20 @@ describe('SetupLiquidsMap', () => { when(mockUseAttachedModules).calledWith().mockReturnValue([]) when(mockGetAttachedProtocolModuleMatches).mockReturnValue([]) when(mockGetLabwareRenderInfo) - .calledWith(simpleAnalysisFileFixture as any, ot2StandardDeckDef as any) + .calledWith(mockProtocolAnalysis, ot2StandardDeckDef as any) .mockReturnValue({}) when(mockGetSimplestDeckConfigForProtocolCommands) - .calledWith(simpleAnalysisFileFixture.commands as RunTimeCommand[]) + .calledWith(mockProtocolAnalysis.commands as RunTimeCommand[]) // TODO(bh, 2023-11-13): mock the cutout config protocol spec .mockReturnValue([]) - when(mockGetRobotTypeFromLoadedLabware) - .calledWith(simpleAnalysisFileFixture.labware as any) - .mockReturnValue(FLEX_ROBOT_TYPE) when(mockParseLiquidsInLoadOrder) .calledWith( - simpleAnalysisFileFixture.liquids as any, - simpleAnalysisFileFixture.commands as any + mockProtocolAnalysis.liquids as any, + mockProtocolAnalysis.commands as any ) .mockReturnValue([]) when(mockParseInitialLoadedLabwareByAdapter) - .calledWith(simpleAnalysisFileFixture.commands as any) + .calledWith(mockProtocolAnalysis.commands as any) .mockReturnValue({}) when(mockLabwareInfoOverlay) .mockReturnValue(
    ) // this (default) empty div will be returned when LabwareInfoOverlay isn't called with expected props @@ -208,21 +205,18 @@ describe('SetupLiquidsMap', () => { }) it('should render base deck - robot type is OT-2', () => { - when(mockGetRobotTypeFromLoadedLabware) - .calledWith(simpleAnalysisFileFixture.labware as any) - .mockReturnValue(OT2_ROBOT_TYPE) when(mockGetDeckDefFromRobotType) .calledWith(OT2_ROBOT_TYPE) .mockReturnValue(ot2StandardDeckDef as any) when(mockParseLabwareInfoByLiquidId) - .calledWith(simpleAnalysisFileFixture.commands as any) + .calledWith(mockProtocolAnalysis.commands as any) .mockReturnValue({}) mockUseAttachedModules.mockReturnValue( mockFetchModulesSuccessActionPayloadModules ) when(mockGetLabwareRenderInfo).mockReturnValue({}) when(mockGetProtocolModulesInfo) - .calledWith(simpleAnalysisFileFixture as any, ot2StandardDeckDef as any) + .calledWith(mockProtocolAnalysis, ot2StandardDeckDef as any) .mockReturnValue(mockProtocolModuleInfo) when(mockGetAttachedProtocolModuleMatches) .calledWith( @@ -271,12 +265,23 @@ describe('SetupLiquidsMap', () => { }) it('should render base deck - robot type is Flex', () => { + const mockFlexAnalysis = { + ...mockProtocolAnalysis, + robotType: FLEX_ROBOT_TYPE, + } + props = { + ...props, + protocolAnalysis: { + ...mockProtocolAnalysis, + robotType: FLEX_ROBOT_TYPE, + }, + } when(mockGetDeckDefFromRobotType) .calledWith(FLEX_ROBOT_TYPE) .mockReturnValue(ot3StandardDeckDef as any) when(mockGetLabwareRenderInfo) - .calledWith(simpleAnalysisFileFixture as any, ot3StandardDeckDef as any) + .calledWith(mockFlexAnalysis, ot3StandardDeckDef as any) .mockReturnValue({ [MOCK_300_UL_TIPRACK_ID]: { labwareDef: fixture_tiprack_300_ul as LabwareDefinition2, @@ -289,14 +294,14 @@ describe('SetupLiquidsMap', () => { }) when(mockParseLabwareInfoByLiquidId) - .calledWith(simpleAnalysisFileFixture.commands as any) + .calledWith(mockFlexAnalysis.commands as any) .mockReturnValue({}) mockUseAttachedModules.mockReturnValue( mockFetchModulesSuccessActionPayloadModules ) when(mockGetProtocolModulesInfo) - .calledWith(simpleAnalysisFileFixture as any, ot3StandardDeckDef as any) + .calledWith(mockFlexAnalysis, ot3StandardDeckDef as any) .mockReturnValue(mockProtocolModuleInfo) when(mockGetAttachedProtocolModuleMatches) .calledWith( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx index df50b6bf721..07c943e8341 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx @@ -71,7 +71,6 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { const { robotName, runId } = props const { t } = useTranslation('protocol_setup') const moduleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById( - robotName, runId ) const { diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx index d441a602e2a..0a555112c8c 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx @@ -8,8 +8,8 @@ import { SPACING, } from '@opentrons/components' import { + FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, } from '@opentrons/shared-data' import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils' @@ -42,7 +42,7 @@ export const SetupModulesMap = ({ // early return null if no protocol analysis if (protocolAnalysis == null) return null - const robotType = getRobotTypeFromLoadedLabware(protocolAnalysis.labware) + const robotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE const deckDef = getDeckDefFromRobotType(robotType) const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx index efe07a3f6b0..7dc10b9d8ef 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx @@ -153,7 +153,7 @@ describe('SetupModulesList', () => { it('should render the list view headers', () => { when(mockUseRunHasStarted).calledWith(RUN_ID).mockReturnValue(false) when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({}) const { getByText } = render(props) getByText('Module') @@ -349,7 +349,7 @@ describe('SetupModulesList', () => { const dupModPort = 10 const dupModHub = 2 when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({ [mockMagneticModule.moduleId]: { moduleId: mockMagneticModule.moduleId, diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx index 429ba4d9145..f2e1e01e4a3 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import '@testing-library/jest-dom' import { when, resetAllWhenMocks } from 'jest-when' import { StaticRouter } from 'react-router-dom' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { renderWithProviders, @@ -113,6 +114,7 @@ describe('SetupModulesMap', () => { .mockReturnValue(({ commands: [], labware: [], + robotType: OT2_ROBOT_TYPE, } as unknown) as CompletedProtocolAnalysis) when(mockGetAttachedProtocolModuleMatches).mockReturnValue([]) }) diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx index b4d2be4862f..877ae596328 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx @@ -39,7 +39,6 @@ const mockUseInstrumentsQuery = useInstrumentsQuery as jest.MockedFunction< const _fixtureAnalysis = (fixtureAnalysis as unknown) as CompletedProtocolAnalysis -const ROBOT_NAME = 'otie' const RUN_ID = 'test123' const mockTempMod = { @@ -97,7 +96,7 @@ describe('ProtocolRunModuleControls', () => { it('renders a magnetic module card', () => { when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({ [mockMagMod.moduleId]: { moduleId: 'magModModuleId', @@ -122,7 +121,7 @@ describe('ProtocolRunModuleControls', () => { it('renders a temperature module card', () => { when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({ [mockTempMod.moduleId]: { moduleId: 'temperatureModuleId', @@ -149,7 +148,7 @@ describe('ProtocolRunModuleControls', () => { it('renders a thermocycler module card', () => { when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({ [mockTCModule.moduleId]: { moduleId: mockTCModule.moduleId, @@ -178,7 +177,7 @@ describe('ProtocolRunModuleControls', () => { it('renders a heater-shaker module card', () => { when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({ [mockHeaterShakerDef.moduleId]: { moduleId: 'heaterShakerModuleId', @@ -206,7 +205,7 @@ describe('ProtocolRunModuleControls', () => { it('renders correct text when module is not attached but required for protocol', () => { when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({ [mockHeaterShakerDef.moduleId]: { moduleId: 'heaterShakerModuleId', diff --git a/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts b/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts index 74bf968d144..5d750e6d7e8 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts @@ -1,7 +1,4 @@ -import { - getPositionFromSlotId, - getSlotHasMatingSurfaceUnitVector, -} from '@opentrons/shared-data' +import { getPositionFromSlotId } from '@opentrons/shared-data' import type { CompletedProtocolAnalysis, DeckDefinition, @@ -64,13 +61,11 @@ export const getLabwareRenderInfo = ( ) return acc } - - const slotHasMatingSurfaceVector = getSlotHasMatingSurfaceUnitVector( - deckDef, - slotName + const isSlot = deckDef.locations.addressableAreas.some( + aa => aa.id === slotName && aa.areaType === 'slot' ) - return slotHasMatingSurfaceVector + return isSlot ? { ...acc, [labwareId]: { diff --git a/app/src/organisms/Devices/hooks/__tests__/useModuleCalibrationStatus.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useModuleCalibrationStatus.test.tsx index d9915af41c6..56e0feec2e5 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useModuleCalibrationStatus.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useModuleCalibrationStatus.test.tsx @@ -86,7 +86,7 @@ describe('useModuleCalibrationStatus hook', () => { it('should return calibration complete if OT-2', () => { when(mockUseIsFlex).calledWith('otie').mockReturnValue(false) when(mockUseModuleRenderInfoForProtocolById) - .calledWith('otie', '1') + .calledWith('1') .mockReturnValue({}) const { result } = renderHook( @@ -100,7 +100,7 @@ describe('useModuleCalibrationStatus hook', () => { it('should return calibration complete if no modules needed', () => { when(mockUseIsFlex).calledWith('otie').mockReturnValue(true) when(mockUseModuleRenderInfoForProtocolById) - .calledWith('otie', '1') + .calledWith('1') .mockReturnValue({}) const { result } = renderHook( @@ -114,7 +114,7 @@ describe('useModuleCalibrationStatus hook', () => { it('should return calibration complete if offset date exists', () => { when(mockUseIsFlex).calledWith('otie').mockReturnValue(true) when(mockUseModuleRenderInfoForProtocolById) - .calledWith('otie', '1') + .calledWith('1') .mockReturnValue({ magneticModuleId: { attachedModuleMatch: { @@ -136,7 +136,7 @@ describe('useModuleCalibrationStatus hook', () => { it('should return calibration needed if offset date does not exist', () => { when(mockUseIsFlex).calledWith('otie').mockReturnValue(true) when(mockUseModuleRenderInfoForProtocolById) - .calledWith('otie', '1') + .calledWith('1') .mockReturnValue({ magneticModuleId: { attachedModuleMatch: { diff --git a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx index b1e92228117..575e349a5a8 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx @@ -126,7 +126,7 @@ const TEMPERATURE_MODULE_INFO = { slotName: 'D1', } -const mockFixture: CutoutConfig = { +const mockCutoutConfig: CutoutConfig = { cutoutId: 'cutoutD1', cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, } @@ -134,7 +134,7 @@ const mockFixture: CutoutConfig = { describe('useModuleRenderInfoForProtocolById hook', () => { beforeEach(() => { when(mockUseDeckConfigurationQuery).mockReturnValue({ - data: [mockFixture], + data: [mockCutoutConfig], } as UseQueryResult) when(mockUseAttachedModules) .calledWith() @@ -163,26 +163,19 @@ describe('useModuleRenderInfoForProtocolById hook', () => { .calledWith('1') .mockReturnValue(null) when(mockUseStoredProtocolAnalysis).calledWith('1').mockReturnValue(null) - const { result } = renderHook(() => - useModuleRenderInfoForProtocolById('otie', '1') - ) + const { result } = renderHook(() => useModuleRenderInfoForProtocolById('1')) expect(result.current).toStrictEqual({}) }) it('should return module render info', () => { - const { result } = renderHook(() => - useModuleRenderInfoForProtocolById('otie', '1') - ) + const { result } = renderHook(() => useModuleRenderInfoForProtocolById('1')) expect(result.current).toStrictEqual({ magneticModuleId: { - // TODO(bh, 2023-11-09): update this test once conflict logic has been updated to use getSimplestDeckConfigForProtocolCommands or similar - // conflictedFixture: mockFixture, - conflictedFixture: undefined, + conflictedFixture: mockCutoutConfig, attachedModuleMatch: mockMagneticModuleGen2, ...MAGNETIC_MODULE_INFO, }, temperatureModuleId: { - // conflictedFixture: mockFixture, - conflictedFixture: undefined, + conflictedFixture: mockCutoutConfig, attachedModuleMatch: mockTemperatureModuleGen2, ...TEMPERATURE_MODULE_INFO, }, diff --git a/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx index 2c94afeedf1..84329276960 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx @@ -13,7 +13,10 @@ import { useProtocolDetailsForRun } from '..' import { RUN_ID_2 } from '../../../../organisms/RunTimeControl/__fixtures__' import type { Protocol, Run } from '@opentrons/api-client' -import { CompletedProtocolAnalysis } from '@opentrons/shared-data' +import { + CompletedProtocolAnalysis, + OT2_ROBOT_TYPE, +} from '@opentrons/shared-data' jest.mock('@opentrons/react-api-client') @@ -39,6 +42,7 @@ const PROTOCOL_RESPONSE = { metadata: { protocolName: 'fake protocol' }, analysisSummaries: [{ id: PROTOCOL_ANALYSIS.id, status: 'completed' }], key: 'fakeProtocolKey', + robotType: OT2_ROBOT_TYPE, }, } as Protocol @@ -68,7 +72,7 @@ describe('useProtocolDetailsForRun hook', () => { protocolData: null, protocolKey: null, isProtocolAnalyzing: false, - robotType: 'OT-2 Standard', + robotType: 'OT-3 Standard', }) }) diff --git a/app/src/organisms/Devices/hooks/__tests__/useUnmatchedModulesForProtocol.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useUnmatchedModulesForProtocol.test.tsx index e524aa53d5a..971df5dc939 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useUnmatchedModulesForProtocol.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useUnmatchedModulesForProtocol.test.tsx @@ -51,7 +51,7 @@ describe('useModuleMatchResults', () => { .calledWith(mockConnectedRobot.name) .mockReturnValue(mockConnectedRobot) when(mockUseModuleRenderInfoForProtocolById) - .calledWith(mockConnectedRobot.name, '1') + .calledWith('1') .mockReturnValue({}) when(mockUseAttachedModules) @@ -67,7 +67,7 @@ describe('useModuleMatchResults', () => { when(mockUseAttachedModules).calledWith().mockReturnValue([]) const moduleId = 'fakeMagBlockId' when(mockUseModuleRenderInfoForProtocolById) - .calledWith(mockConnectedRobot.name, '1') + .calledWith('1') .mockReturnValue({ [moduleId]: { moduleId: moduleId, @@ -94,7 +94,7 @@ describe('useModuleMatchResults', () => { it('should return 1 missing moduleId if requested model not attached', () => { const moduleId = 'fakeMagModuleId' when(mockUseModuleRenderInfoForProtocolById) - .calledWith(mockConnectedRobot.name, '1') + .calledWith('1') .mockReturnValue({ [moduleId]: { moduleId: moduleId, @@ -121,7 +121,7 @@ describe('useModuleMatchResults', () => { it('should return no missing moduleId if compatible model is attached', () => { const moduleId = 'someTempModule' when(mockUseModuleRenderInfoForProtocolById) - .calledWith(mockConnectedRobot.name, '1') + .calledWith('1') .mockReturnValue({ [moduleId]: { moduleId: moduleId, @@ -147,7 +147,7 @@ describe('useModuleMatchResults', () => { it('should return one missing moduleId if nocompatible model is attached', () => { const moduleId = 'someTempModule' when(mockUseModuleRenderInfoForProtocolById) - .calledWith(mockConnectedRobot.name, '1') + .calledWith('1') .mockReturnValue({ [moduleId]: { moduleId: moduleId, diff --git a/app/src/organisms/Devices/hooks/useModuleCalibrationStatus.ts b/app/src/organisms/Devices/hooks/useModuleCalibrationStatus.ts index a484e0cd7a0..e8bddaaeadb 100644 --- a/app/src/organisms/Devices/hooks/useModuleCalibrationStatus.ts +++ b/app/src/organisms/Devices/hooks/useModuleCalibrationStatus.ts @@ -11,7 +11,7 @@ export function useModuleCalibrationStatus( const isFlex = useIsFlex(robotName) // TODO: can probably use getProtocolModulesInfo but in a rush to get out 7.0.1 const moduleRenderInfoForProtocolById = omitBy( - useModuleRenderInfoForProtocolById(robotName, runId), + useModuleRenderInfoForProtocolById(runId), moduleRenderInfo => moduleRenderInfo.moduleDef.moduleType === MAGNETIC_BLOCK_TYPE ) diff --git a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts index 317b6ed9209..1c9570dfcde 100644 --- a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts +++ b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts @@ -1,7 +1,7 @@ import { checkModuleCompatibility, + FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' import { useDeckConfigurationQuery } from '@opentrons/react-api-client/src/deck_configuration' @@ -26,20 +26,20 @@ export interface ModuleRenderInfoById { } export function useModuleRenderInfoForProtocolById( - robotName: string, runId: string ): ModuleRenderInfoById { const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) const { data: deckConfig } = useDeckConfigurationQuery() const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) - const protocolData = robotProtocolAnalysis ?? storedProtocolAnalysis - const robotType = getRobotTypeFromLoadedLabware(protocolData?.labware ?? []) + const protocolAnalysis = robotProtocolAnalysis ?? storedProtocolAnalysis const attachedModules = useAttachedModules() - if (protocolData == null) return {} + if (protocolAnalysis == null) return {} - const deckDef = getDeckDefFromRobotType(robotType) + const deckDef = getDeckDefFromRobotType( + protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE + ) - const protocolModulesInfo = getProtocolModulesInfo(protocolData, deckDef) + const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef) const protocolModulesInfoInLoadOrder = protocolModulesInfo.sort( (modA, modB) => modA.protocolLoadOrder - modB.protocolLoadOrder diff --git a/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts b/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts index ec61b88967f..c8c7b3c9b36 100644 --- a/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts +++ b/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts @@ -1,6 +1,6 @@ import * as React from 'react' import last from 'lodash/last' -import { getRobotTypeFromLoadedLabware } from '@opentrons/shared-data' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { useProtocolQuery, useRunQuery, @@ -61,10 +61,10 @@ export function useProtocolDetailsForRun( protocolData: mostRecentAnalysis ?? null, protocolKey: protocolRecord?.data.key ?? null, isProtocolAnalyzing: protocolRecord != null && mostRecentAnalysis == null, - // this should be deleted as soon as analysis tells us intended robot type robotType: - mostRecentAnalysis?.status === 'completed' - ? getRobotTypeFromLoadedLabware(mostRecentAnalysis.labware) - : 'OT-2 Standard', + protocolRecord?.data.robotType ?? + (mostRecentAnalysis?.status === 'completed' + ? mostRecentAnalysis?.robotType ?? FLEX_ROBOT_TYPE + : FLEX_ROBOT_TYPE), } } diff --git a/app/src/organisms/Devices/hooks/useUnmatchedModulesForProtocol.ts b/app/src/organisms/Devices/hooks/useUnmatchedModulesForProtocol.ts index 5e6d52e292d..9c76e899c45 100644 --- a/app/src/organisms/Devices/hooks/useUnmatchedModulesForProtocol.ts +++ b/app/src/organisms/Devices/hooks/useUnmatchedModulesForProtocol.ts @@ -23,10 +23,7 @@ export function useUnmatchedModulesForProtocol( runId: string ): UnmatchedModuleResults { const robot = useRobot(robotName) - const moduleRenderInfoById = useModuleRenderInfoForProtocolById( - robotName, - runId - ) + const moduleRenderInfoById = useModuleRenderInfoForProtocolById(runId) const attachedModules = useAttachedModules() if (robot === null) { return { missingModuleIds: [], remainingAttachedModules: [] } diff --git a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx index db4b5b883ee..1e26151498d 100644 --- a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx +++ b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx @@ -33,7 +33,6 @@ import { getModuleDisplayName, getModuleType, getOccludedSlotCountForModule, - getRobotTypeFromLoadedLabware, } from '@opentrons/shared-data' import { getRunLabwareRenderInfo, @@ -103,6 +102,7 @@ export interface MoveLabwareInterventionProps { command: MoveLabwareRunTimeCommand analysis: CompletedProtocolAnalysis | null run: RunData + robotType: RobotType isOnDevice: boolean } @@ -110,13 +110,13 @@ export function MoveLabwareInterventionContent({ command, analysis, run, + robotType, isOnDevice, }: MoveLabwareInterventionProps): JSX.Element | null { const { t } = useTranslation(['protocol_setup', 'protocol_command_text']) const analysisCommands = analysis?.commands ?? [] const labwareDefsByUri = getLoadedLabwareDefinitionsByUri(analysisCommands) - const robotType = getRobotTypeFromLoadedLabware(run.labware) const deckDef = getDeckDefFromRobotType(robotType) const moduleRenderInfo = getRunModuleRenderInfo( diff --git a/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx b/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx index 2c937472b64..cd320a21058 100644 --- a/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx +++ b/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx @@ -15,11 +15,16 @@ import { truncatedCommandMessage, } from '../__fixtures__' import { mockTipRackDefinition } from '../../../redux/custom-labware/__fixtures__' +import { useIsFlex } from '../../Devices/hooks' const ROBOT_NAME = 'Otie' const mockOnResumeHandler = jest.fn() +jest.mock('../../Devices/hooks') + +const mockUseIsFlex = useIsFlex as jest.MockedFunction + const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -43,6 +48,7 @@ describe('InterventionModal', () => { ], } as CompletedProtocolAnalysis, } + mockUseIsFlex.mockReturnValue(true) }) it('renders an InterventionModal with the robot name in the header and confirm button', () => { diff --git a/app/src/organisms/InterventionModal/index.tsx b/app/src/organisms/InterventionModal/index.tsx index b5b23666ba5..39c6c3cb4d7 100644 --- a/app/src/organisms/InterventionModal/index.tsx +++ b/app/src/organisms/InterventionModal/index.tsx @@ -35,6 +35,7 @@ import { MoveLabwareInterventionContent } from './MoveLabwareInterventionContent import type { RunCommandSummary, RunData } from '@opentrons/api-client' import type { IconName } from '@opentrons/components' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' +import { useRobotType } from '../Devices/hooks' const LEARN_ABOUT_MANUAL_STEPS_URL = 'https://support.opentrons.com/s/article/Manual-protocol-steps' @@ -108,6 +109,7 @@ export function InterventionModal({ const { t } = useTranslation(['protocol_command_text', 'protocol_info']) const isOnDevice = useSelector(getIsOnDevice) + const robotType = useRobotType(robotName) const childContent = React.useMemo(() => { if ( command.commandType === 'waitForResume' || @@ -122,7 +124,7 @@ export function InterventionModal({ } else if (command.commandType === 'moveLabware') { return ( ) diff --git a/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts b/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts index 4d0ece68dc0..16f8dcc4478 100644 --- a/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts +++ b/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts @@ -20,5 +20,11 @@ export function useMostRecentCompletedAnalysis( { enabled: protocolData != null } ) - return analysis ?? null + return analysis != null + ? { + ...analysis, + // NOTE: this is accounting for pre 7.1 robot-side protocol analysis that may not include the robotType key + robotType: analysis.robotType ?? protocolData?.data.robotType, + } + : null } diff --git a/app/src/organisms/ModuleCard/__tests__/hooks.test.tsx b/app/src/organisms/ModuleCard/__tests__/hooks.test.tsx index fc96e7ae506..0ec0c27de50 100644 --- a/app/src/organisms/ModuleCard/__tests__/hooks.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/hooks.test.tsx @@ -7,9 +7,7 @@ import { I18nextProvider } from 'react-i18next' import { renderHook } from '@testing-library/react-hooks' import { i18n } from '../../../i18n' import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' -import { ModuleModel, ModuleType } from '@opentrons/shared-data' import heaterShakerCommandsWithResultsKey from '@opentrons/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json' -import { getProtocolModulesInfo } from '../../Devices/ProtocolRun/utils/getProtocolModulesInfo' import { useCurrentRunId } from '../../ProtocolUpload/hooks' import { useIsRobotBusy, useRunStatuses } from '../../Devices/hooks' import { @@ -31,7 +29,6 @@ import type { Store } from 'redux' import type { State } from '../../../redux/types' jest.mock('@opentrons/react-api-client') -jest.mock('../../Devices/ProtocolRun/utils/getProtocolModulesInfo') jest.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') jest.mock('../../ProtocolUpload/hooks') jest.mock('../../Devices/hooks') @@ -39,9 +36,6 @@ jest.mock('../../Devices/hooks') const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction< typeof useMostRecentCompletedAnalysis > -const mockGetProtocolModulesInfo = getProtocolModulesInfo as jest.MockedFunction< - typeof getProtocolModulesInfo -> const mockUseLiveCommandMutation = useCreateLiveCommandMutation as jest.MockedFunction< typeof useCreateLiveCommandMutation @@ -608,49 +602,27 @@ describe('useModuleOverflowMenu', () => { }) }) -const mockHeaterShakerDefinition = { - moduleId: 'someHeaterShakerModule', - model: 'heaterShakerModuleV1' as ModuleModel, - type: 'heaterShakerModuleType' as ModuleType, - displayName: 'Heater Shaker Module', - labwareOffset: { x: 5, y: 5, z: 5 }, - cornerOffsetFromSlot: { x: 1, y: 1, z: 1 }, - dimensions: { - xDimension: 100, - yDimension: 100, - footprintXDimension: 50, - footprintYDimension: 50, - labwareInterfaceXDimension: 80, - labwareInterfaceYDimension: 120, - }, - twoDimensionalRendering: { children: [] }, -} - -const HEATER_SHAKER_MODULE_INFO = { - moduleId: 'heaterShakerModuleId', - x: 0, - y: 0, - z: 0, - moduleDef: mockHeaterShakerDefinition as any, - nestedLabwareDef: null, - nestedLabwareId: null, - nestedLabwareDisplayName: null, - protocolLoadOrder: 0, - slotName: '1', -} - describe('useIsHeaterShakerInProtocol', () => { const store: Store = createStore(jest.fn(), {}) beforeEach(() => { when(mockUseCurrentRunId).calledWith().mockReturnValue('1') store.dispatch = jest.fn() - mockGetProtocolModulesInfo.mockReturnValue([HEATER_SHAKER_MODULE_INFO]) when(mockUseMostRecentCompletedAnalysis) .calledWith('1') .mockReturnValue({ ...heaterShakerCommandsWithResultsKey, + modules: [ + { + id: 'fake_module_id', + model: 'heaterShakerModuleV1', + location: { + slotName: '1', + }, + serialNumber: 'fake_serial', + }, + ], labware: Object.keys(heaterShakerCommandsWithResultsKey.labware).map( id => ({ location: 'offDeck', @@ -677,8 +649,20 @@ describe('useIsHeaterShakerInProtocol', () => { }) it('should return false when a heater shaker is NOT in the protocol', () => { - mockGetProtocolModulesInfo.mockReturnValue([]) - + when(mockUseMostRecentCompletedAnalysis) + .calledWith('1') + .mockReturnValue({ + ...heaterShakerCommandsWithResultsKey, + modules: [], + labware: Object.keys(heaterShakerCommandsWithResultsKey.labware).map( + id => ({ + location: 'offDeck', + loadName: id, + definitionUrui: id, + id, + }) + ), + } as any) const wrapper: React.FunctionComponent<{}> = ({ children }) => ( {children} ) diff --git a/app/src/organisms/ModuleCard/hooks.tsx b/app/src/organisms/ModuleCard/hooks.tsx index eb5e835de7c..5f2f7622f2e 100644 --- a/app/src/organisms/ModuleCard/hooks.tsx +++ b/app/src/organisms/ModuleCard/hooks.tsx @@ -3,14 +3,11 @@ import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' import { useTranslation } from 'react-i18next' import { useHoverTooltip } from '@opentrons/components' import { - getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, HEATERSHAKER_MODULE_TYPE, MAGNETIC_MODULE_TYPE, TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' -import { getProtocolModulesInfo } from '../Devices/ProtocolRun/utils/getProtocolModulesInfo' import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { MenuItem } from '../../atoms/MenuList/MenuItem' import { Tooltip } from '../../atoms/Tooltip' @@ -35,15 +32,9 @@ export function useIsHeaterShakerInProtocol(): boolean { const currentRunId = useCurrentRunId() const robotProtocolAnalysis = useMostRecentCompletedAnalysis(currentRunId) if (robotProtocolAnalysis == null) return false - const robotType = getRobotTypeFromLoadedLabware(robotProtocolAnalysis.labware) - const deckDef = getDeckDefFromRobotType(robotType) - const protocolModulesInfo = getProtocolModulesInfo( - robotProtocolAnalysis, - deckDef - ) - return protocolModulesInfo.some( - module => module.moduleDef.model === 'heaterShakerModuleV1' + return robotProtocolAnalysis.modules.some( + module => module.model === 'heaterShakerModuleV1' ) } interface LatchControls { diff --git a/app/src/pages/Devices/ProtocolRunDetails/index.tsx b/app/src/pages/Devices/ProtocolRunDetails/index.tsx index 99852cd77d0..7402f77ab87 100644 --- a/app/src/pages/Devices/ProtocolRunDetails/index.tsx +++ b/app/src/pages/Devices/ProtocolRunDetails/index.tsx @@ -300,7 +300,6 @@ const ModuleControlsTab = ( const { t } = useTranslation('run_details') const currentRunId = useCurrentRunId() const moduleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById( - robotName, runId ) const { isRunStill } = useRunStatuses() diff --git a/shared-data/deck/definitions/4/ot3_standard.json b/shared-data/deck/definitions/4/ot3_standard.json index 728f9743299..a01ee27aa92 100644 --- a/shared-data/deck/definitions/4/ot3_standard.json +++ b/shared-data/deck/definitions/4/ot3_standard.json @@ -205,6 +205,7 @@ "id": "A4", "areaType": "slot", "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -217,6 +218,7 @@ "id": "B4", "areaType": "slot", "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -229,6 +231,7 @@ "id": "C4", "areaType": "slot", "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -241,6 +244,7 @@ "id": "D4", "areaType": "slot", "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, diff --git a/shared-data/js/helpers/__tests__/getRobotTypeFromLoadedLabware.test.ts b/shared-data/js/helpers/__tests__/getRobotTypeFromLoadedLabware.test.ts deleted file mode 100644 index ecba27c7b89..00000000000 --- a/shared-data/js/helpers/__tests__/getRobotTypeFromLoadedLabware.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getRobotTypeFromLoadedLabware } from '..' -import type { LoadedLabware } from '../..' - -describe('getRobotTypeFromLoadedLabware', () => { - it('should return an OT-2 when an OT-2 trash is loaded into the protocol', () => { - const labware: LoadedLabware[] = [ - { - id: 'fixedTrash', - loadName: 'opentrons_1_trash_1100ml_fixed', - definitionUri: 'opentrons/opentrons_1_trash_1100ml_fixed/1', - location: { - slotName: '12', - }, - }, - ] - expect(getRobotTypeFromLoadedLabware(labware)).toBe('OT-2 Standard') - }) - it('should return an OT-3 when an OT-3 trash is loaded into the protocol', () => { - const labware: LoadedLabware[] = [ - { - id: 'fixedTrash', - loadName: 'opentrons_1_trash_3200ml_fixed', - definitionUri: 'opentrons/opentrons_1_trash_3200ml_fixed/1', - location: { - slotName: '12', - }, - }, - ] - expect(getRobotTypeFromLoadedLabware(labware)).toBe('OT-3 Standard') - }) -}) diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index cdc79967cf7..1263dda8cfc 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -7,7 +7,6 @@ import standardFlexDeckDef from '../../deck/definitions/4/ot3_standard.json' import type { DeckDefinition, LabwareDefinition2, - LoadedLabware, ModuleModel, RobotType, ThermalAdapterName, @@ -202,10 +201,10 @@ export const getWellsDepth = ( export const getSlotHasMatingSurfaceUnitVector = ( deckDef: DeckDefinition, - slotNumber: string + addressableAreaName: string ): boolean => { const matingSurfaceUnitVector = deckDef.locations.addressableAreas.find( - orderedSlot => orderedSlot.id === slotNumber + aa => aa.id === addressableAreaName )?.matingSurfaceUnitVector return Boolean(matingSurfaceUnitVector) @@ -337,15 +336,6 @@ export const getCalibrationAdapterLoadName = ( } } -export const getRobotTypeFromLoadedLabware = ( - labware: LoadedLabware[] -): RobotType => { - const isProtocolForOT3 = labware.some( - l => l.loadName === 'opentrons_1_trash_3200ml_fixed' - ) - return isProtocolForOT3 ? 'OT-3 Standard' : 'OT-2 Standard' -} - export const getDeckDefFromRobotType = ( robotType: RobotType ): DeckDefinition => { diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index c295bd8977a..3570d42ddbf 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -473,6 +473,7 @@ export interface CompletedProtocolAnalysis { liquids: Liquid[] commands: RunTimeCommand[] errors: AnalysisError[] + robotType?: RobotType | null } export interface ResourceFile { From 892b0e721175e28c6cd052622f44d0b0178c338c Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 16 Nov 2023 11:49:58 -0500 Subject: [PATCH 28/46] feat(robot-server): Add robotType to analysis results (#13995) --- api/src/opentrons/cli/analyze.py | 6 +++ .../robot_server/protocols/analysis_models.py | 38 ++++++++++++++----- .../robot_server/protocols/analysis_store.py | 4 ++ .../protocols/protocol_analyzer.py | 1 + .../protocols/test_v6_json_upload.tavern.yaml | 1 + .../test_v8_json_upload_flex.tavern.yaml | 1 + .../test_v8_json_upload_ot2.tavern.yaml | 1 + .../tests/protocols/test_analysis_store.py | 5 +++ .../tests/protocols/test_protocol_analyzer.py | 8 +++- 9 files changed, 53 insertions(+), 12 deletions(-) diff --git a/api/src/opentrons/cli/analyze.py b/api/src/opentrons/cli/analyze.py index 9f5af67c584..107da30f734 100644 --- a/api/src/opentrons/cli/analyze.py +++ b/api/src/opentrons/cli/analyze.py @@ -145,10 +145,16 @@ class AnalyzeResults(BaseModel): See robot-server's analysis models for field documentation. """ + # We want to unify this local analysis model with the one that robot-server returns. + # Until that happens, we need to keep these fields in sync manually. + + # Fields that are currently unique to this local analysis module, missing from robot-server: createdAt: datetime files: List[ProtocolFile] config: Union[JsonConfig, PythonConfig] metadata: Dict[str, Any] + + # Fields that should match robot-server: robotType: RobotType commands: List[Command] labware: List[LoadedLabware] diff --git a/robot-server/robot_server/protocols/analysis_models.py b/robot-server/robot_server/protocols/analysis_models.py index d31bb9f4eaa..8caa22f30a7 100644 --- a/robot-server/robot_server/protocols/analysis_models.py +++ b/robot-server/robot_server/protocols/analysis_models.py @@ -1,8 +1,9 @@ """Response models for protocol analysis.""" # TODO(mc, 2021-08-25): add modules to simulation result from enum import Enum +from opentrons_shared_data.robot.dev_types import RobotType from pydantic import BaseModel, Field -from typing import List, Union +from typing import List, Optional, Union from typing_extensions import Literal from opentrons.protocol_engine import ( @@ -75,6 +76,10 @@ class CompletedAnalysis(BaseModel): JSON protocols are currently deterministic by design. """ + # We want to unify this HTTP-facing analysis model with the one that local analysis returns. + # Until that happens, we need to keep these fields in sync manually. + + # Fields that are currently unique to robot-server, missing from local analysis: id: str = Field(..., description="Unique identifier of this analysis resource") status: Literal[AnalysisStatus.COMPLETED] = Field( AnalysisStatus.COMPLETED, @@ -84,9 +89,22 @@ class CompletedAnalysis(BaseModel): ..., description="Whether the protocol is expected to run successfully", ) - pipettes: List[LoadedPipette] = Field( + + # Fields that should match local analysis: + robotType: Optional[RobotType] = Field( + # robotType is deliberately typed as a Literal instead of an Enum. + # It's a bad idea at the moment to store enums in robot-server's database. + # https://opentrons.atlassian.net/browse/RSS-98 + default=None, # default=None to fit objects that were stored before this field existed. + description=( + "The type of robot that this protocol can run on." + " This field was added in v7.1.0. It will be `null` or omitted" + " in analyses that were originally created on older versions." + ), + ) + commands: List[Command] = Field( ..., - description="Pipettes used by the protocol", + description="The protocol commands the run is expected to produce", ) labware: List[LoadedLabware] = Field( ..., @@ -98,13 +116,17 @@ class CompletedAnalysis(BaseModel): " not its *initial* location." ), ) + pipettes: List[LoadedPipette] = Field( + ..., + description="Pipettes used by the protocol", + ) modules: List[LoadedModule] = Field( default_factory=list, description="Modules that have been loaded into the run.", ) - commands: List[Command] = Field( - ..., - description="The protocol commands the run is expected to produce", + liquids: List[Liquid] = Field( + default_factory=list, + description="Liquids used by the protocol", ) errors: List[ErrorOccurrence] = Field( ..., @@ -114,10 +136,6 @@ class CompletedAnalysis(BaseModel): " but it won't have more than one element." ), ) - liquids: List[Liquid] = Field( - default_factory=list, - description="Liquids used by the protocol", - ) ProtocolAnalysis = Union[PendingAnalysis, CompletedAnalysis] diff --git a/robot-server/robot_server/protocols/analysis_store.py b/robot-server/robot_server/protocols/analysis_store.py index 29ddb0d82ec..8165cf9cf8e 100644 --- a/robot-server/robot_server/protocols/analysis_store.py +++ b/robot-server/robot_server/protocols/analysis_store.py @@ -5,6 +5,7 @@ from logging import getLogger from typing import Dict, List, Optional from typing_extensions import Final +from opentrons_shared_data.robot.dev_types import RobotType import sqlalchemy @@ -120,6 +121,7 @@ def add_pending(self, protocol_id: str, analysis_id: str) -> AnalysisSummary: async def update( self, analysis_id: str, + robot_type: RobotType, commands: List[Command], labware: List[LoadedLabware], modules: List[LoadedModule], @@ -132,6 +134,7 @@ async def update( Args: analysis_id: The ID of the analysis to promote. Must point to a valid pending analysis. + robot_type: See `CompletedAnalysis.robotType`. commands: See `CompletedAnalysis.commands`. labware: See `CompletedAnalysis.labware`. modules: See `CompletedAnalysis.modules`. @@ -156,6 +159,7 @@ async def update( completed_analysis = CompletedAnalysis.construct( id=analysis_id, result=result, + robotType=robot_type, commands=commands, labware=labware, modules=modules, diff --git a/robot-server/robot_server/protocols/protocol_analyzer.py b/robot-server/robot_server/protocols/protocol_analyzer.py index cd8c44cb539..cce1c5240a3 100644 --- a/robot-server/robot_server/protocols/protocol_analyzer.py +++ b/robot-server/robot_server/protocols/protocol_analyzer.py @@ -36,6 +36,7 @@ async def analyze( await self._analysis_store.update( analysis_id=analysis_id, + robot_type=protocol_resource.source.robot_type, commands=result.commands, labware=result.state_summary.labware, modules=result.state_summary.modules, diff --git a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml index d4ddc81b2eb..d84cc464cbc 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml @@ -84,6 +84,7 @@ stages: - id: '{analysis_id}' status: completed result: ok + robotType: OT-2 Standard pipettes: - id: pipetteId pipetteName: p10_single diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml index 7165f65d478..a2ec1a8bb6a 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml @@ -85,6 +85,7 @@ stages: - id: '{analysis_id}' status: completed result: ok + robotType: OT-3 Standard pipettes: - id: pipetteId pipetteName: p1000_96 diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml index e33c1d71026..954551ebd53 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml @@ -84,6 +84,7 @@ stages: - id: '{analysis_id}' status: completed result: ok + robotType: OT-2 Standard pipettes: - id: pipetteId pipetteName: p10_single diff --git a/robot-server/tests/protocols/test_analysis_store.py b/robot-server/tests/protocols/test_analysis_store.py index 058ec0497d9..06ca93cd218 100644 --- a/robot-server/tests/protocols/test_analysis_store.py +++ b/robot-server/tests/protocols/test_analysis_store.py @@ -126,6 +126,7 @@ async def test_returned_in_order_added( subject.add_pending(protocol_id="protocol-id", analysis_id=analysis_id) await subject.update( analysis_id=analysis_id, + robot_type="OT-2 Standard", labware=[], modules=[], pipettes=[], @@ -173,6 +174,7 @@ async def test_update_adds_details_and_completes_analysis( subject.add_pending(protocol_id="protocol-id", analysis_id="analysis-id") await subject.update( analysis_id="analysis-id", + robot_type="OT-2 Standard", labware=[labware], pipettes=[pipette], # TODO(mm, 2022-10-21): Give the subject some commands, errors, and liquids here @@ -189,6 +191,7 @@ async def test_update_adds_details_and_completes_analysis( assert result == CompletedAnalysis( id="analysis-id", result=AnalysisResult.OK, + robotType="OT-2 Standard", labware=[labware], pipettes=[pipette], modules=[], @@ -201,6 +204,7 @@ async def test_update_adds_details_and_completes_analysis( "id": "analysis-id", "result": "ok", "status": "completed", + "robotType": "OT-2 Standard", "labware": [ { "id": "labware-id", @@ -270,6 +274,7 @@ async def test_update_infers_status_from_errors( subject.add_pending(protocol_id="protocol-id", analysis_id="analysis-id") await subject.update( analysis_id="analysis-id", + robot_type="OT-2 Standard", commands=commands, errors=errors, labware=[], diff --git a/robot-server/tests/protocols/test_protocol_analyzer.py b/robot-server/tests/protocols/test_protocol_analyzer.py index c655db805a5..b2900b982c8 100644 --- a/robot-server/tests/protocols/test_protocol_analyzer.py +++ b/robot-server/tests/protocols/test_protocol_analyzer.py @@ -4,6 +4,7 @@ from datetime import datetime from pathlib import Path +from opentrons_shared_data.robot.dev_types import RobotType from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons.types import MountType, DeckSlotName @@ -53,6 +54,8 @@ async def test_analyze( subject: ProtocolAnalyzer, ) -> None: """It should be able to analyze a protocol.""" + robot_type: RobotType = "OT-3 Standard" + protocol_resource = ProtocolResource( protocol_id="protocol-id", created_at=datetime(year=2021, month=1, day=1), @@ -62,7 +65,7 @@ async def test_analyze( config=JsonProtocolConfig(schema_version=123), files=[], metadata={}, - robot_type="OT-3 Standard", + robot_type=robot_type, content_hash="abc123", ), protocol_key="dummy-data-111", @@ -101,7 +104,7 @@ async def test_analyze( decoy.when( await protocol_runner.create_simulating_runner( - robot_type="OT-3 Standard", + robot_type=robot_type, protocol_config=JsonProtocolConfig(schema_version=123), ) ).then_return(json_runner) @@ -130,6 +133,7 @@ async def test_analyze( decoy.verify( await analysis_store.update( analysis_id="analysis-id", + robot_type=robot_type, commands=[analysis_command], labware=[analysis_labware], modules=[], From 3b59962b4a2ba00119c78c42058a5e1c52de4fc6 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 16 Nov 2023 12:42:23 -0500 Subject: [PATCH 29/46] fix(api): correct logic for simming partial tips (#13998) When we simulate partial tip pickup we do it by emulating the hardware control logic in the virtual pipette data provider and then passing the result to a nozzle manager the way the hardware controller does. Unfortunately, the logic in the pipette virtual data handler did not match the hardware controller. Until we get to a place where we can have an actual simualting hardware controller or something, we just have to keep these in sync. --- .../protocol_engine/execution/tip_handler.py | 135 ++++++++---------- .../resources/pipette_data_provider.py | 4 +- .../resources/test_pipette_data_provider.py | 23 +++ 3 files changed, 88 insertions(+), 74 deletions(-) diff --git a/api/src/opentrons/protocol_engine/execution/tip_handler.py b/api/src/opentrons/protocol_engine/execution/tip_handler.py index 4ea54df86fa..b04750a343a 100644 --- a/api/src/opentrons/protocol_engine/execution/tip_handler.py +++ b/api/src/opentrons/protocol_engine/execution/tip_handler.py @@ -63,6 +63,47 @@ async def add_tip(self, pipette_id: str, tip: TipGeometry) -> None: """Tell the Hardware API that a tip is attached.""" +async def _available_for_nozzle_layout( + channels: int, + style: str, + primary_nozzle: Optional[str], + front_right_nozzle: Optional[str], +) -> Dict[str, str]: + """Check nozzle layout is compatible with the pipette. + + Returns: + A dict of nozzles used to configure the pipette. + """ + if channels == 1: + raise CommandPreconditionViolated( + message=f"Cannot configure nozzle layout with a {channels} channel pipette." + ) + if style == "EMPTY": + return {} + if style == "ROW" and channels == 8: + raise CommandParameterLimitViolated( + command_name="configure_nozzle_layout", + parameter_name="RowNozzleLayout", + limit_statement="RowNozzleLayout is incompatible with {channels} channel pipettes.", + actual_value=str(primary_nozzle), + ) + if not primary_nozzle: + return {"primary_nozzle": "A1"} + if style == "SINGLE": + return {"primary_nozzle": primary_nozzle} + if not front_right_nozzle: + return { + "primary_nozzle": primary_nozzle, + "front_right_nozzle": PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP[primary_nozzle][ + style + ], + } + return { + "primary_nozzle": primary_nozzle, + "front_right_nozzle": front_right_nozzle, + } + + class HardwareTipHandler(TipHandler): """Pick up and drop tips, using the Hardware API.""" @@ -72,9 +113,9 @@ def __init__( hardware_api: HardwareControlAPI, labware_data_provider: Optional[LabwareDataProvider] = None, ) -> None: - self._state_view = state_view self._hardware_api = hardware_api self._labware_data_provider = labware_data_provider or LabwareDataProvider() + self._state_view = state_view async def available_for_nozzle_layout( self, @@ -83,40 +124,15 @@ async def available_for_nozzle_layout( primary_nozzle: Optional[str] = None, front_right_nozzle: Optional[str] = None, ) -> Dict[str, str]: - """Check nozzle layout is compatible with the pipette.""" + """Returns configuration for nozzle layout to pass to configure_nozzle_layout.""" if self._state_view.pipettes.get_attached_tip(pipette_id): raise CommandPreconditionViolated( message=f"Cannot configure nozzle layout of {str(self)} while it has tips attached." ) channels = self._state_view.pipettes.get_channels(pipette_id) - if channels == 1: - raise CommandPreconditionViolated( - message=f"Cannot configure nozzle layout with a {channels} channel pipette." - ) - if style == "EMPTY": - return {} - if style == "ROW" and channels == 8: - raise CommandParameterLimitViolated( - command_name="configure_nozzle_layout", - parameter_name="RowNozzleLayout", - limit_statement="RowNozzleLayout is incompatible with {channels} channel pipettes.", - actual_value=str(primary_nozzle), - ) - if not primary_nozzle: - return {"primary_nozzle": "A1"} - if style == "SINGLE": - return {"primary_nozzle": primary_nozzle} - if not front_right_nozzle: - return { - "primary_nozzle": primary_nozzle, - "front_right_nozzle": PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP[ - primary_nozzle - ][style], - } - return { - "primary_nozzle": primary_nozzle, - "front_right_nozzle": front_right_nozzle, - } + return await _available_for_nozzle_layout( + channels, style, primary_nozzle, front_right_nozzle + ) async def pick_up_tip( self, @@ -196,48 +212,6 @@ class VirtualTipHandler(TipHandler): def __init__(self, state_view: StateView) -> None: self._state_view = state_view - async def available_for_nozzle_layout( - self, - pipette_id: str, - style: str, - primary_nozzle: Optional[str] = None, - front_right_nozzle: Optional[str] = None, - ) -> Dict[str, str]: - """Check nozzle layout is compatible with the pipette.""" - if self._state_view.pipettes.get_attached_tip(pipette_id): - raise CommandPreconditionViolated( - message=f"Cannot configure nozzle layout of {str(self)} while it has tips attached." - ) - channels = self._state_view.pipettes.get_channels(pipette_id) - if channels == 1: - raise CommandPreconditionViolated( - message=f"Cannot configure nozzle layout with a {channels} channel pipette." - ) - if style == "EMPTY": - return {} - if style == "ROW" and channels == 8: - raise CommandParameterLimitViolated( - command_name="configure_nozzle_layout", - parameter_name="RowNozzleLayout", - limit_statement="RowNozzleLayout is incompatible with {channels} channel pipettes.", - actual_value=str(primary_nozzle), - ) - if not primary_nozzle: - return {"primary_nozzle": "A1"} - if style == "SINGLE": - return {"primary_nozzle": primary_nozzle} - if not front_right_nozzle: - return { - "primary_nozzle": primary_nozzle, - "front_right_nozzle": PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP[ - primary_nozzle - ][style], - } - return { - "primary_nozzle": primary_nozzle, - "front_right_nozzle": front_right_nozzle, - } - async def pick_up_tip( self, pipette_id: str, @@ -262,6 +236,23 @@ async def pick_up_tip( return nominal_tip_geometry + async def available_for_nozzle_layout( + self, + pipette_id: str, + style: str, + primary_nozzle: Optional[str] = None, + front_right_nozzle: Optional[str] = None, + ) -> Dict[str, str]: + """Returns configuration for nozzle layout to pass to configure_nozzle_layout.""" + if self._state_view.pipettes.get_attached_tip(pipette_id): + raise CommandPreconditionViolated( + message=f"Cannot configure nozzle layout of {str(self)} while it has tips attached." + ) + channels = self._state_view.pipettes.get_channels(pipette_id) + return await _available_for_nozzle_layout( + channels, style, primary_nozzle, front_right_nozzle + ) + async def drop_tip( self, pipette_id: str, diff --git a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py index a45d416a526..b3bf334933f 100644 --- a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py @@ -60,12 +60,12 @@ def configure_virtual_pipette_nozzle_layout( pipette_model_string ) new_nozzle_manager = NozzleConfigurationManager.build_from_config(config) - if back_left_nozzle and front_right_nozzle and starting_nozzle: + if back_left_nozzle and front_right_nozzle: new_nozzle_manager.update_nozzle_configuration( back_left_nozzle, front_right_nozzle, starting_nozzle ) self._nozzle_manager_layout_by_id[pipette_id] = new_nozzle_manager - elif back_left_nozzle and front_right_nozzle and starting_nozzle: + elif back_left_nozzle and front_right_nozzle: # Need to make sure that we pass all the right nozzles here. self._nozzle_manager_layout_by_id[pipette_id].update_nozzle_configuration( back_left_nozzle, front_right_nozzle, starting_nozzle diff --git a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py index c3cf10449fc..7478e34f131 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py @@ -138,6 +138,29 @@ def test_load_virtual_pipette_nozzle_layout( assert result.front_right == "E1" assert result.back_left == "A1" + subject_instance.configure_virtual_pipette_nozzle_layout( + "my-pipette", "p300_multi_v2.1" + ) + result = subject_instance.get_nozzle_layout_for_pipette("my-pipette") + assert result.configuration.value == "FULL" + + subject_instance.configure_virtual_pipette_nozzle_layout( + "my-96-pipette", "p1000_96_v3.5", "A1", "A12", "A1" + ) + result = subject_instance.get_nozzle_layout_for_pipette("my-96-pipette") + assert result.configuration.value == "ROW" + subject_instance.configure_virtual_pipette_nozzle_layout( + "my-96-pipette", "p1000_96_v3.5", "A1", "A1" + ) + result = subject_instance.get_nozzle_layout_for_pipette("my-96-pipette") + assert result.configuration.value == "SINGLE" + + subject_instance.configure_virtual_pipette_nozzle_layout( + "my-96-pipette", "p1000_96_v3.5", "A1", "H1" + ) + result = subject_instance.get_nozzle_layout_for_pipette("my-96-pipette") + assert result.configuration.value == "COLUMN" + def test_get_pipette_static_config( supported_tip_fixture: pipette_definition.SupportedTipsDefinition, From 749ad22175e4b68d668d4a1d823d2b6f9d0d38ec Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 16 Nov 2023 12:55:24 -0500 Subject: [PATCH 30/46] fix(app): fix device reset option selector issues (#13996) * fix(app): fix device reset option selector issues --- .../RobotSettingsDashboard/DeviceReset.tsx | 67 ++++++++++++----- .../__tests__/DeviceReset.test.tsx | 71 +++++++++++++++++++ 2 files changed, 120 insertions(+), 18 deletions(-) diff --git a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx b/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx index fe8667f791f..1095694392f 100644 --- a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx +++ b/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx @@ -52,8 +52,6 @@ interface DeviceResetProps { setCurrentOption: SetSettingOption } -// ToDo (kk:08/30/2023) lines that are related to module calibration will be activated when the be is ready. -// The tests for that will be added. export function DeviceReset({ robotName, setCurrentOption, @@ -86,20 +84,19 @@ export function DeviceReset({ ({ id }) => !['authorizedKeys'].includes(id) ) + const isEveryOptionSelected = (obj: ResetConfigRequest): boolean => { + for (const key of targetOptionsOrder) { + if (obj != null && !obj[key]) { + return false + } + } + return true + } + const handleClick = (): void => { if (resetOptions != null) { - // remove clearAllStoredData since its not a setting on the backend - const { clearAllStoredData, ...serverResetOptions } = resetOptions - if (Boolean(clearAllStoredData)) { - dispatchRequest( - resetConfig(robotName, { - ...serverResetOptions, - onDeviceDisplay: true, - }) - ) - } else { - dispatchRequest(resetConfig(robotName, serverResetOptions)) - } + const { ...serverResetOptions } = resetOptions + dispatchRequest(resetConfig(robotName, serverResetOptions)) } } @@ -145,6 +142,33 @@ export function DeviceReset({ dispatch(fetchResetConfigOptions(robotName)) }, [dispatch, robotName]) + React.useEffect(() => { + if ( + isEveryOptionSelected(resetOptions) && + (!resetOptions.authorizedKeys || !resetOptions.onDeviceDisplay) + ) { + setResetOptions({ + ...resetOptions, + authorizedKeys: true, + onDeviceDisplay: true, + }) + } + }, [resetOptions]) + + React.useEffect(() => { + if ( + !isEveryOptionSelected(resetOptions) && + resetOptions.authorizedKeys && + resetOptions.onDeviceDisplay + ) { + setResetOptions({ + ...resetOptions, + authorizedKeys: false, + onDeviceDisplay: false, + }) + } + }, [resetOptions]) + return ( {showConfirmationModal && ( @@ -218,7 +242,8 @@ export function DeviceReset({ value="clearAllStoredData" onChange={() => { setResetOptions( - Boolean(resetOptions.clearAllStoredData) + (resetOptions.authorizedKeys ?? false) && + (resetOptions.onDeviceDisplay ?? false) ? {} : availableOptions.reduce( (acc, val) => { @@ -227,14 +252,18 @@ export function DeviceReset({ [val.id]: true, } }, - { clearAllStoredData: true } + { authorizedKeys: true, onDeviceDisplay: true } ) ) }} /> @@ -243,7 +272,9 @@ export function DeviceReset({ { getByText('Clear module calibration') getByText('Clear protocol run history') getByText('Clears information about past runs of all protocols.') + getByText('Clear all stored data') + getByText( + 'Resets all settings. You’ll have to redo initial setup before using the robot again.' + ) expect(queryByText('Clear the ssh authorized keys')).not.toBeInTheDocument() expect(getByTestId('DeviceReset_clear_data_button')).toBeDisabled() }) @@ -115,4 +119,71 @@ describe('DeviceReset', () => { mockResetConfig('mockRobot', clearMockResetOptions) ) }) + + it('when tapping clear all stored data, all options are active', () => { + const clearMockResetOptions = { + pipetteOffsetCalibrations: true, + moduleCalibration: true, + runsHistory: true, + gripperOffsetCalibrations: true, + authorizedKeys: true, + onDeviceDisplay: true, + } + + const [{ getByText }] = render(props) + getByText('Clear all stored data').click() + const clearButton = getByText('Clear data and restart robot') + fireEvent.click(clearButton) + getByText('Are you sure you want to reset your device?') + fireEvent.click(getByText('Confirm')) + expect(dispatchApiRequest).toBeCalledWith( + mockResetConfig('mockRobot', clearMockResetOptions) + ) + }) + + it('when tapping all options except clear all stored data, all options are active', () => { + const clearMockResetOptions = { + pipetteOffsetCalibrations: true, + moduleCalibration: true, + runsHistory: true, + gripperOffsetCalibrations: true, + authorizedKeys: true, + onDeviceDisplay: true, + } + + const [{ getByText }] = render(props) + getByText('Clear pipette calibration').click() + getByText('Clear gripper calibration').click() + getByText('Clear module calibration').click() + getByText('Clear protocol run history').click() + const clearButton = getByText('Clear data and restart robot') + fireEvent.click(clearButton) + getByText('Are you sure you want to reset your device?') + fireEvent.click(getByText('Confirm')) + expect(dispatchApiRequest).toBeCalledWith( + mockResetConfig('mockRobot', clearMockResetOptions) + ) + }) + + it('when tapping clear all stored data and unselect one options, all options are not active', () => { + const clearMockResetOptions = { + pipetteOffsetCalibrations: false, + moduleCalibration: true, + runsHistory: true, + gripperOffsetCalibrations: true, + authorizedKeys: false, + onDeviceDisplay: false, + } + + const [{ getByText }] = render(props) + getByText('Clear all stored data').click() + getByText('Clear pipette calibration').click() + const clearButton = getByText('Clear data and restart robot') + fireEvent.click(clearButton) + getByText('Are you sure you want to reset your device?') + fireEvent.click(getByText('Confirm')) + expect(dispatchApiRequest).toBeCalledWith( + mockResetConfig('mockRobot', clearMockResetOptions) + ) + }) }) From e4c123f91199e5e0d42fb1e9afe4ee2d2e01964a Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:02:25 -0500 Subject: [PATCH 31/46] feat(app): fE support for aspirateInPlace (#13999) --- .../en/protocol_command_text.json | 1 + .../CommandText/PipettingCommandText.tsx | 4 ++++ .../__tests__/CommandText.test.tsx | 21 +++++++++++++++++++ app/src/organisms/CommandText/index.tsx | 1 + shared-data/command/types/pipetting.ts | 17 +++++++++++++++ 5 files changed, 44 insertions(+) diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index 167b64c08ed..b13548a8696 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -2,6 +2,7 @@ "adapter_in_mod_in_slot": "{{adapter}} on {{module}} in {{slot}}", "adapter_in_slot": "{{adapter}} in {{slot}}", "aspirate": "Aspirating {{volume}} µL from well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", + "aspirate_in_place": "Aspirating {{volume}} µL in place at {{flow_rate}} µL/sec ", "blowout": "Blowing out at well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", "blowout_in_place": "Blowing out in place at {{flow_rate}} µL/sec", "closing_tc_lid": "Closing Thermocycler lid", diff --git a/app/src/organisms/CommandText/PipettingCommandText.tsx b/app/src/organisms/CommandText/PipettingCommandText.tsx index defd89fbab9..001d56a2b7b 100644 --- a/app/src/organisms/CommandText/PipettingCommandText.tsx +++ b/app/src/organisms/CommandText/PipettingCommandText.tsx @@ -126,6 +126,10 @@ export const PipettingCommandText = ({ const { flowRate } = command.params return t('blowout_in_place', { flow_rate: flowRate }) } + case 'aspirateInPlace': { + const { flowRate, volume } = command.params + return t('aspirate_in_place', { volume, flow_rate: flowRate }) + } default: { console.warn( 'PipettingCommandText encountered a command with an unrecognized commandType: ', diff --git a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx index 242c6939eb4..6779a6ee52c 100644 --- a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx +++ b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' import { + AspirateInPlaceRunTimeCommand, BlowoutInPlaceRunTimeCommand, DispenseInPlaceRunTimeCommand, DropTipInPlaceRunTimeCommand, @@ -151,6 +152,26 @@ describe('CommandText', () => { )[0] getByText('Blowing out in place at 300 µL/sec') }) + it('renders correct text for aspirateInPlace', () => { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Aspirating 10 µL in place at 300 µL/sec') + }) it('renders correct text for moveToWell', () => { const dispenseCommand = mockRobotSideAnalysis.commands.find( c => c.commandType === 'aspirate' diff --git a/app/src/organisms/CommandText/index.tsx b/app/src/organisms/CommandText/index.tsx index 00e9bca5808..70cd5f71cb2 100644 --- a/app/src/organisms/CommandText/index.tsx +++ b/app/src/organisms/CommandText/index.tsx @@ -54,6 +54,7 @@ export function CommandText(props: Props): JSX.Element | null { switch (command.commandType) { case 'aspirate': + case 'aspirateInPlace': case 'dispense': case 'dispenseInPlace': case 'blowout': diff --git a/shared-data/command/types/pipetting.ts b/shared-data/command/types/pipetting.ts index fe54ed1cf81..98cfad69f99 100644 --- a/shared-data/command/types/pipetting.ts +++ b/shared-data/command/types/pipetting.ts @@ -1,6 +1,7 @@ import type { CommonCommandRunTimeInfo, CommonCommandCreateInfo } from '.' export type PipettingRunTimeCommand = | AspirateRunTimeCommand + | AspirateInPlaceRunTimeCommand | BlowoutInPlaceRunTimeCommand | BlowoutRunTimeCommand | ConfigureForVolumeRunTimeCommand @@ -14,6 +15,7 @@ export type PipettingRunTimeCommand = export type PipettingCreateCommand = | AspirateCreateCommand + | AspirateInPlaceCreateCommand | BlowoutCreateCommand | BlowoutInPlaceCreateCommand | ConfigureForVolumeCreateCommand @@ -50,6 +52,16 @@ export interface AspirateRunTimeCommand result?: BasicLiquidHandlingResult } +export interface AspirateInPlaceCreateCommand extends CommonCommandCreateInfo { + commandType: 'aspirateInPlace' + params: AspirateInPlaceParams +} +export interface AspirateInPlaceRunTimeCommand + extends CommonCommandRunTimeInfo, + AspirateInPlaceCreateCommand { + result?: BasicLiquidHandlingResult +} + export type DispenseParams = AspDispAirgapParams & { pushOut?: number } export interface DispenseCreateCommand extends CommonCommandCreateInfo { commandType: 'dispense' @@ -174,6 +186,11 @@ export interface DispenseInPlaceParams { pushOut?: number } +export interface AspirateInPlaceParams { + pipetteId: string + volume: number + flowRate: number // µL/s +} interface FlowRateParams { flowRate: number // µL/s } From afe23d475714643a575bc7a9958ba8904c7535c3 Mon Sep 17 00:00:00 2001 From: Laura Cox <31892318+Laura-Danielle@users.noreply.github.com> Date: Thu, 16 Nov 2023 21:52:44 +0200 Subject: [PATCH 32/46] feat(api): disable return tip for partially configured pipette (#13972) It is unsafe for us to return tips to a tiprack while in partial tip configuration because we risk the potential of picking up used tips. Closes RSS-360 --------- Co-authored-by: Seth Foster --- .../protocol_api/core/engine/instrument.py | 6 ++++++ .../opentrons/protocol_engine/commands/drop_tip.py | 8 +++++++- .../opentrons/protocol_engine/state/geometry.py | 8 ++++++++ .../opentrons/protocol_engine/state/pipettes.py | 4 ++++ .../protocol_engine/commands/test_drop_tip.py | 14 +++++++++++++- .../resources/test_pipette_data_provider.py | 1 + .../protocol_engine/state/test_geometry_view.py | 11 +++++++++-- 7 files changed, 48 insertions(+), 4 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 0c762358c14..f05b3c9eeda 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -31,6 +31,7 @@ from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons.protocol_api._nozzle_layout import NozzleLayout +from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType from ..instrument import AbstractInstrument from .well import WellCore @@ -574,6 +575,11 @@ def get_dispense_flow_rate(self, rate: float = 1.0) -> float: def get_blow_out_flow_rate(self, rate: float = 1.0) -> float: return self._blow_out_flow_rate * rate + def get_nozzle_configuration(self) -> NozzleConfigurationType: + return self._engine_client.state.pipettes.get_nozzle_layout_type( + self._pipette_id + ) + def set_flow_rate( self, aspirate: Optional[float] = None, diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip.py b/api/src/opentrons/protocol_engine/commands/drop_tip.py index 90b0c04484b..923c384e630 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip.py @@ -82,8 +82,14 @@ async def execute(self, params: DropTipParams) -> DropTipResult: else: well_location = params.wellLocation + is_partially_configured = self._state_view.pipettes.get_is_partially_configured( + pipette_id=pipette_id + ) tip_drop_location = self._state_view.geometry.get_checked_tip_drop_location( - pipette_id=pipette_id, labware_id=labware_id, well_location=well_location + pipette_id=pipette_id, + labware_id=labware_id, + well_location=well_location, + partially_configured=is_partially_configured, ) position = await self._movement_handler.move_to_well( diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 4051d2383c9..4b5ba2adc07 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -416,12 +416,20 @@ def get_checked_tip_drop_location( pipette_id: str, labware_id: str, well_location: DropTipWellLocation, + partially_configured: bool = False, ) -> WellLocation: """Get tip drop location given labware and hardware pipette. This makes sure that the well location has an appropriate origin & offset if one is not already set previously. """ + if ( + self._labware.get_definition(labware_id).parameters.isTiprack + and partially_configured + ): + raise errors.UnexpectedProtocolError( + "Cannot return tip to a tiprack while the pipette is configured for partial tip." + ) if well_location.origin != DropTipWellOrigin.DEFAULT: return WellLocation( origin=WellOrigin(well_location.origin.value), diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index 4d1f7278971..6529433e0e8 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -623,3 +623,7 @@ def get_nozzle_layout_type(self, pipette_id: str) -> NozzleConfigurationType: return nozzle_map_for_pipette.configuration else: return NozzleConfigurationType.FULL + + def get_is_partially_configured(self, pipette_id: str) -> bool: + """Determine if the provided pipette is partially configured.""" + return self.get_nozzle_layout_type(pipette_id) != NozzleConfigurationType.FULL diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py index 7551c67ea25..4a3c547c07a 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py @@ -86,11 +86,16 @@ async def test_drop_tip_implementation( homeAfter=True, ) + decoy.when( + mock_state_view.pipettes.get_is_partially_configured(pipette_id="abc") + ).then_return(False) + decoy.when( mock_state_view.geometry.get_checked_tip_drop_location( pipette_id="abc", labware_id="123", well_location=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + partially_configured=False, ) ).then_return(WellLocation(offset=WellOffset(x=4, y=5, z=6))) @@ -142,9 +147,16 @@ async def test_drop_tip_with_alternating_locations( ) ).then_return(drop_location) + decoy.when( + mock_state_view.pipettes.get_is_partially_configured(pipette_id="abc") + ).then_return(False) + decoy.when( mock_state_view.geometry.get_checked_tip_drop_location( - pipette_id="abc", labware_id="123", well_location=drop_location + pipette_id="abc", + labware_id="123", + well_location=drop_location, + partially_configured=False, ) ).then_return(WellLocation(offset=WellOffset(x=4, y=5, z=6))) diff --git a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py index 7478e34f131..444ced17857 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py @@ -149,6 +149,7 @@ def test_load_virtual_pipette_nozzle_layout( ) result = subject_instance.get_nozzle_layout_for_pipette("my-96-pipette") assert result.configuration.value == "ROW" + subject_instance.configure_virtual_pipette_nozzle_layout( "my-96-pipette", "p1000_96_v3.5", "A1", "A1" ) diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 86e74a88985..696d02ca5d9 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -1116,15 +1116,22 @@ def test_get_tip_drop_location_with_non_tiprack( ) -def test_get_tip_drop_explicit_location(subject: GeometryView) -> None: +def test_get_tip_drop_explicit_location( + decoy: Decoy, + labware_view: LabwareView, + subject: GeometryView, + tip_rack_def: LabwareDefinition, +) -> None: """It should pass the location through if origin is not WellOrigin.DROP_TIP.""" + decoy.when(labware_view.get_definition("tip-rack-id")).then_return(tip_rack_def) + input_location = DropTipWellLocation( origin=DropTipWellOrigin.TOP, offset=WellOffset(x=1, y=2, z=3), ) result = subject.get_checked_tip_drop_location( - pipette_id="pipette-id", labware_id="labware-id", well_location=input_location + pipette_id="pipette-id", labware_id="tip-rack-id", well_location=input_location ) assert result == WellLocation( From 3fc76f7ac243aabff49a25661cefcf2586d720c4 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 16 Nov 2023 14:59:17 -0500 Subject: [PATCH 33/46] fix(app): fix a merge error from 1.0.0 (#14006) --- app/src/organisms/CommandText/PipettingCommandText.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/organisms/CommandText/PipettingCommandText.tsx b/app/src/organisms/CommandText/PipettingCommandText.tsx index b6e8870c83c..2ac59c1a788 100644 --- a/app/src/organisms/CommandText/PipettingCommandText.tsx +++ b/app/src/organisms/CommandText/PipettingCommandText.tsx @@ -14,9 +14,10 @@ import { getFinalLabwareLocation, getWellRange, } from './utils' -import type { PipetteName } from '@opentrons/shared-data' - -import type { PipettingRunTimeCommand } from '@opentrons/shared-data' +import type { + PipetteName, + PipettingRunTimeCommand, +} from '@opentrons/shared-data' interface PipettingCommandTextProps { command: PipettingRunTimeCommand From 780a62f518adcd3cde6be4810fd1bb15f05febb6 Mon Sep 17 00:00:00 2001 From: Andy Sigler Date: Thu, 16 Nov 2023 15:41:07 -0500 Subject: [PATCH 34/46] fix(shared-data): Gripper PCR plate tunings from hardware testing (#13651) --- .../protocol_engine/state/test_labware_view.py | 2 +- .../protocol_engine/state/test_module_view.py | 4 ++-- shared-data/deck/definitions/3/ot3_standard.json | 2 +- shared-data/deck/definitions/4/ot3_standard.json | 2 +- .../2.json | 2 +- .../definitions/2/opentrons_96_pcr_adapter/1.json | 14 ++++++++++++++ .../2/opentrons_96_well_aluminum_block/1.json | 14 ++++++++++++++ .../2.json | 2 +- .../module/definitions/3/heaterShakerModuleV1.json | 2 +- .../module/definitions/3/magneticBlockV1.json | 2 +- .../module/definitions/3/temperatureModuleV2.json | 2 +- .../module/definitions/3/thermocyclerModuleV2.json | 2 +- 12 files changed, 39 insertions(+), 11 deletions(-) diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view.py b/api/tests/opentrons/protocol_engine/state/test_labware_view.py index d5b94adfbf5..7c4f5905540 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view.py @@ -1349,7 +1349,7 @@ def test_get_deck_gripper_offsets(ot3_standard_deck_def: DeckDefinitionV4) -> No assert subject.get_deck_default_gripper_offsets() == LabwareMovementOffsetData( pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), - dropOffset=LabwareOffsetVector(x=0, y=0, z=-0.25), + dropOffset=LabwareOffsetVector(x=0, y=0, z=-0.75), ) diff --git a/api/tests/opentrons/protocol_engine/state/test_module_view.py b/api/tests/opentrons/protocol_engine/state/test_module_view.py index 5b83cda94f0..d225b64ee61 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_view.py @@ -1756,14 +1756,14 @@ def test_is_edge_move_unsafe( lazy_fixture("thermocycler_v2_def"), LabwareMovementOffsetData( pickUpOffset=LabwareOffsetVector(x=0, y=0, z=4.6), - dropOffset=LabwareOffsetVector(x=0, y=0, z=4.6), + dropOffset=LabwareOffsetVector(x=0, y=0, z=5.6), ), ), ( lazy_fixture("heater_shaker_v1_def"), LabwareMovementOffsetData( pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), - dropOffset=LabwareOffsetVector(x=0, y=0, z=0.5), + dropOffset=LabwareOffsetVector(x=0, y=0, z=1.0), ), ), ( diff --git a/shared-data/deck/definitions/3/ot3_standard.json b/shared-data/deck/definitions/3/ot3_standard.json index 775ba50960f..bead3586dba 100644 --- a/shared-data/deck/definitions/3/ot3_standard.json +++ b/shared-data/deck/definitions/3/ot3_standard.json @@ -982,7 +982,7 @@ "dropOffset": { "x": 0, "y": 0, - "z": -0.25 + "z": -0.75 } } } diff --git a/shared-data/deck/definitions/4/ot3_standard.json b/shared-data/deck/definitions/4/ot3_standard.json index a01ee27aa92..5d5441a8fcf 100644 --- a/shared-data/deck/definitions/4/ot3_standard.json +++ b/shared-data/deck/definitions/4/ot3_standard.json @@ -590,7 +590,7 @@ "dropOffset": { "x": 0, "y": 0, - "z": -0.25 + "z": -0.75 } } } diff --git a/shared-data/labware/definitions/2/armadillo_96_wellplate_200ul_pcr_full_skirt/2.json b/shared-data/labware/definitions/2/armadillo_96_wellplate_200ul_pcr_full_skirt/2.json index f0704d43cca..dec71b2b4e1 100644 --- a/shared-data/labware/definitions/2/armadillo_96_wellplate_200ul_pcr_full_skirt/2.json +++ b/shared-data/labware/definitions/2/armadillo_96_wellplate_200ul_pcr_full_skirt/2.json @@ -55,7 +55,7 @@ "z": 10.7 } }, - "gripForce": 15, + "gripForce": 9, "gripHeightFromLabwareBottom": 10, "ordering": [ ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], diff --git a/shared-data/labware/definitions/2/opentrons_96_pcr_adapter/1.json b/shared-data/labware/definitions/2/opentrons_96_pcr_adapter/1.json index 9447d404d0c..459922d3f22 100644 --- a/shared-data/labware/definitions/2/opentrons_96_pcr_adapter/1.json +++ b/shared-data/labware/definitions/2/opentrons_96_pcr_adapter/1.json @@ -1014,5 +1014,19 @@ "x": 8.5, "y": 5.5, "z": 0 + }, + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": 1.0 + } + } } } diff --git a/shared-data/labware/definitions/2/opentrons_96_well_aluminum_block/1.json b/shared-data/labware/definitions/2/opentrons_96_well_aluminum_block/1.json index 01db61ae047..2eadb732440 100644 --- a/shared-data/labware/definitions/2/opentrons_96_well_aluminum_block/1.json +++ b/shared-data/labware/definitions/2/opentrons_96_well_aluminum_block/1.json @@ -1014,5 +1014,19 @@ "x": 0, "y": 0, "z": 0 + }, + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": 1.0 + } + } } } diff --git a/shared-data/labware/definitions/2/opentrons_96_wellplate_200ul_pcr_full_skirt/2.json b/shared-data/labware/definitions/2/opentrons_96_wellplate_200ul_pcr_full_skirt/2.json index fb6420bac45..4e8314698aa 100644 --- a/shared-data/labware/definitions/2/opentrons_96_wellplate_200ul_pcr_full_skirt/2.json +++ b/shared-data/labware/definitions/2/opentrons_96_wellplate_200ul_pcr_full_skirt/2.json @@ -55,7 +55,7 @@ "z": 10.7 } }, - "gripForce": 15, + "gripForce": 9, "gripHeightFromLabwareBottom": 10, "ordering": [ ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], diff --git a/shared-data/module/definitions/3/heaterShakerModuleV1.json b/shared-data/module/definitions/3/heaterShakerModuleV1.json index 63d6dd31998..30164bd8607 100644 --- a/shared-data/module/definitions/3/heaterShakerModuleV1.json +++ b/shared-data/module/definitions/3/heaterShakerModuleV1.json @@ -43,7 +43,7 @@ "dropOffset": { "x": 0, "y": 0, - "z": 0.5 + "z": 1.0 } } }, diff --git a/shared-data/module/definitions/3/magneticBlockV1.json b/shared-data/module/definitions/3/magneticBlockV1.json index 2dea6a9d2ca..a1fd0a39248 100644 --- a/shared-data/module/definitions/3/magneticBlockV1.json +++ b/shared-data/module/definitions/3/magneticBlockV1.json @@ -38,7 +38,7 @@ "dropOffset": { "x": 0, "y": 0, - "z": 0.5 + "z": 1.0 } } }, diff --git a/shared-data/module/definitions/3/temperatureModuleV2.json b/shared-data/module/definitions/3/temperatureModuleV2.json index cc16e0b1ec4..7e67659eba7 100644 --- a/shared-data/module/definitions/3/temperatureModuleV2.json +++ b/shared-data/module/definitions/3/temperatureModuleV2.json @@ -41,7 +41,7 @@ "dropOffset": { "x": 0, "y": 0, - "z": 0.5 + "z": 1.0 } } }, diff --git a/shared-data/module/definitions/3/thermocyclerModuleV2.json b/shared-data/module/definitions/3/thermocyclerModuleV2.json index 9c7965eb49a..531890def74 100644 --- a/shared-data/module/definitions/3/thermocyclerModuleV2.json +++ b/shared-data/module/definitions/3/thermocyclerModuleV2.json @@ -44,7 +44,7 @@ "dropOffset": { "x": 0, "y": 0, - "z": 4.6 + "z": 5.6 } } }, From 76cb53be5e9b64158965138f0f2b64fd5ce92e58 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:56:02 -0500 Subject: [PATCH 35/46] feat(components): render 4th column slot label for staging area slots (#13969) closes RAUT-782 * feat(components): render 4th column slot label if staging areas present in deck config Creates a 4th column in SlotLabels component to be rendered if used for coordinates for slot positions AND at least one staging area fixture exists in deck config. Note that SlotLabels do not render 4th column when rendered as coordinates for fixture positions --- .../src/hardware-sim/BaseDeck/BaseDeck.tsx | 9 ++++- .../src/hardware-sim/Deck/SlotLabels.tsx | 36 +++++++++++++++---- .../hardware-sim/DeckConfigurator/index.tsx | 6 +++- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx index 30a2d8d37ac..5e0be0fcc66 100644 --- a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx +++ b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx @@ -121,7 +121,14 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { ) : ( <> {showSlotLabels ? ( - + 0 || + wasteChuteStagingAreaFixtures.length > 0 + } + /> ) : null} {singleSlotFixtures.map(fixture => ( { + const widthSmallRem = 10.5 + const widthLargeRem = 15.25 + return robotType === FLEX_ROBOT_TYPE ? ( <> + {show4thColumn ? ( + + + + ) : null} diff --git a/components/src/hardware-sim/DeckConfigurator/index.tsx b/components/src/hardware-sim/DeckConfigurator/index.tsx index e468cb6d376..b7c4940837e 100644 --- a/components/src/hardware-sim/DeckConfigurator/index.tsx +++ b/components/src/hardware-sim/DeckConfigurator/index.tsx @@ -125,7 +125,11 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { fixtureLocation={cutoutId} /> ))} - + 0} + /> {children} ) From 5ec4cb387461c8276449a648c1c85900099d6847 Mon Sep 17 00:00:00 2001 From: Jamey H Date: Thu, 16 Nov 2023 15:59:34 -0500 Subject: [PATCH 36/46] fix(app): fix double rendering labware in basedeck component (#14011) Closes RQA-1899 Adds logic to prevent duplicate rendering of labware if it has already been rendered on top of a module. --- app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx b/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx index 6fdf7b5b79b..7c469acdfcf 100644 --- a/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx +++ b/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx @@ -112,7 +112,9 @@ export const PrepareSpace = (props: PrepareSpaceProps): JSX.Element | null => { labwareLocation: location, definition: labwareDef, }, - ]} + ].filter( + () => !('moduleModel' in location && location.moduleModel != null) + )} deckConfig={deckConfig} /> From 2c83a1943a548f61aff786edbfc927ce2318dd06 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Thu, 16 Nov 2023 16:02:54 -0500 Subject: [PATCH 37/46] Add back in the aexit hook but start the server as an orphaned process instead so python releases everything before the server tries to lock it (#14010) --- .../hardware_testing/opentrons_api/helpers_ot3.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py index 6652a174a73..cd442e3e170 100644 --- a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py +++ b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py @@ -4,10 +4,10 @@ from datetime import datetime from enum import Enum from math import pi -from subprocess import run +from subprocess import run, Popen from time import time from typing import Callable, Coroutine, Dict, List, Optional, Tuple, Union - +import atexit from opentrons_hardware.drivers.can_bus import DriverSettings, build, CanMessenger from opentrons_hardware.drivers.can_bus import settings as can_bus_settings from opentrons_hardware.firmware_bindings.constants import SensorId @@ -77,6 +77,15 @@ def stop_server_ot3() -> None: """Stop opentrons-robot-server on the OT3.""" print('Stopping "opentrons-robot-server"...') run(["systemctl", "stop", "opentrons-robot-server"]) + atexit.register(restart_server_ot3) + + +def restart_server_ot3() -> None: + """Start opentrons-robot-server on the OT3.""" + print('Starting "opentrons-robot-server"...') + Popen( + ["systemctl", "restart", "opentrons-robot-server", "&"], + ) def start_server_ot3() -> None: From 09d48ceb4dfa06b2013eef3f758aca73c5fb40ae Mon Sep 17 00:00:00 2001 From: Andy Sigler Date: Thu, 16 Nov 2023 16:06:46 -0500 Subject: [PATCH 38/46] fix(api): InstrumentContext.mix should not jump up after each dispense (#14004) * api >=2.16 sets push-out to 0 for dispenses during a mix --------- Co-authored-by: Seth Foster --- api/src/opentrons/protocol_api/instrument_context.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 64cef355d7f..e41996058cf 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2,7 +2,7 @@ import logging from contextlib import nullcontext -from typing import Any, List, Optional, Sequence, Union, cast +from typing import Any, List, Optional, Sequence, Union, cast, Dict from opentrons_shared_data.errors.exceptions import ( CommandPreconditionViolated, CommandParameterLimitViolated, @@ -435,6 +435,10 @@ def mix( c_vol = self._core.get_available_volume() if not volume else volume + dispense_kwargs: Dict[str, Any] = {} + if self.api_version >= APIVersion(2, 16): + dispense_kwargs["push_out"] = 0.0 + with publisher.publish_context( broker=self.broker, command=cmds.mix( @@ -446,7 +450,7 @@ def mix( ): self.aspirate(volume, location, rate) while repetitions - 1 > 0: - self.dispense(volume, rate=rate) + self.dispense(volume, rate=rate, **dispense_kwargs) self.aspirate(volume, rate=rate) repetitions -= 1 self.dispense(volume, rate=rate) From c65bf57dbe1d3d24ccbab8a9e04020c8de4cc03b Mon Sep 17 00:00:00 2001 From: Brent Hagen Date: Thu, 16 Nov 2023 17:18:00 -0500 Subject: [PATCH 39/46] fix(app): render compatible fixture in protocol setup fixture list when conflicted (#14008) picks the fist compatible fixture to render name/image when existing configured fixture is not compatible with what is required by the protocol. tweaks trash bin text color because design mismatch was noticed. closes RAUT-863 --- .../SetupModuleAndDeck/SetupFixtureList.tsx | 10 ++++++++-- .../ProtocolSetupModulesAndDeck/FixtureTable.tsx | 4 ++-- .../BaseDeck/WasteChuteStagingAreaFixture.tsx | 1 - components/src/hardware-sim/Deck/FlexTrash.tsx | 6 +++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx index 80f720c51fe..3d6a71a785c 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx @@ -168,7 +168,11 @@ export function FixtureListItem({ ) : null} @@ -176,7 +180,9 @@ export function FixtureListItem({ css={TYPOGRAPHY.pSemiBold} marginLeft={SPACING.spacing20} > - {getFixtureDisplayName(cutoutFixtureId)} + {isCurrentFixtureCompatible + ? getFixtureDisplayName(cutoutFixtureId) + : getFixtureDisplayName(compatibleCutoutFixtureIds?.[0])} - {cutoutFixtureId != null + {cutoutFixtureId != null && isCurrentFixtureCompatible ? getFixtureDisplayName(cutoutFixtureId) - : null} + : getFixtureDisplayName(compatibleCutoutFixtureIds?.[0])} diff --git a/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx b/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx index 44ec32d8c2b..564db96c5fb 100644 --- a/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx +++ b/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx @@ -48,7 +48,6 @@ export function WasteChuteStagingAreaFixture( } return ( - // TODO: render a "Waste chute" foreign object similar to FlexTrash {rotateDegrees === '180' ? ( {rotateDegrees === '0' ? ( - + Trash bin ) : null} From 88f721b478990f9b467d9d58462a1dd1f6c7c82d Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Thu, 16 Nov 2023 17:20:32 -0500 Subject: [PATCH 40/46] feat(robot-server): Load endpoint supplied deck configuration into protocol engine (#13971) Provide deck configuration to protocol engine from robot-server --- api/src/opentrons/cli/analyze.py | 2 +- api/src/opentrons/execute.py | 4 +- .../protocol_engine/actions/actions.py | 3 +- .../protocol_engine/create_protocol_engine.py | 7 +-- .../protocol_engine/protocol_engine.py | 5 +- .../state/addressable_areas.py | 31 ++++++---- .../opentrons/protocol_engine/state/state.py | 4 +- api/src/opentrons/protocol_engine/types.py | 5 +- .../protocol_runner/protocol_runner.py | 16 ++++-- api/src/opentrons/simulate.py | 4 +- .../state/test_command_store.py | 56 +++++++++++++++---- .../state/test_command_view.py | 20 +++++-- .../protocol_engine/state/test_state_store.py | 12 +++- .../opentrons/protocol_engine/test_plugins.py | 4 +- .../protocol_engine/test_protocol_engine.py | 32 ++++++++--- .../smoke_tests/test_legacy_command_mapper.py | 2 +- .../smoke_tests/test_legacy_custom_labware.py | 2 +- .../test_legacy_module_commands.py | 2 +- .../smoke_tests/test_protocol_runner.py | 8 +-- .../protocol_runner/test_protocol_runner.py | 16 +++--- .../g_code_parsing/g_code_engine.py | 4 +- .../robot_server/deck_configuration/router.py | 2 +- .../robot_server/deck_configuration/store.py | 8 +++ .../maintenance_engine_store.py | 4 ++ .../maintenance_run_data_manager.py | 4 ++ .../maintenance_runs/router/base_router.py | 11 ++++ .../protocols/protocol_analyzer.py | 4 +- .../robot_server/runs/engine_store.py | 3 + .../runs/router/actions_router.py | 16 ++++++ .../robot_server/runs/router/base_router.py | 12 ++++ .../robot_server/runs/run_controller.py | 19 +++++-- .../robot_server/runs/run_data_manager.py | 4 ++ .../tests/maintenance_runs/router/conftest.py | 7 +++ .../router/test_base_router.py | 10 +++- .../maintenance_runs/test_engine_store.py | 2 +- .../maintenance_runs/test_run_data_manager.py | 6 ++ .../tests/protocols/test_protocol_analyzer.py | 6 +- robot-server/tests/runs/router/conftest.py | 7 +++ .../tests/runs/router/test_actions_router.py | 13 +++++ .../tests/runs/router/test_base_router.py | 20 ++++++- robot-server/tests/runs/test_engine_store.py | 46 +++++++++++---- .../tests/runs/test_run_controller.py | 13 +++-- .../tests/runs/test_run_data_manager.py | 20 ++++++- 43 files changed, 370 insertions(+), 106 deletions(-) diff --git a/api/src/opentrons/cli/analyze.py b/api/src/opentrons/cli/analyze.py index 107da30f734..4ee9f6507af 100644 --- a/api/src/opentrons/cli/analyze.py +++ b/api/src/opentrons/cli/analyze.py @@ -79,7 +79,7 @@ async def _analyze( runner = await create_simulating_runner( robot_type=protocol_source.robot_type, protocol_config=protocol_source.config ) - analysis = await runner.run(protocol_source) + analysis = await runner.run(deck_configuration=[], protocol_source=protocol_source) if json_output: results = AnalyzeResults.construct( diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py index 4714a72a15b..7ce3a95d288 100644 --- a/api/src/opentrons/execute.py +++ b/api/src/opentrons/execute.py @@ -627,7 +627,9 @@ async def run(protocol_source: ProtocolSource) -> None: try: # TODO(mm, 2023-06-30): This will home and drop tips at the end, which is not how # things have historically behaved with PAPIv2.13 and older or JSONv5 and older. - result = await protocol_runner.run(protocol_source) + result = await protocol_runner.run( + deck_configuration=[], protocol_source=protocol_source + ) finally: unsubscribe() diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index 64e4a5a1fad..8dffc7f012c 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -15,7 +15,7 @@ from opentrons_shared_data.errors import EnumeratedError from ..commands import Command, CommandCreate, CommandPrivateResult -from ..types import LabwareOffsetCreate, ModuleDefinition, Liquid +from ..types import LabwareOffsetCreate, ModuleDefinition, Liquid, DeckConfigurationType @dataclass(frozen=True) @@ -23,6 +23,7 @@ class PlayAction: """Start or resume processing commands in the engine.""" requested_at: datetime + deck_configuration: Optional[DeckConfigurationType] class PauseSource(str, Enum): diff --git a/api/src/opentrons/protocol_engine/create_protocol_engine.py b/api/src/opentrons/protocol_engine/create_protocol_engine.py index d6a10fada2b..cb94dfef918 100644 --- a/api/src/opentrons/protocol_engine/create_protocol_engine.py +++ b/api/src/opentrons/protocol_engine/create_protocol_engine.py @@ -10,10 +10,7 @@ from .protocol_engine import ProtocolEngine from .resources import DeckDataProvider, ModuleDataProvider from .state import Config, StateStore -from .types import PostRunHardwareState - -# TODO move this type to a better location -from .state.addressable_areas import DeckConfiguration +from .types import PostRunHardwareState, DeckConfigurationType # TODO(mm, 2023-06-16): Arguably, this not being a context manager makes us prone to forgetting to @@ -22,7 +19,7 @@ async def create_protocol_engine( hardware_api: HardwareControlAPI, config: Config, load_fixed_trash: bool = False, - deck_configuration: typing.Optional[DeckConfiguration] = None, + deck_configuration: typing.Optional[DeckConfigurationType] = None, ) -> ProtocolEngine: """Create a ProtocolEngine instance. diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 857d787dcd4..a04f24d347a 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -24,6 +24,7 @@ Liquid, HexColor, PostRunHardwareState, + DeckConfigurationType, ) from .execution import ( QueueWorker, @@ -133,13 +134,13 @@ def add_plugin(self, plugin: AbstractPlugin) -> None: """Add a plugin to the engine to customize behavior.""" self._plugin_starter.start(plugin) - def play(self) -> None: + def play(self, deck_configuration: Optional[DeckConfigurationType] = None) -> None: """Start or resume executing commands in the queue.""" requested_at = self._model_utils.get_timestamp() # TODO(mc, 2021-08-05): if starting, ensure plungers motors are # homed if necessary action = self._state_store.commands.validate_action_allowed( - PlayAction(requested_at=requested_at) + PlayAction(requested_at=requested_at, deck_configuration=deck_configuration) ) self._action_dispatcher.dispatch(action) diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index 674923b3485..04abca05a2e 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -24,8 +24,9 @@ AddressableAreaLocation, AddressableArea, PotentialCutoutFixture, + DeckConfigurationType, ) -from ..actions import Action, UpdateCommandAction +from ..actions import Action, UpdateCommandAction, PlayAction from .config import Config from .abstract_store import HasState, HandlesActions @@ -69,7 +70,7 @@ class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions): def __init__( self, - deck_configuration: DeckConfiguration, + deck_configuration: DeckConfigurationType, config: Config, deck_definition: DeckDefinitionV4, ) -> None: @@ -77,14 +78,12 @@ def __init__( if config.use_simulated_deck_config: loaded_addressable_areas_by_name = {} else: - addressable_areas = self._get_addressable_areas_from_deck_configuration( - deck_configuration, - deck_definition, + loaded_addressable_areas_by_name = ( + self._get_addressable_areas_from_deck_configuration( + deck_configuration, + deck_definition, + ) ) - loaded_addressable_areas_by_name = { - area.area_name: area for area in addressable_areas - } - self._state = AddressableAreaState( loaded_addressable_areas_by_name=loaded_addressable_areas_by_name, potential_cutout_fixtures_by_cutout_id={}, @@ -96,7 +95,15 @@ def handle_action(self, action: Action) -> None: """Modify state in reaction to an action.""" if isinstance(action, UpdateCommandAction): self._handle_command(action.command) - # TODO have an action to reload the deck configuration. + if isinstance(action, PlayAction): + current_state = self._state + if action.deck_configuration is not None: + self._state.loaded_addressable_areas_by_name = ( + self._get_addressable_areas_from_deck_configuration( + deck_config=action.deck_configuration, + deck_definition=current_state.deck_definition, + ) + ) def _handle_command(self, command: Command) -> None: """Modify state in reaction to a command.""" @@ -120,7 +127,7 @@ def _handle_command(self, command: Command) -> None: @staticmethod def _get_addressable_areas_from_deck_configuration( deck_config: DeckConfiguration, deck_definition: DeckDefinitionV4 - ) -> List[AddressableArea]: + ) -> Dict[str, AddressableArea]: """Load all provided addressable areas with a valid deck configuration.""" # TODO uncomment once execute is hooked up with this properly # assert ( @@ -144,7 +151,7 @@ def _get_addressable_areas_from_deck_configuration( deck_definition, ) ) - return addressable_areas + return {area.area_name: area for area in addressable_areas} def _check_location_is_addressable_area( self, location: Union[DeckSlotLocation, AddressableAreaLocation, str] diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index 63f7d14db74..5a273085070 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -18,7 +18,6 @@ AddressableAreaState, AddressableAreaStore, AddressableAreaView, - DeckConfiguration, ) from .labware import LabwareState, LabwareStore, LabwareView from .pipettes import PipetteState, PipetteStore, PipetteView @@ -29,6 +28,7 @@ from .motion import MotionView from .config import Config from .state_summary import StateSummary +from ..types import DeckConfigurationType ReturnT = TypeVar("ReturnT") @@ -145,7 +145,7 @@ def __init__( is_door_open: bool, change_notifier: Optional[ChangeNotifier] = None, module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None, - deck_configuration: Optional[DeckConfiguration] = None, + deck_configuration: Optional[DeckConfigurationType] = None, ) -> None: """Initialize a StateStore and its substores. diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index fd13af9e6c2..0ddd8ae96c0 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -5,7 +5,7 @@ from enum import Enum from dataclasses import dataclass from pydantic import BaseModel, Field, validator -from typing import Optional, Union, List, Dict, Any, NamedTuple +from typing import Optional, Union, List, Dict, Any, NamedTuple, Tuple from typing_extensions import Literal, TypeGuard from opentrons_shared_data.pipette.dev_types import PipetteNameType @@ -774,3 +774,6 @@ class QuadrantNozzleLayoutConfiguration(BaseModel): RowNozzleLayoutConfiguration, QuadrantNozzleLayoutConfiguration, ] + +# TODO make the below some sort of better type +DeckConfigurationType = List[Tuple[str, str]] diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py index 56669077efb..eb4225a02b9 100644 --- a/api/src/opentrons/protocol_runner/protocol_runner.py +++ b/api/src/opentrons/protocol_runner/protocol_runner.py @@ -35,7 +35,7 @@ LegacyExecutor, LegacyLoadInfo, ) -from ..protocol_engine.types import PostRunHardwareState +from ..protocol_engine.types import PostRunHardwareState, DeckConfigurationType class RunResult(NamedTuple): @@ -82,9 +82,9 @@ def was_started(self) -> bool: """ return self._protocol_engine.state_view.commands.has_been_played() - def play(self) -> None: + def play(self, deck_configuration: Optional[DeckConfigurationType] = None) -> None: """Start or resume the run.""" - self._protocol_engine.play() + self._protocol_engine.play(deck_configuration=deck_configuration) def pause(self) -> None: """Pause the run.""" @@ -104,6 +104,7 @@ async def stop(self) -> None: @abstractmethod async def run( self, + deck_configuration: DeckConfigurationType, protocol_source: Optional[ProtocolSource] = None, ) -> RunResult: """Run a given protocol to completion.""" @@ -183,6 +184,7 @@ async def load( async def run( # noqa: D102 self, + deck_configuration: DeckConfigurationType, protocol_source: Optional[ProtocolSource] = None, python_parse_mode: PythonParseMode = PythonParseMode.NORMAL, ) -> RunResult: @@ -193,7 +195,7 @@ async def run( # noqa: D102 protocol_source=protocol_source, python_parse_mode=python_parse_mode ) - self.play() + self.play(deck_configuration=deck_configuration) self._task_queue.start() await self._task_queue.join() @@ -278,6 +280,7 @@ async def load(self, protocol_source: ProtocolSource) -> None: async def run( # noqa: D102 self, + deck_configuration: DeckConfigurationType, protocol_source: Optional[ProtocolSource] = None, ) -> RunResult: # TODO(mc, 2022-01-11): move load to runner creation, remove from `run` @@ -285,7 +288,7 @@ async def run( # noqa: D102 if protocol_source: await self.load(protocol_source) - self.play() + self.play(deck_configuration=deck_configuration) self._task_queue.start() await self._task_queue.join() @@ -318,11 +321,12 @@ def prepare(self) -> None: async def run( # noqa: D102 self, + deck_configuration: DeckConfigurationType, protocol_source: Optional[ProtocolSource] = None, ) -> RunResult: assert protocol_source is None await self._hardware_api.home() - self.play() + self.play(deck_configuration=deck_configuration) self._task_queue.start() await self._task_queue.join() diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index c46259572fb..24ccaf824d5 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -900,7 +900,9 @@ async def run(protocol_source: ProtocolSource) -> _SimulateResult: scraper = _CommandScraper(stack_logger, log_level, protocol_runner.broker) with scraper.scrape(): - result = await protocol_runner.run(protocol_source) + result = await protocol_runner.run( + deck_configuration=[], protocol_source=protocol_source + ) if result.state_summary.status != EngineStatus.SUCCEEDED: raise entrypoint_util.ProtocolEngineExecuteError( diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store.py b/api/tests/opentrons/protocol_engine/state/test_command_store.py index 6a53ce46a61..a017df3b362 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_store.py @@ -681,7 +681,11 @@ def test_command_store_handles_pause_action(pause_source: PauseSource) -> None: def test_command_store_handles_play_action(pause_source: PauseSource) -> None: """It should set the running flag on play.""" subject = CommandStore(is_door_open=False, config=_make_config()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) assert subject.state == CommandState( queue_status=QueueStatus.RUNNING, @@ -705,7 +709,11 @@ def test_command_store_handles_finish_action() -> None: """It should change to a succeeded state with FinishAction.""" subject = CommandStore(is_door_open=False, config=_make_config()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) subject.handle_action(FinishAction()) assert subject.state == CommandState( @@ -730,7 +738,11 @@ def test_command_store_handles_finish_action_with_stopped() -> None: """It should change to a stopped state if FinishAction has set_run_status=False.""" subject = CommandStore(is_door_open=False, config=_make_config()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) subject.handle_action(FinishAction(set_run_status=False)) assert subject.state.run_result == RunResult.STOPPED @@ -741,7 +753,11 @@ def test_command_store_handles_stop_action(from_estop: bool) -> None: """It should mark the engine as non-gracefully stopped on StopAction.""" subject = CommandStore(is_door_open=False, config=_make_config()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) subject.handle_action(StopAction(from_estop=from_estop)) assert subject.state == CommandState( @@ -766,7 +782,11 @@ def test_command_store_cannot_restart_after_should_stop() -> None: """It should reject a play action after finish.""" subject = CommandStore(is_door_open=False, config=_make_config()) subject.handle_action(FinishAction()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) assert subject.state == CommandState( queue_status=QueueStatus.PAUSED, @@ -792,7 +812,7 @@ def test_command_store_save_started_completed_run_timestamp() -> None: start_time = datetime(year=2021, month=1, day=1) hardware_stopped_time = datetime(year=2022, month=2, day=2) - subject.handle_action(PlayAction(requested_at=start_time)) + subject.handle_action(PlayAction(requested_at=start_time, deck_configuration=[])) subject.handle_action( HardwareStoppedAction( completed_at=hardware_stopped_time, finish_error_details=None @@ -812,9 +832,9 @@ def test_timestamps_are_latched() -> None: stop_time_1 = datetime(year=2023, month=3, day=3) stop_time_2 = datetime(year=2024, month=4, day=4) - subject.handle_action(PlayAction(requested_at=play_time_1)) + subject.handle_action(PlayAction(requested_at=play_time_1, deck_configuration=[])) subject.handle_action(PauseAction(source=PauseSource.CLIENT)) - subject.handle_action(PlayAction(requested_at=play_time_2)) + subject.handle_action(PlayAction(requested_at=play_time_2, deck_configuration=[])) subject.handle_action( HardwareStoppedAction(completed_at=stop_time_1, finish_error_details=None) ) @@ -979,7 +999,11 @@ def test_command_store_ignores_stop_after_graceful_finish() -> None: """It should no-op on stop if already gracefully finished.""" subject = CommandStore(is_door_open=False, config=_make_config()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) subject.handle_action(FinishAction()) subject.handle_action(StopAction()) @@ -1005,7 +1029,11 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: """It should no-op on finish if already ungracefully stopped.""" subject = CommandStore(is_door_open=False, config=_make_config()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) subject.handle_action(StopAction()) subject.handle_action(FinishAction()) @@ -1119,7 +1147,7 @@ def test_command_store_handles_play_according_to_initial_door_state( """It should set command queue state on play action according to door state.""" subject = CommandStore(is_door_open=is_door_open, config=config) start_time = datetime(year=2021, month=1, day=1) - subject.handle_action(PlayAction(requested_at=start_time)) + subject.handle_action(PlayAction(requested_at=start_time, deck_configuration=[])) assert subject.state.queue_status == expected_queue_status assert subject.state.run_started_at == start_time @@ -1162,7 +1190,11 @@ def test_handles_door_open_and_close_event_after_play( """It should update state when door opened and closed after run is played.""" subject = CommandStore(is_door_open=False, config=config) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) subject.handle_action(DoorChangeAction(door_state=DoorState.OPEN)) assert subject.state.queue_status == expected_queue_status diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view.py b/api/tests/opentrons/protocol_engine/state/test_command_view.py index c52180996f1..985ae5050f9 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view.py @@ -326,19 +326,25 @@ class ActionAllowedSpec(NamedTuple): # play is allowed if the engine is idle ActionAllowedSpec( subject=get_command_view(queue_status=QueueStatus.SETUP), - action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)), + action=PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ), expected_error=None, ), # play is allowed if engine is idle, even if door is blocking ActionAllowedSpec( subject=get_command_view(is_door_blocking=True, queue_status=QueueStatus.SETUP), - action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)), + action=PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ), expected_error=None, ), # play is allowed if the engine is paused ActionAllowedSpec( subject=get_command_view(queue_status=QueueStatus.PAUSED), - action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)), + action=PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ), expected_error=None, ), # pause is allowed if the engine is running @@ -369,13 +375,17 @@ class ActionAllowedSpec(NamedTuple): subject=get_command_view( is_door_blocking=True, queue_status=QueueStatus.PAUSED ), - action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)), + action=PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ), expected_error=errors.RobotDoorOpenError, ), # play is disallowed if stop has been requested ActionAllowedSpec( subject=get_command_view(run_result=RunResult.STOPPED), - action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)), + action=PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ), expected_error=errors.RunStoppedError, ), # pause is disallowed if stop has been requested diff --git a/api/tests/opentrons/protocol_engine/state/test_state_store.py b/api/tests/opentrons/protocol_engine/state/test_state_store.py index 44c42fe50b2..41148eb006c 100644 --- a/api/tests/opentrons/protocol_engine/state/test_state_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_state_store.py @@ -55,7 +55,11 @@ def test_has_state(subject: StateStore) -> None: def test_state_is_immutable(subject: StateStore) -> None: """It should treat the state as immutable.""" result_1 = subject.state - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) result_2 = subject.state assert result_1 is not result_2 @@ -68,7 +72,11 @@ def test_notify_on_state_change( ) -> None: """It should notify state changes when actions are handled.""" decoy.verify(change_notifier.notify(), times=0) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) decoy.verify(change_notifier.notify(), times=1) diff --git a/api/tests/opentrons/protocol_engine/test_plugins.py b/api/tests/opentrons/protocol_engine/test_plugins.py index 0da44ab62bc..471a689e265 100644 --- a/api/tests/opentrons/protocol_engine/test_plugins.py +++ b/api/tests/opentrons/protocol_engine/test_plugins.py @@ -29,7 +29,9 @@ def test_configure( decoy: Decoy, state_view: StateView, action_dispatcher: ActionDispatcher ) -> None: """The engine should be able to configure the plugin.""" - action = PlayAction(requested_at=datetime(year=2021, month=1, day=1)) + action = PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) subject = _MyPlugin() subject._configure( diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index d8928126495..156c68aee8b 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -343,15 +343,23 @@ def test_play( ) decoy.when( state_store.commands.validate_action_allowed( - PlayAction(requested_at=datetime(year=2021, month=1, day=1)) + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) ), - ).then_return(PlayAction(requested_at=datetime(year=2022, month=2, day=2))) + ).then_return( + PlayAction( + requested_at=datetime(year=2022, month=2, day=2), deck_configuration=[] + ) + ) - subject.play() + subject.play(deck_configuration=[]) decoy.verify( action_dispatcher.dispatch( - PlayAction(requested_at=datetime(year=2022, month=2, day=2)) + PlayAction( + requested_at=datetime(year=2022, month=2, day=2), deck_configuration=[] + ) ), hardware_api.resume(HardwarePauseType.PAUSE), ) @@ -371,17 +379,25 @@ def test_play_blocked_by_door( ) decoy.when( state_store.commands.validate_action_allowed( - PlayAction(requested_at=datetime(year=2021, month=1, day=1)) + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) ), - ).then_return(PlayAction(requested_at=datetime(year=2022, month=2, day=2))) + ).then_return( + PlayAction( + requested_at=datetime(year=2022, month=2, day=2), deck_configuration=[] + ) + ) decoy.when(state_store.commands.get_is_door_blocking()).then_return(True) - subject.play() + subject.play(deck_configuration=[]) decoy.verify(hardware_api.resume(HardwarePauseType.PAUSE), times=0) decoy.verify( action_dispatcher.dispatch( - PlayAction(requested_at=datetime(year=2022, month=2, day=2)) + PlayAction( + requested_at=datetime(year=2022, month=2, day=2), deck_configuration=[] + ) ), hardware_api.pause(HardwarePauseType.PAUSE), ) diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py index c5e381e56a1..5baf821ca91 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py @@ -36,7 +36,7 @@ async def simulate_and_get_commands(protocol_file: Path) -> List[commands.Comman robot_type="OT-2 Standard", protocol_config=protocol_source.config, ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) assert result.state_summary.errors == [] assert result.state_summary.status == EngineStatus.SUCCEEDED return result.commands diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py index 32456b98af1..e9c9371fe2a 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py @@ -56,7 +56,7 @@ async def test_legacy_custom_labware(custom_labware_protocol_files: List[Path]) robot_type="OT-2 Standard", protocol_config=protocol_source.config, ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) expected_labware = LoadedLabware.construct( id=matchers.Anything(), diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py index dd7d74885fe..b7f80506593 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py @@ -64,7 +64,7 @@ async def test_runner_with_modules_in_legacy_python( robot_type="OT-2 Standard", protocol_config=protocol_source.config, ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) commands_result = result.commands assert len(commands_result) == 6 diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py index 459361a5972..d1925e3f93c 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py @@ -43,7 +43,7 @@ async def test_runner_with_python( robot_type="OT-2 Standard", protocol_config=protocol_source.config, ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) commands_result = result.commands pipettes_result = result.state_summary.pipettes labware_result = result.state_summary.labware @@ -114,7 +114,7 @@ async def test_runner_with_json(json_protocol_file: Path) -> None: subject = await create_simulating_runner( robot_type="OT-2 Standard", protocol_config=protocol_source.config ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) commands_result = result.commands pipettes_result = result.state_summary.pipettes @@ -176,7 +176,7 @@ async def test_runner_with_legacy_python(legacy_python_protocol_file: Path) -> N robot_type="OT-2 Standard", protocol_config=protocol_source.config, ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) commands_result = result.commands pipettes_result = result.state_summary.pipettes @@ -235,7 +235,7 @@ async def test_runner_with_legacy_json(legacy_json_protocol_file: Path) -> None: subject = await create_simulating_runner( robot_type="OT-2 Standard", protocol_config=protocol_source.config ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) commands_result = result.commands pipettes_result = result.state_summary.pipettes diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index 7965fc3bc1f..0e4f4ec31ca 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -210,9 +210,9 @@ async def test_play_starts_run( subject: AnyRunner, ) -> None: """It should start a protocol run with play.""" - subject.play() + subject.play(deck_configuration=[]) - decoy.verify(protocol_engine.play(), times=1) + decoy.verify(protocol_engine.play(deck_configuration=[]), times=1) @pytest.mark.parametrize( @@ -299,11 +299,11 @@ async def test_run_json_runner( ) assert json_runner_subject.was_started() is False - await json_runner_subject.run() + await json_runner_subject.run(deck_configuration=[]) assert json_runner_subject.was_started() is True decoy.verify( - protocol_engine.play(), + protocol_engine.play(deck_configuration=[]), task_queue.start(), await task_queue.join(), ) @@ -616,11 +616,11 @@ async def test_run_python_runner( ) assert legacy_python_runner_subject.was_started() is False - await legacy_python_runner_subject.run() + await legacy_python_runner_subject.run(deck_configuration=[]) assert legacy_python_runner_subject.was_started() is True decoy.verify( - protocol_engine.play(), + protocol_engine.play(deck_configuration=[]), task_queue.start(), await task_queue.join(), ) @@ -639,12 +639,12 @@ async def test_run_live_runner( ) assert live_runner_subject.was_started() is False - await live_runner_subject.run() + await live_runner_subject.run(deck_configuration=[]) assert live_runner_subject.was_started() is True decoy.verify( await hardware_api.home(), - protocol_engine.play(), + protocol_engine.play(deck_configuration=[]), task_queue.start(), await task_queue.join(), ) diff --git a/g-code-testing/g_code_parsing/g_code_engine.py b/g-code-testing/g_code_parsing/g_code_engine.py index 30aaeacff8c..3ffd2eb750d 100644 --- a/g-code-testing/g_code_parsing/g_code_engine.py +++ b/g-code-testing/g_code_parsing/g_code_engine.py @@ -176,7 +176,9 @@ async def run_protocol( hardware_api=hardware, # type: ignore ) with GCodeWatcher(emulator_settings=self._config) as watcher: - await protocol_runner.run(protocol_source=protocol_source) + await protocol_runner.run( + deck_configuration=[], protocol_source=protocol_source + ) yield GCodeProgram.from_g_code_watcher(watcher) elif isinstance(version, APIVersion) and version < APIVersion(2, 14): protocol = self._get_protocol(file_path) diff --git a/robot-server/robot_server/deck_configuration/router.py b/robot-server/robot_server/deck_configuration/router.py index 890ed5ba8cb..5474662dbaa 100644 --- a/robot-server/robot_server/deck_configuration/router.py +++ b/robot-server/robot_server/deck_configuration/router.py @@ -25,7 +25,7 @@ @router.put( - "/deck_configuration", + path="/deck_configuration", summary="Set the deck configuration", description=( "Inform the robot how its deck is physically set up." diff --git a/robot-server/robot_server/deck_configuration/store.py b/robot-server/robot_server/deck_configuration/store.py index 0bf63de3f06..9adab0a229f 100644 --- a/robot-server/robot_server/deck_configuration/store.py +++ b/robot-server/robot_server/deck_configuration/store.py @@ -4,6 +4,7 @@ from typing import List, Optional from . import models +from opentrons.protocol_engine.types import DeckConfigurationType class DeckConfigurationStore: @@ -26,3 +27,10 @@ async def get(self) -> models.DeckConfigurationResponse: return models.DeckConfigurationResponse.construct( cutoutFixtures=self._cutoutFixtures, lastUpdatedAt=self._last_updated_at ) + + def get_deck_configuration(self) -> DeckConfigurationType: + """Get the robot's current deck configuration in an expected typing.""" + deck_configuration: DeckConfigurationType = [] + for item in self._cutoutFixtures: + deck_configuration.append((item.cutoutId, item.cutoutFixtureId)) + return deck_configuration diff --git a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py index 166faf7767e..e7935b44e83 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py @@ -24,6 +24,8 @@ create_protocol_engine, ) +from opentrons.protocol_engine.types import DeckConfigurationType + class EngineConflictError(RuntimeError): """An error raised if an active engine is already initialized. @@ -125,6 +127,7 @@ async def create( run_id: str, created_at: datetime, labware_offsets: List[LabwareOffsetCreate], + deck_configuration: Optional[DeckConfigurationType] = [], ) -> StateSummary: """Create and store a ProtocolRunner and ProtocolEngine for a given Run. @@ -150,6 +153,7 @@ async def create( RobotTypeEnum.robot_literal_to_enum(self._robot_type) ), ), + deck_configuration=deck_configuration, ) # Using LiveRunner as the runner to allow for future refactor of maintenance runs diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py index ccbfc07ef32..563916a7d16 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py @@ -14,6 +14,8 @@ from .maintenance_engine_store import MaintenanceEngineStore from .maintenance_run_models import MaintenanceRun, MaintenanceRunNotFoundError +from opentrons.protocol_engine.types import DeckConfigurationType + def _build_run( run_id: str, @@ -73,6 +75,7 @@ async def create( run_id: str, created_at: datetime, labware_offsets: List[LabwareOffsetCreate], + deck_configuration: DeckConfigurationType, ) -> MaintenanceRun: """Create a new, current maintenance run. @@ -91,6 +94,7 @@ async def create( run_id=run_id, created_at=created_at, labware_offsets=labware_offsets, + deck_configuration=deck_configuration, ) return _build_run( diff --git a/robot-server/robot_server/maintenance_runs/router/base_router.py b/robot-server/robot_server/maintenance_runs/router/base_router.py index a37c1e66ba2..eb949b4bb0d 100644 --- a/robot-server/robot_server/maintenance_runs/router/base_router.py +++ b/robot-server/robot_server/maintenance_runs/router/base_router.py @@ -35,6 +35,10 @@ from ..maintenance_run_data_manager import MaintenanceRunDataManager from ..dependencies import get_maintenance_run_data_manager +from robot_server.deck_configuration.fastapi_dependencies import ( + get_deck_configuration_store, +) +from robot_server.deck_configuration.store import DeckConfigurationStore log = logging.getLogger(__name__) base_router = APIRouter() @@ -147,6 +151,9 @@ async def create_run( get_is_okay_to_create_maintenance_run ), check_estop: bool = Depends(require_estop_in_good_state), + deck_configuration_store: DeckConfigurationStore = Depends( + get_deck_configuration_store + ), ) -> PydanticResponse[SimpleBody[MaintenanceRun]]: """Create a new maintenance run. @@ -157,6 +164,7 @@ async def create_run( created_at: Timestamp to attach to created run. is_ok_to_create_maintenance_run: Verify if a maintenance run may be created if a protocol run exists. check_estop: Dependency to verify the estop is in a valid state. + deck_configuration_store: Dependency to fetch the deck configuration. """ if not is_ok_to_create_maintenance_run: raise ProtocolRunIsActive( @@ -164,10 +172,13 @@ async def create_run( ).as_error(status.HTTP_409_CONFLICT) offsets = request_body.data.labwareOffsets if request_body is not None else [] + deck_configuration = deck_configuration_store.get_deck_configuration() + run_data = await run_data_manager.create( run_id=run_id, created_at=created_at, labware_offsets=offsets, + deck_configuration=deck_configuration, ) log.info(f'Created an empty run "{run_id}"".') diff --git a/robot-server/robot_server/protocols/protocol_analyzer.py b/robot-server/robot_server/protocols/protocol_analyzer.py index cce1c5240a3..542ece91284 100644 --- a/robot-server/robot_server/protocols/protocol_analyzer.py +++ b/robot-server/robot_server/protocols/protocol_analyzer.py @@ -30,7 +30,9 @@ async def analyze( robot_type=protocol_resource.source.robot_type, protocol_config=protocol_resource.source.config, ) - result = await runner.run(protocol_resource.source) + result = await runner.run( + protocol_source=protocol_resource.source, deck_configuration=[] + ) log.info(f'Completed analysis "{analysis_id}".') diff --git a/robot-server/robot_server/runs/engine_store.py b/robot-server/robot_server/runs/engine_store.py index 558a903332d..056f8d55259 100644 --- a/robot-server/robot_server/runs/engine_store.py +++ b/robot-server/robot_server/runs/engine_store.py @@ -32,6 +32,7 @@ ) from robot_server.protocols import ProtocolResource +from opentrons.protocol_engine.types import DeckConfigurationType class EngineConflictError(RuntimeError): @@ -150,6 +151,7 @@ async def create( self, run_id: str, labware_offsets: List[LabwareOffsetCreate], + deck_configuration: DeckConfigurationType, protocol: Optional[ProtocolResource], ) -> StateSummary: """Create and store a ProtocolRunner and ProtocolEngine for a given Run. @@ -181,6 +183,7 @@ async def create( ), ), load_fixed_trash=load_fixed_trash, + deck_configuration=deck_configuration, ) runner = create_protocol_runner( protocol_engine=engine, diff --git a/robot-server/robot_server/runs/router/actions_router.py b/robot-server/robot_server/runs/router/actions_router.py index 5ac51ffd09f..5c4014a99b7 100644 --- a/robot-server/robot_server/runs/router/actions_router.py +++ b/robot-server/robot_server/runs/router/actions_router.py @@ -11,6 +11,11 @@ from robot_server.service.json_api import RequestModel, SimpleBody, PydanticResponse from robot_server.service.task_runner import TaskRunner, get_task_runner from robot_server.robot.control.dependencies import require_estop_in_good_state +from robot_server.deck_configuration.fastapi_dependencies import ( + get_deck_configuration_store, +) +from robot_server.deck_configuration.store import DeckConfigurationStore +from opentrons.protocol_engine.types import DeckConfigurationType from ..engine_store import EngineStore from ..run_store import RunStore @@ -82,12 +87,16 @@ async def get_run_controller( async def create_run_action( runId: str, request_body: RequestModel[RunActionCreate], + engine_store: EngineStore = Depends(get_engine_store), run_controller: RunController = Depends(get_run_controller), action_id: str = Depends(get_unique_id), created_at: datetime = Depends(get_current_time), maintenance_engine_store: MaintenanceEngineStore = Depends( get_maintenance_engine_store ), + deck_configuration_store: DeckConfigurationStore = Depends( + get_deck_configuration_store + ), check_estop: bool = Depends(require_estop_in_good_state), ) -> PydanticResponse[SimpleBody[RunAction]]: """Create a run control action. @@ -99,11 +108,14 @@ async def create_run_action( Arguments: runId: Run ID pulled from the URL. request_body: Input payload from the request body. + engine_store: Dependency to fetch the engine store. run_controller: Run controller bound to the given run ID. action_id: Generated ID to assign to the control action. created_at: Timestamp to attach to the control action. maintenance_engine_store: The maintenance run's EngineStore + deck_configuration_store: The deck configuration store check_estop: Dependency to verify the estop is in a valid state. + deck_configuration_store: Dependency to fetch the deck configuration. """ action_type = request_body.data.actionType if ( @@ -112,10 +124,14 @@ async def create_run_action( ): await maintenance_engine_store.clear() try: + deck_configuration: DeckConfigurationType = [] + if action_type == RunActionType.PLAY: + deck_configuration = deck_configuration_store.get_deck_configuration() action = run_controller.create_action( action_id=action_id, action_type=action_type, created_at=created_at, + action_payload=deck_configuration, ) except RunActionNotAllowedError as e: diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index 36978003c52..d68e6eb55b3 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -41,6 +41,11 @@ from ..run_data_manager import RunDataManager, RunNotCurrentError from ..dependencies import get_run_data_manager, get_run_auto_deleter +from robot_server.deck_configuration.fastapi_dependencies import ( + get_deck_configuration_store, +) +from robot_server.deck_configuration.store import DeckConfigurationStore + log = logging.getLogger(__name__) base_router = APIRouter() @@ -135,6 +140,9 @@ async def create_run( created_at: datetime = Depends(get_current_time), run_auto_deleter: RunAutoDeleter = Depends(get_run_auto_deleter), check_estop: bool = Depends(require_estop_in_good_state), + deck_configuration_store: DeckConfigurationStore = Depends( + get_deck_configuration_store + ), ) -> PydanticResponse[SimpleBody[Run]]: """Create a new run. @@ -147,11 +155,14 @@ async def create_run( run_auto_deleter: An interface to delete old resources to make room for the new run. check_estop: Dependency to verify the estop is in a valid state. + deck_configuration_store: Dependency to fetch the deck configuration. """ protocol_id = request_body.data.protocolId if request_body is not None else None offsets = request_body.data.labwareOffsets if request_body is not None else [] protocol_resource = None + deck_configuration = deck_configuration_store.get_deck_configuration() + # TODO (tz, 5-16-22): same error raised twice. # Check if we can consolidate to one place. if protocol_id is not None: @@ -170,6 +181,7 @@ async def create_run( run_id=run_id, created_at=created_at, labware_offsets=offsets, + deck_configuration=deck_configuration, protocol=protocol_resource, ) except EngineConflictError as e: diff --git a/robot-server/robot_server/runs/run_controller.py b/robot-server/robot_server/runs/run_controller.py index 6a75c1a3131..66ff5210081 100644 --- a/robot-server/robot_server/runs/run_controller.py +++ b/robot-server/robot_server/runs/run_controller.py @@ -1,7 +1,7 @@ """Control an active run with Actions.""" import logging from datetime import datetime - +from typing import Optional from opentrons.protocol_engine import ProtocolEngineError from opentrons_shared_data.errors.exceptions import RoboticsInteractionError @@ -11,6 +11,7 @@ from .run_store import RunStore from .action_models import RunAction, RunActionType +from opentrons.protocol_engine.types import DeckConfigurationType log = logging.getLogger(__name__) @@ -39,6 +40,7 @@ def create_action( action_id: str, action_type: RunActionType, created_at: datetime, + action_payload: Optional[DeckConfigurationType], ) -> RunAction: """Create a run action. @@ -69,7 +71,11 @@ def create_action( # TODO(mc, 2022-05-13): engine_store.runner.run could raise # the same errors as runner.play, but we are unable to catch them. # This unlikely to occur in production, but should be addressed. - self._task_runner.run(self._run_protocol_and_insert_result) + + self._task_runner.run( + func=self._run_protocol_and_insert_result, + deck_configuration=action_payload, + ) elif action_type == RunActionType.PAUSE: log.info(f'Pausing run "{self._run_id}".') @@ -84,10 +90,15 @@ def create_action( self._run_store.insert_action(run_id=self._run_id, action=action) + # TODO (spp, 2023-11-09): I think the response should also containt the action payload return action - async def _run_protocol_and_insert_result(self) -> None: - result = await self._engine_store.runner.run() + async def _run_protocol_and_insert_result( + self, deck_configuration: DeckConfigurationType + ) -> None: + result = await self._engine_store.runner.run( + deck_configuration=deck_configuration + ) self._run_store.update_run_state( run_id=self._run_id, summary=result.state_summary, diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index 086bd3a94a8..9954e33d8d3 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -20,6 +20,8 @@ from .run_store import RunResource, RunStore from .run_models import Run +from opentrons.protocol_engine.types import DeckConfigurationType + def _build_run( run_resource: RunResource, @@ -87,6 +89,7 @@ async def create( run_id: str, created_at: datetime, labware_offsets: List[LabwareOffsetCreate], + deck_configuration: DeckConfigurationType, protocol: Optional[ProtocolResource], ) -> Run: """Create a new, current run. @@ -115,6 +118,7 @@ async def create( state_summary = await self._engine_store.create( run_id=run_id, labware_offsets=labware_offsets, + deck_configuration=deck_configuration, protocol=protocol, ) run_resource = self._run_store.insert( diff --git a/robot-server/tests/maintenance_runs/router/conftest.py b/robot-server/tests/maintenance_runs/router/conftest.py index f71319e8dd4..e5796a31bba 100644 --- a/robot-server/tests/maintenance_runs/router/conftest.py +++ b/robot-server/tests/maintenance_runs/router/conftest.py @@ -9,6 +9,7 @@ MaintenanceRunDataManager, ) from opentrons.protocol_engine import ProtocolEngine +from robot_server.deck_configuration.store import DeckConfigurationStore @pytest.fixture() @@ -27,3 +28,9 @@ def mock_protocol_engine(decoy: Decoy) -> ProtocolEngine: def mock_maintenance_run_data_manager(decoy: Decoy) -> MaintenanceRunDataManager: """Get a mock RunDataManager.""" return decoy.mock(cls=MaintenanceRunDataManager) + + +@pytest.fixture +def mock_deck_configuration_store(decoy: Decoy) -> DeckConfigurationStore: + """Get a mock DeckConfigurationStore.""" + return decoy.mock(cls=DeckConfigurationStore) diff --git a/robot-server/tests/maintenance_runs/router/test_base_router.py b/robot-server/tests/maintenance_runs/router/test_base_router.py index 53a71b23c62..30bd6a3d28c 100644 --- a/robot-server/tests/maintenance_runs/router/test_base_router.py +++ b/robot-server/tests/maintenance_runs/router/test_base_router.py @@ -33,6 +33,8 @@ AllRunsLinks, ) +from robot_server.deck_configuration.store import DeckConfigurationStore + @pytest.fixture def labware_offset_create() -> LabwareOffsetCreate: @@ -48,6 +50,7 @@ async def test_create_run( decoy: Decoy, mock_maintenance_run_data_manager: MaintenanceRunDataManager, labware_offset_create: pe_types.LabwareOffsetCreate, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should be able to create a basic run.""" run_id = "run-id" @@ -67,11 +70,13 @@ async def test_create_run( liquids=[], ) + decoy.when(mock_deck_configuration_store.get_deck_configuration()).then_return([]) decoy.when( await mock_maintenance_run_data_manager.create( run_id=run_id, created_at=run_created_at, labware_offsets=[labware_offset_create], + deck_configuration=[], ) ).then_return(expected_response) @@ -83,6 +88,7 @@ async def test_create_run( run_id=run_id, created_at=run_created_at, is_ok_to_create_maintenance_run=True, + deck_configuration_store=mock_deck_configuration_store, ) assert result.content.data == expected_response @@ -92,10 +98,11 @@ async def test_create_run( async def test_create_maintenance_run_with_protocol_run_conflict( decoy: Decoy, mock_maintenance_run_data_manager: MaintenanceRunDataManager, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should respond with a conflict error if protocol run is active during maintenance run creation.""" created_at = datetime(year=2021, month=1, day=1) - + decoy.when(mock_deck_configuration_store.get_deck_configuration()).then_return([]) with pytest.raises(ApiError) as exc_info: await create_run( run_id="run-id", @@ -103,6 +110,7 @@ async def test_create_maintenance_run_with_protocol_run_conflict( request_body=None, run_data_manager=mock_maintenance_run_data_manager, is_ok_to_create_maintenance_run=False, + deck_configuration_store=mock_deck_configuration_store, ) assert exc_info.value.status_code == 409 assert exc_info.value.content["errors"][0]["id"] == "ProtocolRunIsActive" diff --git a/robot-server/tests/maintenance_runs/test_engine_store.py b/robot-server/tests/maintenance_runs/test_engine_store.py index 907c244e71d..d0a3ccfc1c8 100644 --- a/robot-server/tests/maintenance_runs/test_engine_store.py +++ b/robot-server/tests/maintenance_runs/test_engine_store.py @@ -106,7 +106,7 @@ async def test_clear_engine(subject: MaintenanceEngineStore) -> None: await subject.create( run_id="run-id", labware_offsets=[], created_at=datetime(2023, 5, 1) ) - await subject.runner.run() + await subject.runner.run(deck_configuration=[]) result = await subject.clear() assert subject.current_run_id is None diff --git a/robot-server/tests/maintenance_runs/test_run_data_manager.py b/robot-server/tests/maintenance_runs/test_run_data_manager.py index eed2138baa7..d7bfc3e03a6 100644 --- a/robot-server/tests/maintenance_runs/test_run_data_manager.py +++ b/robot-server/tests/maintenance_runs/test_run_data_manager.py @@ -91,6 +91,7 @@ async def test_create( run_id=run_id, labware_offsets=[], created_at=created_at, + deck_configuration=[], ) ).then_return(engine_state_summary) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -100,6 +101,7 @@ async def test_create( run_id=run_id, created_at=created_at, labware_offsets=[], + deck_configuration=[], ) assert result == MaintenanceRun( @@ -138,6 +140,7 @@ async def test_create_with_options( run_id=run_id, labware_offsets=[labware_offset], created_at=created_at, + deck_configuration=[], ) ).then_return(engine_state_summary) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -148,6 +151,7 @@ async def test_create_with_options( run_id=run_id, created_at=created_at, labware_offsets=[labware_offset], + deck_configuration=[], ) assert result == MaintenanceRun( @@ -179,6 +183,7 @@ async def test_create_engine_error( run_id, labware_offsets=[], created_at=created_at, + deck_configuration=[], ) ).then_raise(EngineConflictError("oh no")) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -190,6 +195,7 @@ async def test_create_engine_error( run_id=run_id, created_at=created_at, labware_offsets=[], + deck_configuration=[], ) diff --git a/robot-server/tests/protocols/test_protocol_analyzer.py b/robot-server/tests/protocols/test_protocol_analyzer.py index b2900b982c8..03784e62c8e 100644 --- a/robot-server/tests/protocols/test_protocol_analyzer.py +++ b/robot-server/tests/protocols/test_protocol_analyzer.py @@ -109,7 +109,11 @@ async def test_analyze( ) ).then_return(json_runner) - decoy.when(await json_runner.run(protocol_resource.source)).then_return( + decoy.when( + await json_runner.run( + deck_configuration=[], protocol_source=protocol_resource.source + ) + ).then_return( protocol_runner.RunResult( commands=[analysis_command], state_summary=StateSummary( diff --git a/robot-server/tests/runs/router/conftest.py b/robot-server/tests/runs/router/conftest.py index 77d80ce2f23..f7d1f0fead6 100644 --- a/robot-server/tests/runs/router/conftest.py +++ b/robot-server/tests/runs/router/conftest.py @@ -8,6 +8,7 @@ from robot_server.runs.engine_store import EngineStore from robot_server.runs.run_data_manager import RunDataManager from robot_server.maintenance_runs import MaintenanceEngineStore +from robot_server.deck_configuration.store import DeckConfigurationStore from opentrons.protocol_engine import ProtocolEngine @@ -52,3 +53,9 @@ def mock_run_auto_deleter(decoy: Decoy) -> RunAutoDeleter: def mock_maintenance_engine_store(decoy: Decoy) -> MaintenanceEngineStore: """Get a mock MaintenanceEngineStore interface.""" return decoy.mock(cls=MaintenanceEngineStore) + + +@pytest.fixture +def mock_deck_configuration_store(decoy: Decoy) -> DeckConfigurationStore: + """Get a mock DeckConfigurationStore.""" + return decoy.mock(cls=DeckConfigurationStore) diff --git a/robot-server/tests/runs/router/test_actions_router.py b/robot-server/tests/runs/router/test_actions_router.py index 0c9dbfdb742..669002708cf 100644 --- a/robot-server/tests/runs/router/test_actions_router.py +++ b/robot-server/tests/runs/router/test_actions_router.py @@ -15,6 +15,7 @@ from robot_server.runs.router.actions_router import create_run_action from robot_server.maintenance_runs import MaintenanceEngineStore +from robot_server.deck_configuration.store import DeckConfigurationStore @pytest.fixture @@ -27,6 +28,7 @@ async def test_create_run_action( decoy: Decoy, mock_run_controller: RunController, mock_maintenance_engine_store: MaintenanceEngineStore, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should create a run action.""" run_id = "some-run-id" @@ -39,12 +41,14 @@ async def test_create_run_action( createdAt=created_at, actionType=RunActionType.PLAY, ) + decoy.when(mock_deck_configuration_store.get_deck_configuration()).then_return([]) decoy.when(mock_maintenance_engine_store.current_run_id).then_return(None) decoy.when( mock_run_controller.create_action( action_id=action_id, action_type=action_type, created_at=created_at, + action_payload=[], ) ).then_return(expected_result) @@ -55,6 +59,7 @@ async def test_create_run_action( action_id=action_id, created_at=created_at, maintenance_engine_store=mock_maintenance_engine_store, + deck_configuration_store=mock_deck_configuration_store, ) assert result.content.data == expected_result @@ -65,6 +70,7 @@ async def test_play_action_clears_maintenance_run( decoy: Decoy, mock_run_controller: RunController, mock_maintenance_engine_store: MaintenanceEngineStore, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should clear an existing maintenance run before issuing play action.""" run_id = "some-run-id" @@ -77,12 +83,14 @@ async def test_play_action_clears_maintenance_run( createdAt=created_at, actionType=RunActionType.PLAY, ) + decoy.when(mock_deck_configuration_store.get_deck_configuration()).then_return([]) decoy.when(mock_maintenance_engine_store.current_run_id).then_return("some-id") decoy.when( mock_run_controller.create_action( action_id=action_id, action_type=action_type, created_at=created_at, + action_payload=[], ) ).then_return(expected_result) @@ -93,6 +101,7 @@ async def test_play_action_clears_maintenance_run( action_id=action_id, created_at=created_at, maintenance_engine_store=mock_maintenance_engine_store, + deck_configuration_store=mock_deck_configuration_store, ) decoy.verify(await mock_maintenance_engine_store.clear(), times=1) @@ -114,6 +123,7 @@ async def test_create_play_action_not_allowed( expected_error_id: str, expected_status_code: int, mock_maintenance_engine_store: MaintenanceEngineStore, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should 409 if the runner is not able to handle the action.""" run_id = "some-run-id" @@ -122,12 +132,14 @@ async def test_create_play_action_not_allowed( action_type = RunActionType.PLAY request_body = RequestModel(data=RunActionCreate(actionType=action_type)) + decoy.when(mock_deck_configuration_store.get_deck_configuration()).then_return([]) decoy.when(mock_maintenance_engine_store.current_run_id).then_return(None) decoy.when( mock_run_controller.create_action( action_id=action_id, action_type=action_type, created_at=created_at, + action_payload=[], ) ).then_raise(exception) @@ -139,6 +151,7 @@ async def test_create_play_action_not_allowed( action_id=action_id, created_at=created_at, maintenance_engine_store=mock_maintenance_engine_store, + deck_configuration_store=mock_deck_configuration_store, ) assert exc_info.value.status_code == expected_status_code diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index d52f2687be6..21ee705cc1a 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -39,6 +39,8 @@ update_run, ) +from robot_server.deck_configuration.store import DeckConfigurationStore + @pytest.fixture def labware_offset_create() -> LabwareOffsetCreate: @@ -55,6 +57,7 @@ async def test_create_run( mock_run_data_manager: RunDataManager, mock_run_auto_deleter: RunAutoDeleter, labware_offset_create: pe_types.LabwareOffsetCreate, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should be able to create a basic run.""" run_id = "run-id" @@ -74,12 +77,13 @@ async def test_create_run( status=pe_types.EngineStatus.IDLE, liquids=[], ) - + decoy.when(mock_deck_configuration_store.get_deck_configuration()).then_return([]) decoy.when( await mock_run_data_manager.create( run_id=run_id, created_at=run_created_at, labware_offsets=[labware_offset_create], + deck_configuration=[], protocol=None, ) ).then_return(expected_response) @@ -92,6 +96,7 @@ async def test_create_run( run_id=run_id, created_at=run_created_at, run_auto_deleter=mock_run_auto_deleter, + deck_configuration_store=mock_deck_configuration_store, ) assert result.content.data == expected_response @@ -105,6 +110,7 @@ async def test_create_protocol_run( mock_protocol_store: ProtocolStore, mock_run_data_manager: RunDataManager, mock_run_auto_deleter: RunAutoDeleter, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should be able to create a protocol run.""" run_id = "run-id" @@ -140,7 +146,7 @@ async def test_create_protocol_run( status=pe_types.EngineStatus.IDLE, liquids=[], ) - + decoy.when(mock_deck_configuration_store.get_deck_configuration()).then_return([]) decoy.when(mock_protocol_store.get(protocol_id=protocol_id)).then_return( protocol_resource ) @@ -150,6 +156,7 @@ async def test_create_protocol_run( run_id=run_id, created_at=run_created_at, labware_offsets=[], + deck_configuration=[], protocol=protocol_resource, ) ).then_return(expected_response) @@ -161,6 +168,7 @@ async def test_create_protocol_run( run_id=run_id, created_at=run_created_at, run_auto_deleter=mock_run_auto_deleter, + deck_configuration_store=mock_deck_configuration_store, ) assert result.content.data == expected_response @@ -172,16 +180,18 @@ async def test_create_protocol_run( async def test_create_protocol_run_bad_protocol_id( decoy: Decoy, mock_protocol_store: ProtocolStore, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should 404 if a protocol for a run does not exist.""" error = ProtocolNotFoundError("protocol-id") - + decoy.when(mock_deck_configuration_store.get_deck_configuration()).then_return([]) decoy.when(mock_protocol_store.get(protocol_id="protocol-id")).then_raise(error) with pytest.raises(ApiError) as exc_info: await create_run( request_body=RequestModel(data=RunCreate(protocolId="protocol-id")), protocol_store=mock_protocol_store, + deck_configuration_store=mock_deck_configuration_store, ) assert exc_info.value.status_code == 404 @@ -192,15 +202,18 @@ async def test_create_run_conflict( decoy: Decoy, mock_run_data_manager: RunDataManager, mock_run_auto_deleter: RunAutoDeleter, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should respond with a conflict error if multiple engines are created.""" created_at = datetime(year=2021, month=1, day=1) + decoy.when(mock_deck_configuration_store.get_deck_configuration()).then_return([]) decoy.when( await mock_run_data_manager.create( run_id="run-id", created_at=created_at, labware_offsets=[], + deck_configuration=[], protocol=None, ) ).then_raise(EngineConflictError("oh no")) @@ -212,6 +225,7 @@ async def test_create_run_conflict( request_body=None, run_data_manager=mock_run_data_manager, run_auto_deleter=mock_run_auto_deleter, + deck_configuration_store=mock_deck_configuration_store, ) assert exc_info.value.status_code == 409 diff --git a/robot-server/tests/runs/test_engine_store.py b/robot-server/tests/runs/test_engine_store.py index fc22e4ea900..bd8ef9b3678 100644 --- a/robot-server/tests/runs/test_engine_store.py +++ b/robot-server/tests/runs/test_engine_store.py @@ -50,7 +50,9 @@ async def json_protocol_source(tmp_path: Path) -> ProtocolSource: async def test_create_engine(subject: EngineStore) -> None: """It should create an engine for a run.""" - result = await subject.create(run_id="run-id", labware_offsets=[], protocol=None) + result = await subject.create( + run_id="run-id", labware_offsets=[], protocol=None, deck_configuration=[] + ) assert subject.current_run_id == "run-id" assert isinstance(result, StateSummary) @@ -78,6 +80,7 @@ async def test_create_engine_with_protocol( result = await subject.create( run_id="run-id", labware_offsets=[], + deck_configuration=[], protocol=protocol, ) assert subject.current_run_id == "run-id" @@ -99,7 +102,9 @@ async def test_create_engine_uses_robot_type( hardware_api=hardware_api, robot_type=robot_type, deck_type=deck_type ) - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) assert subject.engine.state_view.config.robot_type == robot_type @@ -115,6 +120,7 @@ async def test_create_engine_with_labware_offsets(subject: EngineStore) -> None: result = await subject.create( run_id="run-id", labware_offsets=[labware_offset], + deck_configuration=[], protocol=None, ) @@ -131,18 +137,24 @@ async def test_create_engine_with_labware_offsets(subject: EngineStore) -> None: async def test_archives_state_if_engine_already_exists(subject: EngineStore) -> None: """It should not create more than one engine / runner pair.""" - await subject.create(run_id="run-id-1", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id-1", labware_offsets=[], deck_configuration=[], protocol=None + ) with pytest.raises(EngineConflictError): - await subject.create(run_id="run-id-2", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id-2", labware_offsets=[], deck_configuration=[], protocol=None + ) assert subject.current_run_id == "run-id-1" async def test_clear_engine(subject: EngineStore) -> None: """It should clear a stored engine entry.""" - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) - await subject.runner.run() + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) + await subject.runner.run(deck_configuration=[]) result = await subject.clear() assert subject.current_run_id is None @@ -159,8 +171,10 @@ async def test_clear_engine_not_stopped_or_idle( subject: EngineStore, json_protocol_source: ProtocolSource ) -> None: """It should raise a conflict if the engine is not stopped.""" - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) - subject.runner.play() + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) + subject.runner.play(deck_configuration=[]) with pytest.raises(EngineConflictError): await subject.clear() @@ -168,7 +182,9 @@ async def test_clear_engine_not_stopped_or_idle( async def test_clear_idle_engine(subject: EngineStore) -> None: """It should successfully clear engine if idle (not started).""" - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) assert subject.engine is not None assert subject.runner is not None @@ -210,7 +226,9 @@ async def test_get_default_engine_robot_type( async def test_get_default_engine_current_unstarted(subject: EngineStore) -> None: """It should allow a default engine if another engine current but unstarted.""" - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) result = await subject.get_default_engine() assert isinstance(result, ProtocolEngine) @@ -218,7 +236,9 @@ async def test_get_default_engine_current_unstarted(subject: EngineStore) -> Non async def test_get_default_engine_conflict(subject: EngineStore) -> None: """It should not allow a default engine if another engine is executing commands.""" - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) subject.engine.play() with pytest.raises(EngineConflictError): @@ -227,7 +247,9 @@ async def test_get_default_engine_conflict(subject: EngineStore) -> None: async def test_get_default_engine_run_stopped(subject: EngineStore) -> None: """It allow a default engine if another engine is terminal.""" - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) await subject.engine.finish() result = await subject.get_default_engine() diff --git a/robot-server/tests/runs/test_run_controller.py b/robot-server/tests/runs/test_run_controller.py index 7387af2d912..da433043650 100644 --- a/robot-server/tests/runs/test_run_controller.py +++ b/robot-server/tests/runs/test_run_controller.py @@ -102,6 +102,7 @@ async def test_create_play_action_to_resume( action_id="some-action-id", action_type=RunActionType.PLAY, created_at=datetime(year=2021, month=1, day=1), + action_payload=[], ) assert result == RunAction( @@ -112,7 +113,7 @@ async def test_create_play_action_to_resume( decoy.verify(mock_run_store.insert_action(run_id, result), times=1) decoy.verify(mock_json_runner.play(), times=1) - decoy.verify(await mock_json_runner.run(), times=0) + decoy.verify(await mock_json_runner.run(deck_configuration=[]), times=0) async def test_create_play_action_to_start( @@ -134,6 +135,7 @@ async def test_create_play_action_to_start( action_id="some-action-id", action_type=RunActionType.PLAY, created_at=datetime(year=2021, month=1, day=1), + action_payload=[], ) assert result == RunAction( @@ -145,16 +147,16 @@ async def test_create_play_action_to_start( decoy.verify(mock_run_store.insert_action(run_id, result), times=1) background_task_captor = matchers.Captor() - decoy.verify(mock_task_runner.run(background_task_captor)) + decoy.verify(mock_task_runner.run(background_task_captor, deck_configuration=[])) - decoy.when(await mock_python_runner.run()).then_return( + decoy.when(await mock_python_runner.run(deck_configuration=[])).then_return( RunResult( commands=protocol_commands, state_summary=engine_state_summary, ) ) - await background_task_captor.value() + await background_task_captor.value(deck_configuration=[]) decoy.verify( mock_run_store.update_run_state( @@ -178,6 +180,7 @@ async def test_create_pause_action( action_id="some-action-id", action_type=RunActionType.PAUSE, created_at=datetime(year=2021, month=1, day=1), + action_payload=[], ) assert result == RunAction( @@ -203,6 +206,7 @@ async def test_create_stop_action( action_id="some-action-id", action_type=RunActionType.STOP, created_at=datetime(year=2021, month=1, day=1), + action_payload=[], ) assert result == RunAction( @@ -243,4 +247,5 @@ async def test_action_not_allowed( action_id="whatever", action_type=action_type, created_at=datetime(year=2021, month=1, day=1), + action_payload=[], ) diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index 21e7cb8072b..33e348d8854 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -121,7 +121,12 @@ async def test_create( created_at = datetime(year=2021, month=1, day=1) decoy.when( - await mock_engine_store.create(run_id=run_id, labware_offsets=[], protocol=None) + await mock_engine_store.create( + run_id=run_id, + labware_offsets=[], + protocol=None, + deck_configuration=[], + ) ).then_return(engine_state_summary) decoy.when( mock_run_store.insert( @@ -136,6 +141,7 @@ async def test_create( created_at=created_at, labware_offsets=[], protocol=None, + deck_configuration=[], ) assert result == Run( @@ -184,6 +190,7 @@ async def test_create_with_options( run_id=run_id, labware_offsets=[labware_offset], protocol=protocol, + deck_configuration=[], ) ).then_return(engine_state_summary) @@ -200,6 +207,7 @@ async def test_create_with_options( created_at=created_at, labware_offsets=[labware_offset], protocol=protocol, + deck_configuration=[], ) assert result == Run( @@ -229,7 +237,12 @@ async def test_create_engine_error( created_at = datetime(year=2021, month=1, day=1) decoy.when( - await mock_engine_store.create(run_id, labware_offsets=[], protocol=None) + await mock_engine_store.create( + run_id, + labware_offsets=[], + protocol=None, + deck_configuration=[], + ) ).then_raise(EngineConflictError("oh no")) with pytest.raises(EngineConflictError): @@ -238,6 +251,7 @@ async def test_create_engine_error( created_at=created_at, labware_offsets=[], protocol=None, + deck_configuration=[], ) decoy.verify( @@ -602,6 +616,7 @@ async def test_create_archives_existing( run_id=run_id_new, labware_offsets=[], protocol=None, + deck_configuration=[], ) ).then_return(engine_state_summary) @@ -618,6 +633,7 @@ async def test_create_archives_existing( created_at=datetime(year=2021, month=1, day=1), labware_offsets=[], protocol=None, + deck_configuration=[], ) decoy.verify( From 12a630bdc9ebef4010c8d866324e6300dea6adf9 Mon Sep 17 00:00:00 2001 From: Andy Sigler Date: Thu, 16 Nov 2023 17:27:05 -0500 Subject: [PATCH 41/46] fix(api): Aspirate, dispense, and mix can perform at 0ul (#13989) I would expect .aspirate(0) and .dispense(0) to aspirate/dispense 0 ul. Currently .aspirate(0) will aspirate the tip's max-volume, and .dispense(0) will dispense all volume currently in the tip. Looking in git history and this line was added in instrument_context.py in 2020, where the if not volume is used to determine if the volume should be defaulted: c_vol = self._core.get_available_volume() if not volume else volume I'm assuming this behavior wasn't intentional. --------- Co-authored-by: Seth Foster --- .../protocol_api/instrument_context.py | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index e41996058cf..98975147112 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -165,9 +165,15 @@ def aspirate( See :ref:`new-aspirate` for more details and examples. - :param volume: The volume to aspirate, measured in µL. If 0 or unspecified, + :param volume: The volume to aspirate, measured in µL. If unspecified, defaults to the maximum volume for the pipette and its currently attached tip. + + If ``aspirate`` is called with a volume of precisely 0, its behavior + depends on the API level of the protocol. On API levels below 2.16, + it will behave the same as a volume of ``None``/unspecified: aspirate + until the pipette is full. On API levels at or above 2.16, no liquid + will be aspirated. :type volume: int or float :param location: Tells the robot where to aspirate from. The location can be a :py:class:`.Well` or a :py:class:`.Location`. @@ -236,7 +242,10 @@ def aspirate( reject_adapter=self.api_version >= APIVersion(2, 15), ) - c_vol = self._core.get_available_volume() if not volume else volume + if self.api_version >= APIVersion(2, 16): + c_vol = self._core.get_available_volume() if volume is None else volume + else: + c_vol = self._core.get_available_volume() if not volume else volume flow_rate = self._core.get_aspirate_flow_rate(rate) with publisher.publish_context( @@ -260,7 +269,7 @@ def aspirate( return self - @requires_version(2, 0) + @requires_version(2, 0) # noqa: C901 def dispense( self, volume: Optional[float] = None, @@ -273,9 +282,15 @@ def dispense( See :ref:`new-dispense` for more details and examples. - :param volume: The volume to dispense, measured in µL. If 0 or unspecified, + :param volume: The volume to dispense, measured in µL. If unspecified, defaults to :py:attr:`current_volume`. If only a volume is passed, the pipette will dispense from its current position. + + If ``dispense`` is called with a volume of precisely 0, its behavior + depends on the API level of the protocol. On API levels below 2.16, + it will behave the same as a volume of ``None``/unspecified: dispense + all liquid in the pipette. On API levels at or above 2.16, no liquid + will be dispensed. :type volume: int or float :param location: Tells the robot where to dispense liquid held in the pipette. @@ -363,7 +378,10 @@ def dispense( reject_adapter=self.api_version >= APIVersion(2, 15), ) - c_vol = self._core.get_current_volume() if not volume else volume + if self.api_version >= APIVersion(2, 16): + c_vol = self._core.get_current_volume() if volume is None else volume + else: + c_vol = self._core.get_current_volume() if not volume else volume flow_rate = self._core.get_dispense_flow_rate(rate) @@ -403,8 +421,14 @@ def mix( See :ref:`mix` for examples. :param repetitions: Number of times to mix (default is 1). - :param volume: The volume to mix, measured in µL. If 0 or unspecified, defaults + :param volume: The volume to mix, measured in µL. If unspecified, defaults to the maximum volume for the pipette and its attached tip. + + If ``mix`` is called with a volume of precisely 0, its behavior + depends on the API level of the protocol. On API levels below 2.16, + it will behave the same as a volume of ``None``/unspecified: mix + the full working volume of the pipette. On API levels at or above 2.16, + no liquid will be mixed. :param location: The :py:class:`.Well` or :py:class:`~.types.Location` where the pipette will mix. If unspecified, the pipette will mix at its current position. @@ -433,7 +457,10 @@ def mix( if not self._core.has_tip(): raise UnexpectedTipRemovalError("mix", self.name, self.mount) - c_vol = self._core.get_available_volume() if not volume else volume + if self.api_version >= APIVersion(2, 16): + c_vol = self._core.get_available_volume() if volume is None else volume + else: + c_vol = self._core.get_available_volume() if not volume else volume dispense_kwargs: Dict[str, Any] = {} if self.api_version >= APIVersion(2, 16): From 388d1066df537efa5f75590e0d1514353de19238 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 16 Nov 2023 17:32:27 -0500 Subject: [PATCH 42/46] fix(api): MoveToAddressableArea noop not raise (#14015) This lets us test the parts of the frontend that require the command. --- .../protocol_engine/commands/move_to_addressable_area.py | 3 +-- .../protocol/models/protocol_schema_v8.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py index 883da9ff8fd..822b99e7921 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal -# from ..types import DeckPoint from .pipetting_common import ( PipetteIdMixin, MovementMixin, @@ -48,7 +47,7 @@ async def execute( self, params: MoveToAddressableAreaParams ) -> MoveToAddressableAreaResult: """Move the requested pipette to the requested addressable area.""" - raise NotImplementedError() + return MoveToAddressableAreaResult() class MoveToAddressableArea( diff --git a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py index 4147afb1149..081fe604d95 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py @@ -63,6 +63,8 @@ class Params(BaseModel): namespace: Optional[str] version: Optional[int] pushOut: Optional[float] + # schema v8 add-ons + addressableAreaName: Optional[str] class Command(BaseModel): From 0ae2f41283561c89fa95baf669677b4ec0e1c82c Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 16 Nov 2023 17:46:23 -0500 Subject: [PATCH 43/46] chore: release notes for internal-release 1.1.0 (#14012) Co-authored-by: Brian Cooper --- api/release-notes-internal.md | 32 +++++++++++++++-------- app-shell/build/release-notes-internal.md | 13 +++++---- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index 426958f061b..6bba1e17950 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -4,17 +4,27 @@ For more details about this release, please see the full [technical change log][ --- -# Internal Release 1.0.0 +# Internal Release 1.1.0 ## New Stuff In This Release -- Fixed an issue where the robot wasn't actually checking for updates; you will now correctly get prompted to update your robot from the settings tab of the ODD when an update is available, including during onboarding -- You can update the robot by putting a system update (ot3-system.zip) on a flash drive and plugging it in the front USB port, then going to robot settings -- Support for 96-channel pipettes in protocols -- Early provisional support for deck configuration and trash chutes in protocols - - -## Big Things That Don't Work Yet So Don't Report Bugs About Them - -### Robot Control -- Pipette partial tip pickup is present but not fully validated or developed yet. Partial tip pickup on 96 channel pipettes will not use correct motion parameters; using the front channel of a pipette in partial tip pickup does not work. +This is a tracking internal release coming off of the edge branch to contain rapid dev on new features for 7.1.0. Features will change drastically between successive alphas even over the course of the day. For this reason, these release notes will not be in their usual depth. + +The biggest new features, however, are +- There is a new protocol API version, 2.16, which changes how the default trash is loaded and gates features like partial tip pickup and waste chute usage: + - Protocols do not load a trash by default. To load the normal trash, load ``opentrons_1_trash_3200ml_fixed`` in slot ``A3``. + - But also you can load it in any other edge slot if you want (columns 1 and 3). + - Protocols can load trash chutes; the details of exactly how this works are still in flux. + - Protocols can configure their 96 and 8 channel pipettes to pick up only a subset of tips using ``configure_nozzle_layout``. +- Support for json protocol V8 and command V8, which adds JSON protocol support for the above features. +- ODD support for rendering the above features in protocols +- ODD support for configuring the loaded deck fixtures like trash chutes +- Labware position check now uses the calibration probe (the same one used for pipette and module calibration) instead of a tip; this should increase the accuracy of LPC. +- Support for P1000S v3.6 +- Updated liquid handling functions for all 96 channel pipettes + +## Known Issues + +- The ``MoveToAddressableArea`` command will noop. This means that all commands that use the movable trash bin will not "move to the trash bin". The command will analyze successfully. +- The deck configuration on the robot is not persistent, this means that between boots of a robot, you must PUT a deck configuration on the robot via HTTP. + diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md index 3c53342b57c..a15d877c0ab 100644 --- a/app-shell/build/release-notes-internal.md +++ b/app-shell/build/release-notes-internal.md @@ -3,7 +3,7 @@ For more details about this release, please see the full [technical changelog][] --- -# Internal Release 1.0.0 +# Internal Release 1.1.0 This is 1.0.0, an internal release for the app supporting the Opentrons Flex. @@ -11,10 +11,13 @@ This is still pretty early in the process, so some things are known not to work, ## New Stuff In This Release -- Support for running labware position check using the calibration adapter for added accuracy (note: not an automated flow, just uses the adapter instead of a tip) -- Support for 96-channel pipettes in protocols -- Early provisional support for deck configuration and trash chutes in protocols - +- There is now UI for configuring the loaded deck fixtures such as trash chutes on your Flex. +- Support for analyzing python protocol API 2.16 and JSON protocol V8 +- Labware position check now uses the calibration (the same one used for pipette and module calibration) instead of a tip; this should increase the accuracy of LPC. +- Connecting a Flex to a wifi network while the app is connected to it with USB should work now +- The app should generally be better about figuring out what kind of robot a protocol is for, and displaying the correct deck layout accordingly +## Known Issues +- Labware Renders are slightly askew towards the top right. From 59b8713f94115b75a6e3b013be44bf5cc5d3bab8 Mon Sep 17 00:00:00 2001 From: Brent Hagen Date: Thu, 16 Nov 2023 17:58:18 -0500 Subject: [PATCH 44/46] fix(app): render protocol setup labware addressableAreaName location (#14013) Co-authored-by: Brian Cooper --- api-client/src/protocols/utils.ts | 12 ++++++------ .../ProtocolRun/SetupLabware/LabwareListItem.tsx | 16 ++++++++++++---- app/src/organisms/ProtocolSetupLabware/index.tsx | 11 +++++++++++ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/api-client/src/protocols/utils.ts b/api-client/src/protocols/utils.ts index 3ed44a9053a..fefa2fab7f5 100644 --- a/api-client/src/protocols/utils.ts +++ b/api-client/src/protocols/utils.ts @@ -292,12 +292,12 @@ export function parseAllAddressableAreas( ...acc, command.params.location.addressableAreaName as AddressableAreaName, ] - } - // TODO(BC, 11/6/23): once moveToAddressableArea command exists add it back here - // else if (command.commandType === 'moveToAddressableArea') { - // ... - // } - else { + } else if ( + command.commandType === 'moveToAddressableArea' && + !acc.includes(command.params.addressableAreaName as AddressableAreaName) + ) { + return [...acc, command.params.addressableAreaName as AddressableAreaName] + } else { return acc } }, []) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx index d91a0673d32..b82c3d7b302 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx @@ -93,10 +93,18 @@ export function LabwareListItem( const { createLiveCommand } = useCreateLiveCommandMutation() const [isLatchLoading, setIsLatchLoading] = React.useState(false) const [isLatchClosed, setIsLatchClosed] = React.useState(false) - let slotInfo: string | null = - initialLocation !== 'offDeck' && 'slotName' in initialLocation - ? initialLocation.slotName - : null + + let slotInfo: string | null = null + + if (initialLocation !== 'offDeck' && 'slotName' in initialLocation) { + slotInfo = initialLocation.slotName + } else if ( + initialLocation !== 'offDeck' && + 'addressableAreaName' in initialLocation + ) { + slotInfo = initialLocation.addressableAreaName + } + let moduleDisplayName: string | null = null let extraAttentionText: JSX.Element | null = null let isCorrectHeaterShakerAttached: boolean = false diff --git a/app/src/organisms/ProtocolSetupLabware/index.tsx b/app/src/organisms/ProtocolSetupLabware/index.tsx index 65da8173326..eace12eaada 100644 --- a/app/src/organisms/ProtocolSetupLabware/index.tsx +++ b/app/src/organisms/ProtocolSetupLabware/index.tsx @@ -142,6 +142,14 @@ export function ProtocolSetupLabware({ 'slotName' in selectedLabware?.location ) { location = + } else if ( + selectedLabware != null && + typeof selectedLabware.location === 'object' && + 'addressableAreaName' in selectedLabware?.location + ) { + location = ( + + ) } else if ( selectedLabware != null && typeof selectedLabware.location === 'object' && @@ -490,6 +498,9 @@ function RowLabware({ } else if ('slotName' in initialLocation) { slotName = initialLocation.slotName location = + } else if ('addressableAreaName' in initialLocation) { + slotName = initialLocation.addressableAreaName + location = } else if (matchedModuleType != null && matchedModule?.slotName != null) { slotName = matchedModule.slotName location = ( From 18addf6954f0c9a1750a5584b290188544eaac4d Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Fri, 17 Nov 2023 09:34:41 -0500 Subject: [PATCH 45/46] feat(step-generation): update compound command creators for drop tip in waste chute (#14000) closes RAUT-854 * feat(step-generation): update drop tip in waste chute commands for transfer + consolidate If the droptip location for transfer/consolidate is the waste chute, call a utility that returns the correct commands (moveToAddressableArea > dropTipInPlace) --- .../src/__tests__/consolidate.test.ts | 39 +++++++++++++++++++ .../src/__tests__/transfer.test.ts | 36 +++++++++++++++++ .../commandCreators/compound/consolidate.ts | 29 ++++++++++---- .../src/commandCreators/compound/transfer.ts | 28 ++++++++++--- 4 files changed, 118 insertions(+), 14 deletions(-) diff --git a/step-generation/src/__tests__/consolidate.test.ts b/step-generation/src/__tests__/consolidate.test.ts index 1b1e10dd555..ad0a1d0adfb 100644 --- a/step-generation/src/__tests__/consolidate.test.ts +++ b/step-generation/src/__tests__/consolidate.test.ts @@ -22,6 +22,8 @@ import { pickUpTipHelper, SOURCE_LABWARE, AIR_GAP_META, + moveToAddressableAreaHelper, + dropTipInPlaceHelper, } from '../fixtures' import { DEST_WELL_BLOWOUT_DESTINATION } from '../utils' import type { AspDispAirgapParams, CreateCommand } from '@opentrons/shared-data' @@ -120,6 +122,43 @@ describe('consolidate single-channel', () => { ]) }) + it('Minimal single-channel: A1 A2 to B1, 50uL with p300, drop in waste chute', () => { + const data = { + ...mixinArgs, + sourceWells: ['A1', 'A2'], + volume: 50, + changeTip: 'once', + dropTipLocation: 'wasteChuteId', + dispenseAirGapVolume: 5, + } as ConsolidateArgs + + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + wasteChuteId: { + name: 'wasteChute', + id: 'wasteChuteId', + location: 'cutoutD3', + }, + }, + } + + const result = consolidate(data, invariantContext, initialRobotState) + const res = getSuccessResult(result) + + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), + aspirateHelper('A1', 50), + aspirateHelper('A2', 50), + dispenseHelper('B1', 100), + airGapHelper('B1', 5, { labwareId: 'destPlateId' }), + moveToAddressableAreaHelper({ + addressableAreaName: '1and8ChannelWasteChute', + }), + dropTipInPlaceHelper(), + ]) + }) + it('Single-channel with exceeding pipette max: A1 A2 A3 A4 to B1, 150uL with p300', () => { // TODO Ian 2018-05-03 is this a duplicate of exceeding max with changeTip="once"??? const data = { diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index 82a0ae78c9b..dd7c449cd05 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -19,6 +19,8 @@ import { SOURCE_LABWARE, makeDispenseAirGapHelper, AIR_GAP_META, + dropTipInPlaceHelper, + moveToAddressableAreaHelper, } from '../fixtures' import { FIXED_TRASH_ID } from '..' import { @@ -109,6 +111,40 @@ describe('pick up tip if no tip on pipette', () => { expect(res.commands[0]).toEqual(pickUpTipHelper('A1')) }) }) + it('...once, drop tip in waste chute', () => { + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + wasteChuteId: { + name: 'wasteChute', + id: 'wasteChuteId', + location: 'cutoutD3', + }, + }, + } + + noTipArgs = { + ...noTipArgs, + changeTip: 'always', + dropTipLocation: 'wasteChuteId', + dispenseAirGapVolume: 5, + } as TransferArgs + + const result = transfer(noTipArgs, invariantContext, robotStateWithTip) + + const res = getSuccessResult(result) + + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), + aspirateHelper('A1', 30), + dispenseHelper('B2', 30), + airGapHelper('B2', 5, { labwareId: 'destPlateId' }), + moveToAddressableAreaHelper({ + addressableAreaName: '1and8ChannelWasteChute', + }), + dropTipInPlaceHelper(), + ]) + }) it('...never (should not pick up tip, and fail)', () => { noTipArgs = { diff --git a/step-generation/src/commandCreators/compound/consolidate.ts b/step-generation/src/commandCreators/compound/consolidate.ts index d07841cb579..5908d96bdc0 100644 --- a/step-generation/src/commandCreators/compound/consolidate.ts +++ b/step-generation/src/commandCreators/compound/consolidate.ts @@ -8,6 +8,7 @@ import { blowoutUtil, curryCommandCreator, reduceCommandCreators, + wasteChuteCommandsUtil, } from '../../utils' import { configureForVolume } from '../atomic/configureForVolume' import { @@ -96,6 +97,25 @@ export const consolidate: CommandCreator = ( const sourceWellChunks = chunk(args.sourceWells, maxWellsPerChunk) + const isWasteChute = + invariantContext.additionalEquipmentEntities[args.dropTipLocation] != null + + const addressableAreaName = + invariantContext.pipetteEntities[args.pipette].spec.channels === 96 + ? '96ChannelWasteChute' + : '1and8ChannelWasteChute' + + const dropTipCommand = isWasteChute + ? curryCommandCreator(wasteChuteCommandsUtil, { + type: 'dropTip', + pipetteId: args.pipette, + addressableAreaName, + }) + : curryCommandCreator(dropTip, { + pipette: args.pipette, + dropTipLocation: args.dropTipLocation, + }) + const commandCreators = flatMap( sourceWellChunks, ( @@ -302,14 +322,7 @@ export const consolidate: CommandCreator = ( : [] // if using dispense > air gap, drop or change the tip at the end const dropTipAfterDispenseAirGap = - airGapAfterDispenseCommands.length > 0 - ? [ - curryCommandCreator(dropTip, { - pipette: args.pipette, - dropTipLocation: dropTipLocation, - }), - ] - : [] + airGapAfterDispenseCommands.length > 0 ? [dropTipCommand] : [] const blowoutCommand = blowoutUtil({ pipette: args.pipette, sourceLabwareId: args.sourceLabware, diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index 962885376b1..6715cab5b14 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -9,6 +9,7 @@ import { curryCommandCreator, getDispenseAirGapLocation, reduceCommandCreators, + wasteChuteCommandsUtil, } from '../../utils' import { aspirate, @@ -91,6 +92,26 @@ export const transfer: CommandCreator = ( errors, } const pipetteSpec = invariantContext.pipetteEntities[args.pipette].spec + + const isWasteChute = + invariantContext.additionalEquipmentEntities[args.dropTipLocation] != null + + const addressableAreaName = + pipetteSpec.channels === 96 + ? '96ChannelWasteChute' + : '1and8ChannelWasteChute' + + const dropTipCommand = isWasteChute + ? curryCommandCreator(wasteChuteCommandsUtil, { + type: 'dropTip', + pipetteId: args.pipette, + addressableAreaName: addressableAreaName, + }) + : curryCommandCreator(dropTip, { + pipette: args.pipette, + dropTipLocation: args.dropTipLocation, + }) + // TODO: BC 2019-07-08 these argument names are a bit misleading, instead of being values bound // to the action of aspiration of dispensing in a given command, they are actually values bound // to a given labware associated with a command (e.g. Source, Destination). For this reason we @@ -407,12 +428,7 @@ export const transfer: CommandCreator = ( // if using dispense > air gap, drop or change the tip at the end const dropTipAfterDispenseAirGap = airGapAfterDispenseCommands.length > 0 && isLastChunk && isLastPair - ? [ - curryCommandCreator(dropTip, { - pipette: args.pipette, - dropTipLocation: args.dropTipLocation, - }), - ] + ? [dropTipCommand] : [] const blowoutCommand = blowoutUtil({ pipette: args.pipette, From f73e925174d8284a4e9e781ffe22eec3128a762d Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Fri, 17 Nov 2023 09:55:59 -0500 Subject: [PATCH 46/46] refactor(protocol-designer): staging area slots now render properly (#14002) --- .../src/components/DeckSetup/SlotLabels.tsx | 4 +++- .../src/components/DeckSetup/index.tsx | 22 +++++++++---------- .../components/modules/StagingAreasModal.tsx | 12 +++++----- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/protocol-designer/src/components/DeckSetup/SlotLabels.tsx b/protocol-designer/src/components/DeckSetup/SlotLabels.tsx index a357197e98c..0efcc8a5a3b 100644 --- a/protocol-designer/src/components/DeckSetup/SlotLabels.tsx +++ b/protocol-designer/src/components/DeckSetup/SlotLabels.tsx @@ -15,6 +15,7 @@ import type { RobotType } from '@opentrons/shared-data' interface SlotLabelsProps { robotType: RobotType hasStagingAreas: boolean + hasWasteChute: boolean } /** @@ -25,6 +26,7 @@ interface SlotLabelsProps { export const SlotLabels = ({ robotType, hasStagingAreas, + hasWasteChute, }: SlotLabelsProps): JSX.Element | null => { return robotType === FLEX_ROBOT_TYPE ? ( <> @@ -59,7 +61,7 @@ export const SlotLabels = ({ height="2.5rem" width={hasStagingAreas ? '40.5rem' : '30.375rem'} x="-15" - y="-65" + y={hasWasteChute ? '-90' : '-65'} > { ] const wasteChuteFixtures = Object.values( activeDeckSetup.additionalEquipmentOnDeck - ).filter(aE => - WASTE_CHUTE_ADDRESSABLE_AREAS.includes(aE.name as AddressableAreaName) - ) + ).filter(aE => WASTE_CHUTE_CUTOUT.includes(aE.location as CutoutId)) const stagingAreaFixtures: AdditionalEquipmentEntity[] = Object.values( activeDeckSetup.additionalEquipmentOnDeck - ).filter(aE => - FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes( - aE.name as AddressableAreaName - ) - ) + ).filter(aE => STAGING_AREA_CUTOUTS.includes(aE.location as CutoutId)) + + const hasWasteChute = wasteChuteFixtures.length > 0 const filteredAddressableAreas = deckDef.locations.addressableAreas.filter( aa => isAddressableAreaStandardSlot(aa.id, deckDef) @@ -561,7 +556,11 @@ export const DeckSetup = (): JSX.Element => { {({ getRobotCoordsFromDOMCoords }) => ( <> @@ -647,6 +646,7 @@ export const DeckSetup = (): JSX.Element => { 0} + hasWasteChute={hasWasteChute} /> )} diff --git a/protocol-designer/src/components/modules/StagingAreasModal.tsx b/protocol-designer/src/components/modules/StagingAreasModal.tsx index a5fd5a258ec..6a4cada13d3 100644 --- a/protocol-designer/src/components/modules/StagingAreasModal.tsx +++ b/protocol-designer/src/components/modules/StagingAreasModal.tsx @@ -23,6 +23,7 @@ import { STAGING_AREA_CUTOUTS, STAGING_AREA_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' +import { getStagingAreaSlots } from '../../utils' import { i18n } from '../../localization' import { createDeckFixture, @@ -31,8 +32,7 @@ import { import { getSlotIsEmpty } from '../../step-forms' import { getInitialDeckSetup } from '../../step-forms/selectors' import { PDAlert } from '../alerts/PDAlert' -import { AdditionalEquipmentEntity } from '@opentrons/step-generation' -import { getStagingAreaSlots } from '../../utils' +import type { AdditionalEquipmentEntity } from '@opentrons/step-generation' export interface StagingAreasValues { selectedSlots: string[] @@ -107,15 +107,15 @@ const StagingAreasModalComponent = ( const handleClickRemove = (cutoutId: string): void => { const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { if (slot.cutoutId === cutoutId) { - return { ...slot, loadName: SINGLE_RIGHT_SLOT_FIXTURE } + return { ...slot, cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE } } return slot }) setUpdatedSlots(modifiedSlots) - const updatedSelectedSlots = values.selectedSlots.filter( - item => item !== cutoutId + setFieldValue( + 'selectedSlots', + values.selectedSlots.filter(item => item !== cutoutId) ) - setFieldValue('selectedSlots', updatedSelectedSlots) } return (