From 187ba1ada8674a76ea734ee70b2515aa2c1899a6 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:51:52 -0500 Subject: [PATCH 01/25] fix(protocol-designer): blowout field checkbox properly populating (#17093) closes RQA-3792 This was a weird bug only affecting the UI and not any commands being generated. Basically, the blowout checkbox was incorrectly not being selected because there was logic in place to hardcode it to false due to the disposal volume checkbox being true. IDK why the disposal volume checkbox is true -- seems like something we allowed for all move liquid steps back in the day. So for a UI fix, I added the field to be dependent on the disposal volume fields and removed the useEffect in place. --- .../StepForm/StepTools/MoveLiquidTools/index.tsx | 8 -------- .../handleFormChange/dependentFieldsUpdateMoveLiquid.ts | 1 + .../formLevel/handleFormChange/test/moveLiquid.test.ts | 1 + 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx index fcb64ff8834..e3b0c2f7828 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { @@ -134,13 +133,6 @@ export function MoveLiquidTools(props: StepFormProps): JSX.Element { const mappedErrorsToField = getFormErrorsMappedToField(visibleFormErrors) - // auto-collapse blowout field if disposal volume is checked - useEffect(() => { - if (formData.disposalVolume_checkbox) { - propsForFields.blowout_checkbox.updateValue(false) - } - }, [formData.disposalVolume_checkbox]) - return toolboxStep === 0 ? ( { dispense_mix_checkbox: false, dispense_mix_times: null, dispense_mix_volume: null, + blowout_checkbox: false, }) }) From 071356319648917c31514036f3d5afc0e57361cb Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Thu, 12 Dec 2024 16:24:26 -0500 Subject: [PATCH 02/25] feat(app): add renderer source maps (#17062) Closes EXEC-1046 Currently, whenever browser layer errors occur in an electron app production build, renderer process debugging is difficult, because production builds are minified. Enter source maps, which, when utilized alongside a library that provides stack trace support for V8, make error messaging much more useful. --- app/package.json | 2 ++ app/src/App/index.tsx | 1 + app/tsconfig.json | 3 ++- app/vite.config.mts | 1 + yarn.lock | 16 ++++++++++++++++ 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index 33ae6252d1a..5528120de1f 100644 --- a/app/package.json +++ b/app/package.json @@ -64,6 +64,7 @@ "rxjs": "^6.5.1", "semver": "5.7.2", "simple-keyboard-layouts": "3.4.41", + "source-map-support": "0.5.10", "styled-components": "5.3.6", "typeface-open-sans": "0.0.75", "uuid": "3.2.1" @@ -75,6 +76,7 @@ "@types/jszip": "3.1.7", "@types/mixpanel-browser": "^2.35.6", "@types/node-fetch": "2.6.11", + "@types/source-map-support": "0.5.10", "@types/styled-components": "^5.1.26", "axios": "^0.21.1", "electron-updater": "6.3.9", diff --git a/app/src/App/index.tsx b/app/src/App/index.tsx index f0ba1de0304..2e05c91359e 100644 --- a/app/src/App/index.tsx +++ b/app/src/App/index.tsx @@ -1,5 +1,6 @@ import type * as React from 'react' import { useSelector } from 'react-redux' +import 'source-map-support/register' import { Flex, POSITION_FIXED, DIRECTION_ROW } from '@opentrons/components' diff --git a/app/tsconfig.json b/app/tsconfig.json index 92921aa2b1c..db9d81301e0 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -35,7 +35,8 @@ "outDir": "lib", "paths": { "/app/*": ["./src/*"] - } + }, + "sourceMap": true, }, "include": ["typings", "src"], "exclude": ["**/*.stories.tsx"] diff --git a/app/vite.config.mts b/app/vite.config.mts index f10fedf4f7e..4828283b107 100644 --- a/app/vite.config.mts +++ b/app/vite.config.mts @@ -19,6 +19,7 @@ export default defineConfig( build: { // Relative to the root outDir: 'dist', + sourcemap: true }, plugins: [ react({ diff --git a/yarn.lock b/yarn.lock index d788db962fd..98619e9a945 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3684,6 +3684,7 @@ rxjs "^6.5.1" semver "5.7.2" simple-keyboard-layouts "3.4.41" + source-map-support "0.5.10" styled-components "5.3.6" typeface-open-sans "0.0.75" uuid "3.2.1" @@ -6122,6 +6123,13 @@ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.8.tgz#518609aefb797da19bf222feb199e8f653ff7627" integrity sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg== +"@types/source-map-support@0.5.10": + version "0.5.10" + resolved "https://registry.yarnpkg.com/@types/source-map-support/-/source-map-support-0.5.10.tgz#824dcef989496bae98e9d04c8dc1ac1d70e1bd39" + integrity sha512-tgVP2H469x9zq34Z0m/fgPewGhg/MLClalNOiPIzQlXrSS2YrKu/xCdSCKnEDwkFha51VKEKB6A9wW26/ZNwzA== + dependencies: + source-map "^0.6.0" + "@types/styled-components@^5.1.26": version "5.1.34" resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.34.tgz#4107df8ef8a7eaba4fa6b05f78f93fba4daf0300" @@ -20718,6 +20726,14 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" +source-map-support@0.5.10: + version "0.5.10" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c" + integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-support@^0.5.16, source-map-support@^0.5.19, source-map-support@^0.5.6, source-map-support@~0.5.12, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" From f8cdc82615b5fb2db437ea26728e995e0ff8c8ec Mon Sep 17 00:00:00 2001 From: Anthony Ngumah <68346382+AnthonyNASC20@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:48:04 -0500 Subject: [PATCH 03/25] Changes process calls to speed up code execution (#17099) # Overview Switches to using a queue instead of running a separate print process which speeds up execution of push-folder command Co-authored-by: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> --- abr-testing/abr_testing/tools/make_push.py | 46 ++++++++++------------ 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/abr-testing/abr_testing/tools/make_push.py b/abr-testing/abr_testing/tools/make_push.py index 28a69b11103..665831158bc 100644 --- a/abr-testing/abr_testing/tools/make_push.py +++ b/abr-testing/abr_testing/tools/make_push.py @@ -1,7 +1,8 @@ """Push one or more folders to one or more robots.""" import subprocess -import multiprocessing import json +from typing import List +from multiprocessing import Process, Queue global folders # Opentrons folders that can be pushed to robot @@ -13,13 +14,13 @@ ] -def push_subroutine(cmd: str) -> None: +def push_subroutine(cmd: str, queue: Queue) -> None: """Pushes specified folder to specified robot.""" try: subprocess.run(cmd) + queue.put(f"{cmd}: SUCCESS!\n") except Exception: - print("failed to push folder") - raise + queue.put(f"{cmd}: FAILED\n") def main(folder_to_push: str, robot_to_push: str) -> int: @@ -27,6 +28,8 @@ def main(folder_to_push: str, robot_to_push: str) -> int: cmd = "make -C {folder} push-ot3 host={ip}" robot_ip_path = "" push_cmd = "" + processes: List[Process] = [] + queue: Queue = Queue() folder_int = int(folder_to_push) if folders[folder_int].lower() == "abr-testing + hardware-testing": if robot_to_push.lower() == "all": @@ -41,20 +44,16 @@ def main(folder_to_push: str, robot_to_push: str) -> int: for folder_name in folders[:-2]: # Push abr-testing and hardware-testing folders to all robots for robot in robot_ips: - print_proc = multiprocessing.Process( - target=print, args=(f"Pushing {folder_name} to {robot}!\n\n",) - ) - print_proc.start() - print_proc.join() push_cmd = cmd.format(folder=folder_name, ip=robot) - process = multiprocessing.Process( - target=push_subroutine, args=(push_cmd,) + process = Process( + target=push_subroutine, + args=( + push_cmd, + queue, + ), ) process.start() - process.join() - print_proc = multiprocessing.Process(target=print, args=("Done!\n\n",)) - print_proc.start() - print_proc.join() + processes.append(process) else: if folder_int == (len(folders) - 1): @@ -72,18 +71,15 @@ def main(folder_to_push: str, robot_to_push: str) -> int: # Push folder to robots for robot in robot_ips: - print_proc = multiprocessing.Process( - target=print, args=(f"Pushing {folder_name} to {robot}!\n\n",) - ) - print_proc.start() - print_proc.join() push_cmd = cmd.format(folder=folder_name, ip=robot) - process = multiprocessing.Process(target=push_subroutine, args=(push_cmd,)) + process = Process(target=push_subroutine, args=(push_cmd, queue)) process.start() - process.join() - print_proc = multiprocessing.Process(target=print, args=("Done!\n\n",)) - print_proc.start() - print_proc.join() + processes.append(process) + + for process in processes: + process.join() + result = queue.get() + print(f"\n{result}") return 0 From 7bb771a2032dc49c9031c95d1f2081d27e95dd0c Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Thu, 12 Dec 2024 16:55:36 -0500 Subject: [PATCH 04/25] fix(app): fix manual file upload (#17098) The electron update made it no longer possible to access the file path from the renderer process, see #17010 for more details. That PR did not update one important case: uploading zip files. Currently, fun behavior happens when uploading a system zip, such as throwing an error or better yet, pushing an old, cache file instead of the expected system file. This commit fixes that. --- .../AdvancedTab/UpdateRobotSoftware.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx index a893c616508..55c64ee648a 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx @@ -23,6 +23,7 @@ import { ExternalLink } from '/app/atoms/Link/ExternalLink' import { TertiaryButton } from '/app/atoms/buttons' import { getRobotUpdateDisplayInfo } from '/app/redux/robot-update' import { useDispatchStartRobotUpdate } from '/app/redux/robot-update/hooks' +import { remote } from '/app/redux/shell/remote' import type { ChangeEventHandler, MouseEventHandler } from 'react' import type { State } from '/app/redux/types' @@ -55,14 +56,19 @@ export function UpdateRobotSoftware({ const handleChange: ChangeEventHandler = event => { const { files } = event.target - if (files?.length === 1 && !updateDisabled) { - dispatchStartRobotUpdate(robotName, files[0].path) - onUpdateStart() - } - // this is to reset the state of the file picker so users can reselect the same - // system image if the upload fails - if (inputRef.current?.value != null) { - inputRef.current.value = '' + + if (files != null) { + void remote.getFilePathFrom(files[0]).then(filePath => { + if (files.length === 1 && !updateDisabled) { + dispatchStartRobotUpdate(robotName, filePath) + onUpdateStart() + } + // this is to reset the state of the file picker so users can reselect the same + // system image if the upload fails + if (inputRef.current?.value != null) { + inputRef.current.value = '' + } + }) } } From a0bd4ee26d470737b59b0ff222e5eb12437aca75 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 13 Dec 2024 10:19:52 -0500 Subject: [PATCH 05/25] refactor(app): remove unneeded source maps dependency (#17100) After playing with source map dependencies, it turns out that we don't need the extra library, and the tsconfig source maps are irrelevant due to the way vite bundles source maps. All we need is to keep the existing sourceMaps: true property in our vite.config build options! This cuts the bundle size dramatically: it's now only ~3MB increase. This also fixes an interaction with the source maps library and the network settings tab, causing the app to crash (on the desktop app). --- app/package.json | 2 -- app/src/App/index.tsx | 1 - app/tsconfig.json | 1 - yarn.lock | 16 ---------------- 4 files changed, 20 deletions(-) diff --git a/app/package.json b/app/package.json index 5528120de1f..33ae6252d1a 100644 --- a/app/package.json +++ b/app/package.json @@ -64,7 +64,6 @@ "rxjs": "^6.5.1", "semver": "5.7.2", "simple-keyboard-layouts": "3.4.41", - "source-map-support": "0.5.10", "styled-components": "5.3.6", "typeface-open-sans": "0.0.75", "uuid": "3.2.1" @@ -76,7 +75,6 @@ "@types/jszip": "3.1.7", "@types/mixpanel-browser": "^2.35.6", "@types/node-fetch": "2.6.11", - "@types/source-map-support": "0.5.10", "@types/styled-components": "^5.1.26", "axios": "^0.21.1", "electron-updater": "6.3.9", diff --git a/app/src/App/index.tsx b/app/src/App/index.tsx index 2e05c91359e..f0ba1de0304 100644 --- a/app/src/App/index.tsx +++ b/app/src/App/index.tsx @@ -1,6 +1,5 @@ import type * as React from 'react' import { useSelector } from 'react-redux' -import 'source-map-support/register' import { Flex, POSITION_FIXED, DIRECTION_ROW } from '@opentrons/components' diff --git a/app/tsconfig.json b/app/tsconfig.json index db9d81301e0..1a24f98b959 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -36,7 +36,6 @@ "paths": { "/app/*": ["./src/*"] }, - "sourceMap": true, }, "include": ["typings", "src"], "exclude": ["**/*.stories.tsx"] diff --git a/yarn.lock b/yarn.lock index 98619e9a945..d788db962fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3684,7 +3684,6 @@ rxjs "^6.5.1" semver "5.7.2" simple-keyboard-layouts "3.4.41" - source-map-support "0.5.10" styled-components "5.3.6" typeface-open-sans "0.0.75" uuid "3.2.1" @@ -6123,13 +6122,6 @@ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.8.tgz#518609aefb797da19bf222feb199e8f653ff7627" integrity sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg== -"@types/source-map-support@0.5.10": - version "0.5.10" - resolved "https://registry.yarnpkg.com/@types/source-map-support/-/source-map-support-0.5.10.tgz#824dcef989496bae98e9d04c8dc1ac1d70e1bd39" - integrity sha512-tgVP2H469x9zq34Z0m/fgPewGhg/MLClalNOiPIzQlXrSS2YrKu/xCdSCKnEDwkFha51VKEKB6A9wW26/ZNwzA== - dependencies: - source-map "^0.6.0" - "@types/styled-components@^5.1.26": version "5.1.34" resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.34.tgz#4107df8ef8a7eaba4fa6b05f78f93fba4daf0300" @@ -20726,14 +20718,6 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@0.5.10: - version "0.5.10" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c" - integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - source-map-support@^0.5.16, source-map-support@^0.5.19, source-map-support@^0.5.6, source-map-support@~0.5.12, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" From a7a84df0096a7aefc46d93288690601a57e70e01 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 13 Dec 2024 11:26:24 -0500 Subject: [PATCH 06/25] fix(ci): fix failing builds due to oom issues (#17103) The macos build in particular is failing due to the node heap running out of available memory. Upping the memory fixes this. --- app/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Makefile b/app/Makefile index b11fed0b2b3..8f77700e3b7 100644 --- a/app/Makefile +++ b/app/Makefile @@ -47,7 +47,7 @@ clean: dist: export NODE_ENV := production dist: echo "Building app JS bundle (browser layer)" - vite build + NODE_OPTIONS="--max-old-space-size=8192" vite build # development ##################################################################### From 352e4ac80f67674b812bf6c2c176cab7c73b85bc Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Fri, 13 Dec 2024 11:50:59 -0500 Subject: [PATCH 07/25] fix(api): Speed up LLD by reducing the post-success raise. (#17102) # Overview Right now if we successfully detect liquid we raise (slowly) the entire max pass distance. this takes a while, especially on the 96 channel since it's calculated off the max plunger speed. This reduces the post-success raise to the "safe plunger prep" distance which we know is a safe distance from the liquid to prevent unwanted capillary action or adhesion to the outside of the tip. ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- api/src/opentrons/hardware_control/backends/flex_protocol.py | 1 + api/src/opentrons/hardware_control/backends/ot3controller.py | 2 ++ api/src/opentrons/hardware_control/backends/ot3simulator.py | 1 + api/src/opentrons/hardware_control/ot3api.py | 3 +++ .../hardware_control/backends/test_ot3_controller.py | 1 + api/tests/opentrons/hardware_control/test_ot3_api.py | 3 +++ hardware/opentrons_hardware/hardware_control/tool_sensors.py | 4 +++- .../opentrons_hardware/hardware_control/test_tool_sensors.py | 1 + 8 files changed, 15 insertions(+), 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/backends/flex_protocol.py b/api/src/opentrons/hardware_control/backends/flex_protocol.py index 64f82a751ef..ef38af631e7 100644 --- a/api/src/opentrons/hardware_control/backends/flex_protocol.py +++ b/api/src/opentrons/hardware_control/backends/flex_protocol.py @@ -165,6 +165,7 @@ async def liquid_probe( threshold_pascals: float, plunger_impulse_time: float, num_baseline_reads: int, + z_offset_for_plunger_prep: float, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, force_both_sensors: bool = False, response_queue: Optional[PipetteSensorResponseQueue] = None, diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index f710eb405ac..cdbd8818111 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -1493,6 +1493,7 @@ async def liquid_probe( threshold_pascals: float, plunger_impulse_time: float, num_baseline_reads: int, + z_offset_for_plunger_prep: float, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, force_both_sensors: bool = False, response_queue: Optional[PipetteSensorResponseQueue] = None, @@ -1535,6 +1536,7 @@ def response_capture(data: Dict[SensorId, List[SensorDataType]]) -> None: threshold_pascals=threshold_pascals, plunger_impulse_time=plunger_impulse_time, num_baseline_reads=num_baseline_reads, + z_offset_for_plunger_prep=z_offset_for_plunger_prep, sensor_id=sensor_id_for_instrument(probe), force_both_sensors=force_both_sensors, emplace_data=response_capture, diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index e85b51ad53f..10f4ebcfe2a 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -353,6 +353,7 @@ async def liquid_probe( threshold_pascals: float, plunger_impulse_time: float, num_baseline_reads: int, + z_offset_for_plunger_prep: float, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, force_both_sensors: bool = False, response_queue: Optional[PipetteSensorResponseQueue] = None, diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 7bb5e05f47b..6295757e7ab 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2677,6 +2677,7 @@ async def _liquid_probe_pass( probe_settings: LiquidProbeSettings, probe: InstrumentProbeType, p_travel: float, + z_offset_for_plunger_prep: float, force_both_sensors: bool = False, response_queue: Optional[PipetteSensorResponseQueue] = None, ) -> float: @@ -2689,6 +2690,7 @@ async def _liquid_probe_pass( probe_settings.sensor_threshold_pascals, probe_settings.plunger_impulse_time, probe_settings.samples_for_baselining, + z_offset_for_plunger_prep, probe=probe, force_both_sensors=force_both_sensors, response_queue=response_queue, @@ -2838,6 +2840,7 @@ async def prep_plunger_for_probe_move( probe_settings, checked_probe, plunger_travel_mm + sensor_baseline_plunger_move_mm, + z_offset_for_plunger_prep, force_both_sensors, response_queue, ) diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py index 9c03bed68b2..9bd87fe62ec 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py @@ -745,6 +745,7 @@ async def test_liquid_probe( threshold_pascals=fake_liquid_settings.sensor_threshold_pascals, plunger_impulse_time=fake_liquid_settings.plunger_impulse_time, num_baseline_reads=fake_liquid_settings.samples_for_baselining, + z_offset_for_plunger_prep=2.0, ) except PipetteLiquidNotFoundError: # the move raises a liquid not found now since we don't call the move group and it doesn't diff --git a/api/tests/opentrons/hardware_control/test_ot3_api.py b/api/tests/opentrons/hardware_control/test_ot3_api.py index 2fd3fb4377c..a0775d48e19 100644 --- a/api/tests/opentrons/hardware_control/test_ot3_api.py +++ b/api/tests/opentrons/hardware_control/test_ot3_api.py @@ -858,6 +858,7 @@ async def test_liquid_probe( fake_settings_aspirate.sensor_threshold_pascals, fake_settings_aspirate.plunger_impulse_time, fake_settings_aspirate.samples_for_baselining, + probe_safe_reset_mm, probe=InstrumentProbeType.PRIMARY, force_both_sensors=False, response_queue=None, @@ -1114,6 +1115,7 @@ async def test_multi_liquid_probe( fake_settings_aspirate.sensor_threshold_pascals, fake_settings_aspirate.plunger_impulse_time, fake_settings_aspirate.samples_for_baselining, + 2.0, probe=InstrumentProbeType.PRIMARY, force_both_sensors=False, response_queue=None, @@ -1149,6 +1151,7 @@ async def _fake_pos_update_and_raise( threshold_pascals: float, plunger_impulse_time: float, num_baseline_reads: int, + z_offset_for_plunger_prep: float, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, force_both_sensors: bool = False, response_queue: Optional[ diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index e9475de8b95..9df634f11b1 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -269,6 +269,7 @@ async def liquid_probe( threshold_pascals: float, plunger_impulse_time: float, num_baseline_reads: int, + z_offset_for_plunger_prep: float, sensor_id: SensorId = SensorId.S0, force_both_sensors: bool = False, emplace_data: Optional[ @@ -331,8 +332,9 @@ async def liquid_probe( ) sensor_runner = MoveGroupRunner(move_groups=[[lower_plunger], [sensor_group]]) + # Only raise the z a little so we don't do a huge slow travel raise_z = create_step( - distance={head_node: float64(max_z_distance)}, + distance={head_node: float64(z_offset_for_plunger_prep)}, velocity={head_node: float64(-1 * mount_speed)}, acceleration={}, duration=float64(max_z_distance / mount_speed), diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py index 0c53b81057a..ebcfcc5f05a 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py @@ -237,6 +237,7 @@ def move_responder( threshold_pascals=threshold_pascals, plunger_impulse_time=0.2, num_baseline_reads=20, + z_offset_for_plunger_prep=2.0, sensor_id=SensorId.S0, ) assert position[motor_node].positions_only()[0] == 14 From 8c43dcc9b32a7ff5bb62af3105438b9ad38c5817 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 13 Dec 2024 14:57:47 -0500 Subject: [PATCH 08/25] fix(app): fix dev ODD system image build causing infinite spinner (#17104) Dev ODD system image builds have been failing. When digging into the Electron renderer process logs, this appears to be because of the way we inject the mixpanel config in our dev builds: it's not what mixpanel expects. During mixpanel initialization, if an unexpected configuration/id is supplied, certain methods on mixpanel are undefined. We've had similar issues like this in the past, and a more general-case solution to all of these issues is just to wrap the whole mixpanel initialization process in a try-catch block. --- app/src/redux/analytics/mixpanel.ts | 70 ++++++++++++++++++----------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/app/src/redux/analytics/mixpanel.ts b/app/src/redux/analytics/mixpanel.ts index aa5ad5a7893..5304bbbe563 100644 --- a/app/src/redux/analytics/mixpanel.ts +++ b/app/src/redux/analytics/mixpanel.ts @@ -1,9 +1,9 @@ -// mixpanel actions import mixpanel from 'mixpanel-browser' -import { createLogger } from '../../logger' +import { createLogger } from '/app/logger' import { CURRENT_VERSION } from '../shell' +import type { Config as MixpanelConfig } from 'mixpanel-browser' import type { AnalyticsEvent, AnalyticsConfig } from './types' const log = createLogger(new URL('', import.meta.url).pathname) @@ -11,7 +11,7 @@ const log = createLogger(new URL('', import.meta.url).pathname) // pulled in from environment at build time const MIXPANEL_ID = process.env.OT_APP_MIXPANEL_ID -const MIXPANEL_OPTS = { +const MIXPANEL_OPTS: Partial = { // opt out by default opt_out_tracking_by_default: true, // user details are persisted in our own config store @@ -27,9 +27,13 @@ export function initializeMixpanel( isOnDevice: boolean | null ): void { if (MIXPANEL_ID != null) { - initMixpanelInstanceOnce(config) - setMixpanelTracking(config, isOnDevice) - trackEvent({ name: 'appOpen', properties: {} }, config) + try { + initMixpanelInstanceOnce(config) + setMixpanelTracking(config, isOnDevice) + trackEvent({ name: 'appOpen', properties: {} }, config) + } catch (error) { + console.error('Failed to initialize Mixpanel:', error) + } } else { log.warn('MIXPANEL_ID not found; this is a bug if build is production') } @@ -43,11 +47,15 @@ export function trackEvent( log.debug('Trackable event', { event, optedIn }) if (MIXPANEL_ID != null && optedIn) { - if (event.superProperties != null) { - mixpanel.register(event.superProperties) - } - if ('name' in event && event.name != null) { - mixpanel.track(event.name, event.properties) + try { + if (event.superProperties != null) { + mixpanel.register(event.superProperties) + } + if ('name' in event && event.name != null) { + mixpanel.track(event.name, event.properties) + } + } catch (error) { + console.error('Failed to track event:', error) } } } @@ -58,19 +66,23 @@ export function setMixpanelTracking( ): void { if (MIXPANEL_ID != null) { initMixpanelInstanceOnce(config) - if (config.optedIn) { - log.debug('User has opted into analytics; tracking with Mixpanel') - mixpanel.identify(config.appId) - mixpanel.opt_in_tracking() - mixpanel.register({ - appVersion: CURRENT_VERSION, - appId: config.appId, - appMode: Boolean(isOnDevice) ? 'ODD' : 'Desktop', - }) - } else { - log.debug('User has opted out of analytics; stopping tracking') - mixpanel.opt_out_tracking?.() - mixpanel.reset?.() + try { + if (config.optedIn) { + log.debug('User has opted into analytics; tracking with Mixpanel') + mixpanel.identify(config.appId) + mixpanel.opt_in_tracking() + mixpanel.register({ + appVersion: CURRENT_VERSION, + appId: config.appId, + appMode: Boolean(isOnDevice) ? 'ODD' : 'Desktop', + }) + } else { + log.debug('User has opted out of analytics; stopping tracking') + mixpanel.opt_out_tracking() + mixpanel.reset() + } + } catch (error) { + console.error('Failed to set Mixpanel tracking:', error) } } } @@ -82,9 +94,13 @@ function initializeMixpanelInstanceOnce( return function (config: AnalyticsConfig): undefined { if (!hasBeenInitialized && MIXPANEL_ID != null) { - hasBeenInitialized = true - log.debug('Initializing Mixpanel', { config }) - mixpanel.init(MIXPANEL_ID, MIXPANEL_OPTS) + try { + hasBeenInitialized = true + log.debug('Initializing Mixpanel', { config }) + mixpanel.init(MIXPANEL_ID, MIXPANEL_OPTS) + } catch (error) { + console.error('Failed to initialize Mixpanel instance:', error) + } } } } From d6e5a5ee3ea6fa33a9f7e5069029466acb604299 Mon Sep 17 00:00:00 2001 From: koji Date: Fri, 13 Dec 2024 15:10:53 -0500 Subject: [PATCH 09/25] chore(protocol-designer): remove unused vite from package.json (#17108) * chore(protocol-designer): remove unused vite from package.json --- protocol-designer/package.json | 2 - yarn.lock | 174 +-------------------------------- 2 files changed, 4 insertions(+), 172 deletions(-) diff --git a/protocol-designer/package.json b/protocol-designer/package.json index 547b15de023..95f5acf4813 100755 --- a/protocol-designer/package.json +++ b/protocol-designer/package.json @@ -21,7 +21,6 @@ "dependencies": { "@fontsource/public-sans": "5.0.3", "@hot-loader/react-dom": "17.0.1", - "@vitejs/plugin-react": "^4.2.1", "@vituum/vite-plugin-postcss": "1.1.0", "@hookform/resolvers": "3.1.1", "@opentrons/components": "link:../components", @@ -62,7 +61,6 @@ "styled-components": "5.3.6", "ua-parser-js": "^0.7.23", "uuid": "3.3.2", - "vite": "5.0.5", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.4", "postcss-apply": "0.12.0", diff --git a/yarn.lock b/yarn.lock index d788db962fd..ee61c887c8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -804,7 +804,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02" integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g== -"@babel/core@>=7.2.2", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.18.9", "@babel/core@^7.20.12", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.23.3", "@babel/core@^7.23.5": +"@babel/core@>=7.2.2", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.18.9", "@babel/core@^7.20.12", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.23.3": version "7.24.4" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.4.tgz#1f758428e88e0d8c563874741bc4ffc4f71a4717" integrity sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg== @@ -2556,11 +2556,6 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== -"@esbuild/aix-ppc64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" - integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== - "@esbuild/aix-ppc64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" @@ -2581,11 +2576,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== -"@esbuild/android-arm64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" - integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== - "@esbuild/android-arm64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" @@ -2606,11 +2596,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== -"@esbuild/android-arm@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" - integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== - "@esbuild/android-arm@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" @@ -2631,11 +2616,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== -"@esbuild/android-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" - integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== - "@esbuild/android-x64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" @@ -2656,11 +2636,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== -"@esbuild/darwin-arm64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e" - integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== - "@esbuild/darwin-arm64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" @@ -2681,11 +2656,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== -"@esbuild/darwin-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" - integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== - "@esbuild/darwin-x64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" @@ -2706,11 +2676,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== -"@esbuild/freebsd-arm64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" - integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== - "@esbuild/freebsd-arm64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" @@ -2731,11 +2696,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== -"@esbuild/freebsd-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" - integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== - "@esbuild/freebsd-x64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" @@ -2756,11 +2716,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== -"@esbuild/linux-arm64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" - integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== - "@esbuild/linux-arm64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" @@ -2781,11 +2736,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== -"@esbuild/linux-arm@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" - integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== - "@esbuild/linux-arm@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" @@ -2806,11 +2756,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== -"@esbuild/linux-ia32@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" - integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== - "@esbuild/linux-ia32@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" @@ -2831,11 +2776,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== -"@esbuild/linux-loong64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" - integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== - "@esbuild/linux-loong64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" @@ -2856,11 +2796,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== -"@esbuild/linux-mips64el@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" - integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== - "@esbuild/linux-mips64el@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" @@ -2881,11 +2816,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== -"@esbuild/linux-ppc64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" - integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== - "@esbuild/linux-ppc64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" @@ -2906,11 +2836,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== -"@esbuild/linux-riscv64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" - integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== - "@esbuild/linux-riscv64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" @@ -2931,11 +2856,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== -"@esbuild/linux-s390x@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" - integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== - "@esbuild/linux-s390x@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" @@ -2956,11 +2876,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== -"@esbuild/linux-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" - integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== - "@esbuild/linux-x64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" @@ -2981,11 +2896,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== -"@esbuild/netbsd-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" - integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== - "@esbuild/netbsd-x64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" @@ -3011,11 +2921,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== -"@esbuild/openbsd-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" - integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== - "@esbuild/openbsd-x64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" @@ -3036,11 +2941,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== -"@esbuild/sunos-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" - integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== - "@esbuild/sunos-x64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" @@ -3061,11 +2961,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== -"@esbuild/win32-arm64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" - integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== - "@esbuild/win32-arm64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" @@ -3086,11 +2981,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== -"@esbuild/win32-ia32@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" - integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== - "@esbuild/win32-ia32@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" @@ -3111,11 +3001,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== -"@esbuild/win32-x64@0.19.12": - version "0.19.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" - integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== - "@esbuild/win32-x64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" @@ -5581,7 +5466,7 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== -"@types/babel__core@^7.0.0", "@types/babel__core@^7.18.0", "@types/babel__core@^7.20.4", "@types/babel__core@^7.20.5": +"@types/babel__core@^7.0.0", "@types/babel__core@^7.18.0", "@types/babel__core@^7.20.4": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== @@ -6400,17 +6285,6 @@ magic-string "^0.27.0" react-refresh "^0.14.0" -"@vitejs/plugin-react@^4.2.1": - version "4.2.1" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz#744d8e4fcb120fc3dbaa471dadd3483f5a304bb9" - integrity sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ== - dependencies: - "@babel/core" "^7.23.5" - "@babel/plugin-transform-react-jsx-self" "^7.23.3" - "@babel/plugin-transform-react-jsx-source" "^7.23.3" - "@types/babel__core" "^7.20.5" - react-refresh "^0.14.0" - "@vitest/coverage-v8@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-1.3.0.tgz#31f98b1bad1d5e9db733a4c1ae8d46dec549cd3c" @@ -11041,35 +10915,6 @@ esbuild@^0.18.0: "@esbuild/win32-ia32" "0.18.20" "@esbuild/win32-x64" "0.18.20" -esbuild@^0.19.3: - version "0.19.12" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04" - integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg== - optionalDependencies: - "@esbuild/aix-ppc64" "0.19.12" - "@esbuild/android-arm" "0.19.12" - "@esbuild/android-arm64" "0.19.12" - "@esbuild/android-x64" "0.19.12" - "@esbuild/darwin-arm64" "0.19.12" - "@esbuild/darwin-x64" "0.19.12" - "@esbuild/freebsd-arm64" "0.19.12" - "@esbuild/freebsd-x64" "0.19.12" - "@esbuild/linux-arm" "0.19.12" - "@esbuild/linux-arm64" "0.19.12" - "@esbuild/linux-ia32" "0.19.12" - "@esbuild/linux-loong64" "0.19.12" - "@esbuild/linux-mips64el" "0.19.12" - "@esbuild/linux-ppc64" "0.19.12" - "@esbuild/linux-riscv64" "0.19.12" - "@esbuild/linux-s390x" "0.19.12" - "@esbuild/linux-x64" "0.19.12" - "@esbuild/netbsd-x64" "0.19.12" - "@esbuild/openbsd-x64" "0.19.12" - "@esbuild/sunos-x64" "0.19.12" - "@esbuild/win32-arm64" "0.19.12" - "@esbuild/win32-ia32" "0.19.12" - "@esbuild/win32-x64" "0.19.12" - esbuild@^0.20.1: version "0.20.2" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" @@ -18490,7 +18335,7 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21 picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.1.7, postcss@^8.4, postcss@^8.4.32, postcss@^8.4.38: +postcss@^8.1.7, postcss@^8.4, postcss@^8.4.38: version "8.4.38" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== @@ -20038,7 +19883,7 @@ rollup@^2.44.0: optionalDependencies: fsevents "~2.3.2" -rollup@^4.13.0, rollup@^4.2.0: +rollup@^4.13.0: version "4.16.2" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.16.2.tgz#43bcbd225d0a6bc68df97a6e41c45003188a3845" integrity sha512-sxDP0+pya/Yi5ZtptF4p3avI+uWCIf/OdrfdH2Gbv1kWddLKk0U7WE3PmQokhi5JrektxsK3sK8s4hzAmjqahw== @@ -22886,17 +22731,6 @@ vite-node@1.2.2: picocolors "^1.0.0" vite "^5.0.0" -vite@5.0.5: - version "5.0.5" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.5.tgz#3eebe3698e3b32cea36350f58879258fec858a3c" - integrity sha512-OekeWqR9Ls56f3zd4CaxzbbS11gqYkEiBtnWFFgYR2WV8oPJRRKq0mpskYy/XaoCL3L7VINDhqqOMNDiYdGvGg== - dependencies: - esbuild "^0.19.3" - postcss "^8.4.32" - rollup "^4.2.0" - optionalDependencies: - fsevents "~2.3.3" - vite@5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.2.tgz#2f0a8531c71060467ed3e0a205a203f269b6d9c8" From a4f233853db66a0ac6763113748bc07474946d67 Mon Sep 17 00:00:00 2001 From: Andy Sigler Date: Mon, 16 Dec 2024 09:55:05 -0500 Subject: [PATCH 10/25] chore(shared-data): adds liquid-class Ethanol-80 and Glycerol-50 (#17068) --- .../definitions/1/ethanol_80.json | 2602 +++++++++++++++++ .../definitions/1/glycerol_50.json | 2462 ++++++++++++++++ 2 files changed, 5064 insertions(+) create mode 100644 shared-data/liquid-class/definitions/1/ethanol_80.json create mode 100644 shared-data/liquid-class/definitions/1/glycerol_50.json diff --git a/shared-data/liquid-class/definitions/1/ethanol_80.json b/shared-data/liquid-class/definitions/1/ethanol_80.json new file mode 100644 index 00000000000..392d1454f8f --- /dev/null +++ b/shared-data/liquid-class/definitions/1/ethanol_80.json @@ -0,0 +1,2602 @@ +{ + "liquidClassName": "ethanol_80", + "displayName": "Ethanol-80", + "schemaVersion": 1, + "namespace": "opentrons", + "byPipette": [ + { + "pipetteModel": "flex_1channel_50", + "byTipType": [ + { + "tiprack": "opentrons_flex_96_tiprack_50ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [1.0, -1.6], + [10.0, -1.1], + [50.0, -3.0] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 30.0]], + "correctionByVolume": [ + [1.0, -1.6], + [10.0, -1.1], + [50.0, -3.0] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 1.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 30.0]], + "correctionByVolume": [ + [1.0, -1.6], + [10.0, -1.1], + [50.0, -3.0] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_8channel_50", + "byTipType": [ + { + "tiprack": "opentrons_flex_96_tiprack_50ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [1.0, -1.6], + [10.0, -1.1], + [50.0, -3.0] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 30.0]], + "correctionByVolume": [ + [1.0, -1.6], + [10.0, -1.1], + [50.0, -3.0] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 1.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 30.0]], + "correctionByVolume": [ + [1.0, -1.6], + [10.0, -1.1], + [50.0, -3.0] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_1channel_1000", + "byTipType": [ + { + "tiprack": "opentrons_flex_96_tiprack_50ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [5.0, -2.1], + [10.0, -1.7], + [50.0, -3.3] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 125.0]], + "correctionByVolume": [ + [5.0, -2.1], + [10.0, -1.7], + [50.0, -3.3] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 10.0], + [10.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 125.0]], + "correctionByVolume": [ + [5.0, -2.1], + [10.0, -1.7], + [50.0, -3.3] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + }, + { + "tiprack": "opentrons_flex_96_tiprack_200ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [5.0, 7.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [5.0, -1.5], + [50.0, -2.2], + [200.0, -7.4] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 125.0]], + "correctionByVolume": [ + [5.0, -1.5], + [50.0, -2.2], + [200.0, -7.4] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 8.0], + [50.0, 4.0], + [200.0, 4.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 125.0]], + "correctionByVolume": [ + [5.0, -1.5], + [50.0, -2.2], + [200.0, -7.4] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + }, + { + "tiprack": "opentrons_flex_96_tiprack_1000ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [10.0, -1.9], + [100.0, -3.6], + [1000.0, -32.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [10.0, -1.9], + [100.0, -3.6], + [1000.0, -32.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [10.0, 6.0], + [100.0, 3.0], + [1000.0, 3.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [10.0, -1.9], + [100.0, -3.6], + [1000.0, -32.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_8channel_1000", + "byTipType": [ + { + "tiprack": "opentrons_flex_96_tiprack_50ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [5.0, -2.1], + [10.0, -1.7], + [50.0, -3.3] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 125.0]], + "correctionByVolume": [ + [5.0, -2.1], + [10.0, -1.7], + [50.0, -3.3] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 10.0], + [10.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 125.0]], + "correctionByVolume": [ + [5.0, -2.1], + [10.0, -1.7], + [50.0, -3.3] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + }, + { + "tiprack": "opentrons_flex_96_tiprack_200ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [5.0, 7.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [5.0, -1.5], + [50.0, -2.2], + [200.0, -7.4] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 125.0]], + "correctionByVolume": [ + [5.0, -1.5], + [50.0, -2.2], + [200.0, -7.4] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 8.0], + [50.0, 4.0], + [200.0, 4.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 125.0]], + "correctionByVolume": [ + [5.0, -1.5], + [50.0, -2.2], + [200.0, -7.4] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + }, + { + "tiprack": "opentrons_flex_96_tiprack_1000ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [10.0, -1.9], + [100.0, -3.6], + [1000.0, -32.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [10.0, -1.9], + [100.0, -3.6], + [1000.0, -32.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [10.0, 6.0], + [100.0, 3.0], + [1000.0, 3.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [10.0, -1.9], + [100.0, -3.6], + [1000.0, -32.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_96channel_1000", + "byTipType": [ + { + "tiprack": "opentrons_flex_96_tiprack_50ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [5.0, -2.1], + [10.0, -1.7], + [50.0, -3.3] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 125.0]], + "correctionByVolume": [ + [5.0, -2.1], + [10.0, -1.7], + [50.0, -3.3] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 10.0], + [10.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 125.0]], + "correctionByVolume": [ + [5.0, -2.1], + [10.0, -1.7], + [50.0, -3.3] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + }, + { + "tiprack": "opentrons_flex_96_tiprack_200ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [5.0, 7.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [5.0, -1.5], + [50.0, -2.2], + [200.0, -7.4] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 125.0]], + "correctionByVolume": [ + [5.0, -1.5], + [50.0, -2.2], + [200.0, -7.4] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 8.0], + [50.0, 4.0], + [200.0, 4.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 125.0]], + "correctionByVolume": [ + [5.0, -1.5], + [50.0, -2.2], + [200.0, -7.4] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + }, + { + "tiprack": "opentrons_flex_96_tiprack_1000ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [10.0, -1.9], + [100.0, -3.6], + [1000.0, -32.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 200.0]], + "correctionByVolume": [ + [10.0, -1.9], + [100.0, -3.6], + [1000.0, -32.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [10.0, 6.0], + [100.0, 3.0], + [1000.0, 3.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 200.0]], + "correctionByVolume": [ + [10.0, -1.9], + [100.0, -3.6], + [1000.0, -32.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + } + ] + } + ] +} diff --git a/shared-data/liquid-class/definitions/1/glycerol_50.json b/shared-data/liquid-class/definitions/1/glycerol_50.json new file mode 100644 index 00000000000..0cb8dad21b8 --- /dev/null +++ b/shared-data/liquid-class/definitions/1/glycerol_50.json @@ -0,0 +1,2462 @@ +{ + "liquidClassName": "glycerol_50", + "displayName": "Glycerol-50", + "schemaVersion": 1, + "namespace": "opentrons", + "byPipette": [ + { + "pipetteModel": "flex_1channel_50", + "byTipType": [ + { + "tiprack": "opentrons_flex_96_tiprack_50ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 50.0] + ], + "correctionByVolume": [ + [1.0, -0.5], + [10.0, -0.2], + [50.0, -0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [1.0, -0.5], + [10.0, -0.2], + [50.0, -0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 11.7], + [4.999, 11.7], + [5.0, 3.9], + [50.0, 3.9] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [1.0, -0.5], + [10.0, -0.2], + [50.0, -0.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_8channel_50", + "byTipType": [ + { + "tiprack": "opentrons_flex_96_tiprack_50ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 50.0] + ], + "correctionByVolume": [ + [1.0, -0.5], + [10.0, -0.2], + [50.0, -0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [1.0, -0.5], + [10.0, -0.2], + [50.0, -0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 11.7], + [4.999, 11.7], + [5.0, 3.9], + [50.0, 3.9] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [1.0, -0.5], + [10.0, -0.2], + [50.0, -0.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_1channel_1000", + "byTipType": [ + { + "tiprack": "opentrons_flex_96_tiprack_50ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [5.0, -0.3], + [10.0, -0.2], + [50.0, 0.0] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [5.0, -0.3], + [10.0, -0.2], + [50.0, 0.0] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 30.0], + [10.0, 20.0], + [50.0, 20.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [5.0, -0.3], + [10.0, -0.2], + [50.0, 0.0] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons_flex_96_tiprack_200ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [5.0, 10.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [5.0, -0.3], + [50.0, -0.3], + [200.0, -0.8] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [5.0, -0.3], + [50.0, -0.3], + [200.0, -0.8] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 20.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [5.0, -0.3], + [50.0, -0.3], + [200.0, -0.8] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons_flex_96_tiprack_1000ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [1000.0, 800.0] + ], + "correctionByVolume": [ + [10.0, -0.2], + [100.0, -0.1], + [1000.0, -2.5] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.7 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [10.0, -0.2], + [100.0, -0.1], + [1000.0, -2.5] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 35.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [10.0, -0.2], + [100.0, -0.1], + [1000.0, -2.5] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_8channel_1000", + "byTipType": [ + { + "tiprack": "opentrons_flex_96_tiprack_50ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [5.0, -0.3], + [10.0, -0.2], + [50.0, 0.0] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [5.0, -0.3], + [10.0, -0.2], + [50.0, 0.0] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 30.0], + [10.0, 20.0], + [50.0, 20.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [5.0, -0.3], + [10.0, -0.2], + [50.0, 0.0] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons_flex_96_tiprack_200ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [5.0, 10.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [5.0, -0.3], + [50.0, -0.3], + [200.0, -0.8] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [5.0, -0.3], + [50.0, -0.3], + [200.0, -0.8] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 20.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [5.0, -0.3], + [50.0, -0.3], + [200.0, -0.8] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons_flex_96_tiprack_1000ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [1000.0, 800.0] + ], + "correctionByVolume": [ + [10.0, -0.2], + [100.0, -0.1], + [1000.0, -2.5] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.7 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [10.0, -0.2], + [100.0, -0.1], + [1000.0, -2.5] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 35.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [10.0, -0.2], + [100.0, -0.1], + [1000.0, -2.5] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_96channel_1000", + "byTipType": [ + { + "tiprack": "opentrons_flex_96_tiprack_50ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [5.0, -0.3], + [10.0, -0.2], + [50.0, 0.0] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [5.0, -0.3], + [10.0, -0.2], + [50.0, 0.0] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 30.0], + [10.0, 20.0], + [50.0, 20.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [5.0, -0.3], + [10.0, -0.2], + [50.0, 0.0] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons_flex_96_tiprack_200ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [5.0, 10.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [5.0, -0.3], + [50.0, -0.3], + [200.0, -0.8] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [5.0, -0.3], + [50.0, -0.3], + [200.0, -0.8] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 20.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [5.0, -0.3], + [50.0, -0.3], + [200.0, -0.8] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons_flex_96_tiprack_1000ul", + "aspirate": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [200.0, 200.0], + [1000.0, 200.0] + ], + "correctionByVolume": [ + [10.0, -0.2], + [100.0, -0.1], + [1000.0, -2.5] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.7 + } + } + }, + "singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [10.0, -0.2], + [100.0, -0.1], + [1000.0, -2.5] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 35.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmToEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + }, + "flowRateByVolume": [[0.0, 200.0]], + "correctionByVolume": [ + [10.0, -0.2], + [100.0, -0.1], + [1000.0, -2.5] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + } + ] + } + ] +} From d37b151f743c4608a2bc9f866c3cfebfaff612f5 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 16 Dec 2024 10:21:05 -0500 Subject: [PATCH 11/25] fix(app): remove marginLeft from ExternalLink component (#17112) * fix(app): remove marginLeft from ExternalLink component --- app/src/atoms/Link/ExternalLink.tsx | 21 ++++++++++++++----- .../Link/__tests__/ExternalLink.test.tsx | 1 - 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/atoms/Link/ExternalLink.tsx b/app/src/atoms/Link/ExternalLink.tsx index 5d24a06fdb4..2490266927e 100644 --- a/app/src/atoms/Link/ExternalLink.tsx +++ b/app/src/atoms/Link/ExternalLink.tsx @@ -1,22 +1,33 @@ -import type * as React from 'react' +import { css } from 'styled-components' +import { + DISPLAY_INLINE_BLOCK, + Icon, + Link, + SPACING, + TYPOGRAPHY, +} from '@opentrons/components' -import { Link, Icon, TYPOGRAPHY, SPACING } from '@opentrons/components' +import type { ReactNode } from 'react' import type { LinkProps } from '@opentrons/components' - export interface ExternalLinkProps extends LinkProps { href: string id?: string - children: React.ReactNode + children: ReactNode } export const ExternalLink = (props: ExternalLinkProps): JSX.Element => ( {props.children} + ) + +const SPAN_STYLE = css` + display: ${DISPLAY_INLINE_BLOCK}; + width: 0.4375rem; +` diff --git a/app/src/atoms/Link/__tests__/ExternalLink.test.tsx b/app/src/atoms/Link/__tests__/ExternalLink.test.tsx index e245541c514..f58ad595169 100644 --- a/app/src/atoms/Link/__tests__/ExternalLink.test.tsx +++ b/app/src/atoms/Link/__tests__/ExternalLink.test.tsx @@ -38,6 +38,5 @@ describe('ExternalLink', () => { const icon = screen.getByLabelText('open_in_new_icon') expect(icon).toBeInTheDocument() expect(icon).toHaveStyle('width: 0.5rem; height: 0.5rem') - expect(icon).toHaveStyle('margin-left: 0.4375rem') }) }) From c2d4908a93501172ea5e5d903733caa34a1b9a0b Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Mon, 16 Dec 2024 13:12:08 -0500 Subject: [PATCH 12/25] chore(release): internal release notes ot3@2.3.0-alpha.2 (#17114) --- api/release-notes-internal.md | 4 ++++ app-shell/build/release-notes-internal.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index ac762aebec4..bc5398781e4 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -2,6 +2,10 @@ For more details about this release, please see the full [technical change log][ [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.3.0-alpha.2 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.3.0. It's for internal testing only. + ## Internal Release 2.3.0-alpha.1 This internal release, pulled from the `edge` branch, contains features being developed for 8.3.0. It's for internal testing only. diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md index 5459cc2593e..565c5e1aa9b 100644 --- a/app-shell/build/release-notes-internal.md +++ b/app-shell/build/release-notes-internal.md @@ -1,6 +1,10 @@ For more details about this release, please see the full [technical changelog][]. [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.3.0-alpha.2 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.3.0. It's for internal testing only. + ## Internal Release 2.3.0-alpha.1 This internal release, pulled from the `edge` branch, contains features being developed for 8.3.0. It's for internal testing only. From 0540c01f9f877453b9551018bf89a55d9f56fc0d Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Mon, 16 Dec 2024 13:39:50 -0500 Subject: [PATCH 13/25] test(api): install opentrons and simulate (#17048) --- .github/workflows/api-test-lint-deploy.yaml | 56 ++++++- .gitignore | 2 + package-testing/Makefile | 43 ++++++ package-testing/README.md | 34 +++++ package-testing/help.ps1 | 69 +++++++++ package-testing/help.sh | 48 ++++++ package-testing/run_tests.py | 154 ++++++++++++++++++++ package-testing/setup.ps1 | 42 ++++++ package-testing/setup.sh | 35 +++++ package-testing/simulate.ps1 | 45 ++++++ package-testing/simulate.sh | 48 ++++++ 11 files changed, 574 insertions(+), 2 deletions(-) create mode 100644 package-testing/Makefile create mode 100644 package-testing/README.md create mode 100644 package-testing/help.ps1 create mode 100755 package-testing/help.sh create mode 100644 package-testing/run_tests.py create mode 100644 package-testing/setup.ps1 create mode 100755 package-testing/setup.sh create mode 100644 package-testing/simulate.ps1 create mode 100755 package-testing/simulate.sh diff --git a/.github/workflows/api-test-lint-deploy.yaml b/.github/workflows/api-test-lint-deploy.yaml index 6297cb7c747..e1790f28f20 100644 --- a/.github/workflows/api-test-lint-deploy.yaml +++ b/.github/workflows/api-test-lint-deploy.yaml @@ -73,8 +73,6 @@ jobs: strategy: matrix: os: ['windows-2022', 'ubuntu-22.04', 'macos-latest'] - # TODO(mc, 2022-02-24): expand this matrix to 3.8 and 3.9, - # preferably in a nightly cronjob on edge or something python: ['3.10'] with-ot-hardware: ['true', 'false'] exclude: @@ -128,6 +126,60 @@ jobs: files: ./api/coverage.xml flags: api + test-package: + name: 'installed package tests on ${{ matrix.os }}' + timeout-minutes: 5 + strategy: + matrix: + os: ['ubuntu-22.04', 'macos-latest', 'windows-2022'] + runs-on: '${{ matrix.os }}' + steps: + - uses: 'actions/checkout@v4' + - name: 'Fix actions/checkout odd handling of tags' + if: startsWith(github.ref, 'refs/tags') + run: | + git fetch -f origin ${{ github.ref }}:${{ github.ref }} + git checkout ${{ github.ref }} + - uses: 'actions/setup-python@v4' + with: + python-version: '3.10' + - name: Set up package-testing + id: setup + if: ${{ matrix.os != 'windows-2022' }} + working-directory: package-testing + shell: bash + run: make setup + - name: Set up package-testing (Windows) + id: setup-windows + if: ${{ matrix.os == 'windows-2022' }} + working-directory: package-testing + shell: pwsh + run: make setup-windows + - name: Run the tests + if: ${{ matrix.os != 'windows-2022' }} + shell: bash + id: test + working-directory: package-testing + run: make test + - name: Run the tests (Windows) + shell: pwsh + id: test-windows + working-directory: package-testing + run: make test-windows + - name: Save the test results + if: ${{ always() && steps.setup.outcome == 'success' || steps.setup-windows.outcome == 'success' }} + id: results + uses: actions/upload-artifact@v4 + with: + name: package-test-results-${{ matrix.os }} + path: package-testing/results + - name: Set job summary + if: ${{ always() }} + run: | + echo "## Opentrons Package Test Results ${{matrix.os}}" >> $GITHUB_STEP_SUMMARY + echo "### Test Outcome: Unixy ${{ steps.test.outcome }} Windows: ${{ steps.test-windows.outcome }}" >> $GITHUB_STEP_SUMMARY + echo "[Download the test results artifact](${{steps.results.outputs.artifact-url}})" >> $GITHUB_STEP_SUMMARY + deploy: name: 'deploy opentrons package' needs: [test] diff --git a/.gitignore b/.gitignore index 319ccc32e67..2d8b2ff20cb 100755 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,5 @@ opentrons-robot-app.tar.gz mock_dir .npm-cache/ .eslintcache + +package-testing/results diff --git a/package-testing/Makefile b/package-testing/Makefile new file mode 100644 index 00000000000..6b60cf3401d --- /dev/null +++ b/package-testing/Makefile @@ -0,0 +1,43 @@ +VENV_DIR ?= venv +RESULTS_DIR ?= results +TESTS ?= all + +.PHONY: setup +setup: + @echo "Setting up environment for Unix-like system..." + ./setup.sh $(VENV_DIR) + +.PHONY: setup-windows +setup-windows: + @echo "Setting up environment for Windows..." + pwsh -ExecutionPolicy Bypass -File ./setup.ps1 $(VENV_DIR) + +.PHONY: clean +clean: + @echo "Removing the results directory $(RESULTS_DIR)..." + rm -rf $(RESULTS_DIR) || true + +.PHONY: clean-windows +clean-windows: + @echo "Removing the results directory $(RESULTS_DIR)..." + pwsh -Command "if (Test-Path '$(RESULTS_DIR)') { Remove-Item -Recurse -Force '$(RESULTS_DIR)' }" + +.PHONY: teardown +teardown: clean + rm -rf $(VENV_DIR) || true + +.PHONY: teardown-windows +teardown-windows: clean-windows + pwsh -Command "if (Test-Path '$(VENV_DIR)') { Remove-Item -Recurse -Force '$(VENV_DIR)' }" + + + +.PHONY: test +test: clean + @echo "Running $(TESTS) tests for Unix-like system..." + python run_tests.py $(VENV_DIR) $(RESULTS_DIR) $(TESTS) + +.PHONY: test-windows +test-windows: clean-windows + @echo "Running $(TESTS) tests for Windows..." + python run_tests.py $(VENV_DIR) $(RESULTS_DIR) $(TESTS) diff --git a/package-testing/README.md b/package-testing/README.md new file mode 100644 index 00000000000..88ea89e4e19 --- /dev/null +++ b/package-testing/README.md @@ -0,0 +1,34 @@ +# Test Scripts for the opentrons package + +## Structure + +- Makefile has targets for setting up, tearing down, and running tests for windows and unix-ish systems +- setup.\* is the script run create the virtual environment and install the packages +- help.\* is a script to test --help +- simulate.\* is a script to test that the simulation runs and produces the expected status code +- run_tests.py is the main script that drives test execution and contains the test mapping data + +## Use the tests on Linux and Mac + +1. cd package-testing +2. pyenv local 3.10 +3. make setup - note that this deletes and recreates the virtual environment +4. make test + +## Use the tests on Windows + +- powershell is mapped to pwsh and is version 7 +- python is on the path is version 3.10.\* + +1. cd package-testing +2. make setup-windows - note that this deletes and recreates the virtual environment +3. make test-windows + +## Notes + +- find . -name "\*.sh" -exec shellcheck {} + + +## TODO + +- setup shellcheck and python linting +- more tests diff --git a/package-testing/help.ps1 b/package-testing/help.ps1 new file mode 100644 index 00000000000..277daac5ec8 --- /dev/null +++ b/package-testing/help.ps1 @@ -0,0 +1,69 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS +Validate `opentrons_simulate --help`. + +.PARAMETER TestKey +The test key to identify the test. + +.PARAMETER ExpectedOutput +The expected string to validate in the output of `opentrons_simulate --help`. + +.PARAMETER Venv +The virtual environment directory (default: "venv"). + +.PARAMETER ResultDir +The result directory to store logs (default: "results"). +#> + +param ( + [string]$TestKey, + [string]$ExpectedOutput, + [string]$Venv = "venv", + [string]$ResultDir = "results" +) + +# Ensure the result directory exists +if (-not (Test-Path -Path $ResultDir)) { + New-Item -ItemType Directory -Path $ResultDir | Out-Null +} + +$resultFile = Join-Path -Path $ResultDir -ChildPath "$TestKey.txt" + +Write-Output "Activating virtual environment $Venv..." +$venvActivate = Join-Path -Path $Venv -ChildPath "Scripts/Activate.ps1" +if (-not (Test-Path -Path $venvActivate)) { + Write-Error "FAIL: Virtual environment not found at $venv" + exit 1 +} + +# Source the virtual environment +& $venvActivate + +Write-Output "Validating opentrons_simulate --help for test: $TestKey..." + +# Run the command and capture the output and return code +$output = & opentrons_simulate --help 2>&1 +$returnCode = $LASTEXITCODE + +if ($returnCode -ne 0) { + Write-Output "FAIL: Return code is $returnCode, expected 0" | Tee-Object -FilePath $resultFile + Write-Output "Output was:" | Add-Content -Path $resultFile + $output | Add-Content -Path $resultFile + Rename-Item -Path $resultFile -NewName "${resultFile.Substring(0, $resultFile.Length - 4)}_FAIL.txt" + exit 1 +} + +Write-Output "PASS: Return code is $returnCode, expected 0" | Tee-Object -FilePath $resultFile +Write-Output "Output was:" | Add-Content -Path $resultFile +$output | Add-Content -Path $resultFile + +if ($output -match [regex]::Escape($ExpectedOutput)) { + Write-Output "PASS: Output contains expected string" | Tee-Object -FilePath $resultFile -Append + Write-Output "PASS: Test $TestKey completed successfully." | Tee-Object -FilePath $resultFile -Append + exit 0 +} + +Write-Output "FAIL: Output does not contain expected string" | Tee-Object -FilePath $resultFile -Append +exit 1 diff --git a/package-testing/help.sh b/package-testing/help.sh new file mode 100755 index 00000000000..1d0db6f8f99 --- /dev/null +++ b/package-testing/help.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Function to validate `opentrons_simulate --help` +# Arguments: +# 1. Test Key (required) +# 2. Virtual Environment Directory (optional, default: "venv") +# 3. Result Directory (optional, default: "results") +test_opentrons_simulate_help() { + local test_key="$1" + local expected_output="${2}" + local venv="${3:-"venv"}" + local result_dir="${4:-"results"}" + mkdir -p "$result_dir" + local result_file="$result_dir/$test_key.txt" + + echo "Activating virtual environment $venv..." + # shellcheck disable=SC1091 + source "$venv/bin/activate" + + echo "Validating opentrons_simulate --help for test: $test_key..." + + local output + local return_code + output=$(opentrons_simulate --help 2>&1) + return_code=$? + + if [ $return_code -ne 0 ]; then + echo "FAIL: Return code is $return_code, expected 0" | tee "$result_file" + echo "Output was:" >> "$result_file" + echo "$output" >> "$result_file" + mv "$result_file" "${result_file%.txt}_FAIL.txt" + return 1 + fi + + echo "PASS: Return code is $return_code, expected 0" | tee "$result_file" + echo "Output was:" >> "$result_file" + echo "$output" >> "$result_file" + + if echo "$output" | grep -q "$expected_output"; then + echo "PASS: Output contains expected string" | tee -a "$result_file" + echo "PASS: Test $test_key completed successfully." | tee -a "$result_file" + return 0 + fi + echo "FAIL: Output does not contain expected string" | tee -a "$result_file" + return 1 +} + +test_opentrons_simulate_help "$@" \ No newline at end of file diff --git a/package-testing/run_tests.py b/package-testing/run_tests.py new file mode 100644 index 00000000000..ce67266df78 --- /dev/null +++ b/package-testing/run_tests.py @@ -0,0 +1,154 @@ +from dataclasses import dataclass +import os +import platform +import subprocess +import sys +from typing import List, Union + + +@dataclass +class TestConfig: + test_key: str + test_helper: str + + +@dataclass +class TestHelp(TestConfig): + expected_output: str + + +@dataclass +class TestSimulate(TestConfig): + protocol_path: str + expected_return_code: str + + +tests: List[Union[TestHelp, TestSimulate]] = [ + TestHelp( + test_key="help", + test_helper="help", + expected_output="Simulate a protocol for an Opentrons robot" + ), + TestSimulate( + test_key="Flex_v2_19_expect_success", + test_helper="simulate", + protocol_path="../analyses-snapshot-testing/files/protocols/Flex_S_v2_19_Illumina_DNA_Prep_48x.py", + expected_return_code="0", + ), + TestSimulate( + test_key="OT2_v2_20_expect_success", + test_helper="simulate", + protocol_path="../analyses-snapshot-testing/files/protocols/OT2_S_v2_20_8_None_SINGLE_HappyPath.py", + expected_return_code="0", + ), + TestSimulate( + test_key="Flex_v2_16_expect_error", + test_helper="simulate", + protocol_path="../analyses-snapshot-testing/files/protocols/Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict.py", + expected_return_code="1", + ), + TestSimulate( + test_key="OT2_v6PD_expect_error", + test_helper="simulate", + protocol_path="../analyses-snapshot-testing/files/protocols/OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods.json", + expected_return_code="1", + ), +] + + +def get_os_type() -> str: + """Determine the current operating system type.""" + return "windows_nt" if "windows" in platform.system().lower() else "unix" + + +def build_command(test: TestConfig, script_ext: str, venv_dir: str, results_dir: str) -> List[str]: + """ + Build the command to run a test based on the test type. + + Args: + test: The test object (either TestHelp or TestSimulate). + script_ext: The script file extension (e.g., "ps1", "sh"). + venv_dir: The virtual environment directory. + results_dir: The directory for storing test results. + + Returns: + A list of strings representing the command to execute. + + Raises: + ValueError: If the test type is unknown. + """ + prefix = ["pwsh", "-File"] if get_os_type() == "windows_nt" else [] + + # Build command based on test type + if isinstance(test, TestHelp): + return prefix + [ + f"./{test.test_helper}.{script_ext}", + test.test_key, + test.expected_output, + venv_dir, + results_dir, + ] + elif isinstance(test, TestSimulate): + return prefix + [ + f"./{test.test_helper}.{script_ext}", + test.test_key, + test.protocol_path, + test.expected_return_code, + venv_dir, + results_dir, + ] + else: + raise ValueError(f"Unknown test type: {type(test)}") + + +def run_test(test: TestConfig, script_ext: str, venv_dir: str, results_dir: str): + """ + Run a single test. + + Args: + test: The test object to run. + script_ext: The script extension (e.g., "ps1", "sh"). + venv_dir: The virtual environment directory. + results_dir: The directory to store test results. + """ + # Ensure results directory exists + os.makedirs(results_dir, exist_ok=True) + + # Build and execute command + command = build_command(test, script_ext, venv_dir, results_dir) + print(f"Running {test.test_key} using {test.test_helper}.{script_ext}...") + try: + subprocess.run(command, check=True, shell=False) + print(f"Test {test.test_key} passed.") + except subprocess.CalledProcessError as e: + print(f"Test {test.test_key} failed: {e}") + sys.exit(1) + + +def main(): + if len(sys.argv) < 4: + print("Usage: python run_tests.py ") + sys.exit(1) + + venv_dir, results_dir, tests_arg = sys.argv[1:4] + os_type = get_os_type() + script_ext = "ps1" if os_type == "windows_nt" else "sh" + + # Filter tests based on user input + if tests_arg.lower() == "all": + selected_tests = tests + else: + requested_keys = tests_arg.split(",") + selected_tests = [test for test in tests if test.test_key in requested_keys] + + if not selected_tests: + print(f"No matching tests found for the provided test keys: {tests_arg}") + sys.exit(1) + + # Run selected tests + for test in selected_tests: + run_test(test, script_ext, venv_dir, results_dir) + + +if __name__ == "__main__": + main() diff --git a/package-testing/setup.ps1 b/package-testing/setup.ps1 new file mode 100644 index 00000000000..afb2258d2b8 --- /dev/null +++ b/package-testing/setup.ps1 @@ -0,0 +1,42 @@ +# Exit immediately on any errors +$ErrorActionPreference = "Stop" + +$VENV_DIR = $null -ne $env:VENV_DIR ? $env:VENV_DIR : "venv" + + +if (Test-Path -Path $VENV_DIR) { + Write-Output "Removing existing virtual environment..." + Remove-Item -Recurse -Force -Path $VENV_DIR +} + +Write-Output "Creating virtual environment in $VENV_DIR..." +python -m venv $VENV_DIR + +Write-Output "Activating virtual environment..." +if ($IsWindows) { + . "$VENV_DIR\Scripts\Activate.ps1" +} else { + . "$VENV_DIR/bin/activate" +} + +Write-Output "Installing packages..." +pip install -U ../shared-data/python ../api + +Write-Output "Validating that opentrons-hardware is not installed..." +$pipList = pip list 2>&1 +if ($pipList -match "opentrons-hardware") { + Write-Output "FAIL: opentrons-hardware is installed" + exit 1 +} else { + Write-Output "PASS: opentrons-hardware is not installed" +} + +Write-Output "Packages installed successfully." +pip list + +Write-Output "To activate the virtual environment, run:" +if ($IsWindows) { + Write-Output ".\$VENV_DIR\Scripts\Activate.ps1" +} else { + Write-Output "source $VENV_DIR/bin/activate" +} diff --git a/package-testing/setup.sh b/package-testing/setup.sh new file mode 100755 index 00000000000..9eefa990cde --- /dev/null +++ b/package-testing/setup.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Exit immediately if a command exits with a non-zero status +set -e + +VENV_DIR=${VENV_DIR:-"venv"} + +if [ -d "$VENV_DIR" ]; then + echo "Removing existing virtual environment..." + rm -rf "$VENV_DIR" +fi + +echo "Creating virtual environment in $VENV_DIR..." +python -m venv "$VENV_DIR" + +echo "Activating virtual environment..." +# shellcheck disable=SC1091 +source "$VENV_DIR/bin/activate" + +echo "Installing packages..." +pip install -U ../shared-data/python ../api # add ../hardware here to validate the below check + +echo "Validate opentrons-hardware is not installed..." +if pip list 2>/dev/null | grep -q "opentrons-hardware"; then + echo "FAIL: opentrons-hardware is installed" + exit 1 +else + echo "PASS: opentrons-hardware is not installed" +fi + +echo "Packages installed successfully." +pip list + +echo "To activate the virtual environment, run:" +echo "source $VENV_DIR/bin/activate" diff --git a/package-testing/simulate.ps1 b/package-testing/simulate.ps1 new file mode 100644 index 00000000000..ecbb3289b02 --- /dev/null +++ b/package-testing/simulate.ps1 @@ -0,0 +1,45 @@ +param ( + [Parameter(Mandatory = $true)][string]$TestKey, + [Parameter(Mandatory = $true)][string]$ProtocolFilePath, + [Parameter(Mandatory = $true)][int]$ExpectedReturnCode, + [string]$Venv = "venv", + [string]$ResultDir = "results" +) + +# Ensure result directory exists +if (-Not (Test-Path -Path $ResultDir)) { + Write-Output "Creating result directory at $ResultDir..." + New-Item -ItemType Directory -Path $ResultDir | Out-Null +} + +# Construct result file path +$ResultFile = Join-Path -Path $ResultDir -ChildPath "$TestKey.txt" + +Write-Output "Activating virtual environment $Venv..." +if (Test-Path -Path "$Venv/Scripts/Activate.ps1") { + . "$Venv/Scripts/Activate.ps1" +} else { + Write-Error "Virtual environment not found at $Venv." + exit 1 +} + +Write-Output "Running opentrons_simulate for protocol:" +Write-Output $ProtocolFilePath + +# Run opentrons_simulate and capture output +$Output = & opentrons_simulate $ProtocolFilePath 2>&1 +$ReturnCode = $LASTEXITCODE + +# Validate return code +if ($ReturnCode -ne $ExpectedReturnCode) { + Write-Output "FAIL: Return code is $ReturnCode, expected $ExpectedReturnCode" | Tee-Object -FilePath $ResultFile + Add-Content -Path $ResultFile -Value "Output was:" + Add-Content -Path $ResultFile -Value $Output + Rename-Item -Path $ResultFile -NewName "$($ResultFile -replace '\.txt$', '_FAIL.txt')" + exit 1 +} else { + Write-Output "PASS: Return code is $ReturnCode, expected $ExpectedReturnCode" | Tee-Object -FilePath $ResultFile + Add-Content -Path $ResultFile -Value "Output was:" + Add-Content -Path $ResultFile -Value $Output + exit 0 +} diff --git a/package-testing/simulate.sh b/package-testing/simulate.sh new file mode 100755 index 00000000000..6eb7039e06c --- /dev/null +++ b/package-testing/simulate.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Function to test `opentrons_simulate` with a given protocol file +# Arguments: +# 1. Test Key (required) +# 2. Protocol File Path (required) +# 3. Expected return code (required) +# 4. Virtual Environment Directory (optional) +# 5. Result Directory (optional) +simulate_protocol() { + local test_key="$1" + local protocol_file_path="$2" + local expected_return_code="$3" + local venv="${4:-"venv"}" + local result_dir="${5:-"results"}" + mkdir -p "$result_dir" + local result_file="$result_dir/$test_key.txt" + + # echo "test_key: $test_key, protocol_file_path: $protocol_file_path, expected_return_code: $expected_return_code, venv: $venv, result_dir:$result_dir" + # Fail fast if protocol file does not exist + if [ ! -f "$protocol_file_path" ]; then + echo "FAIL: Protocol file not found: $protocol_file_path" + exit 1 + fi + echo "Activating virtual environment $venv ..." + # shellcheck disable=SC1091 + source "$venv/bin/activate" + + printf "Running opentrons_simulate for protocol:\n %s\n" "$protocol_file_path" + + output=$(opentrons_simulate "$protocol_file_path" 2>&1) + return_code=$? + + if [ $return_code -ne "$expected_return_code" ]; then + echo "FAIL: Return code is $return_code, expected $expected_return_code" | tee "$result_file" + echo "Output was:" >> "$result_file" + echo "$output" >> "$result_file" + mv "$result_file" "${result_file%.txt}_FAIL.txt" + exit 1 + else + echo "PASS: Return code is $return_code, expected $expected_return_code" | tee "$result_file" + echo "Output was:" >> "$result_file" + echo "$output" >> "$result_file" + exit 0 + fi +} + +simulate_protocol "$@" From f22ed4167c343c2c78c477a5a3b69aac860663ac Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Mon, 16 Dec 2024 16:34:33 -0500 Subject: [PATCH 14/25] feat(engine): add mmFromEdge parameter to touchTip (#17107) Adds a new optional parameter `mmFromEdge` to the protocol engine `touchTip` command, primarily for usage for transfering liquids defined by a liquid class. --- .../protocol_engine/commands/touch_tip.py | 19 ++- .../protocol_engine/errors/__init__.py | 2 + .../protocol_engine/errors/exceptions.py | 13 +++ .../protocol_engine/state/_move_types.py | 14 ++- .../opentrons/protocol_engine/state/motion.py | 7 +- .../commands/test_touch_tip.py | 110 +++++++++++++++++- .../protocol_engine/state/test_motion_view.py | 2 + .../protocol_engine/state/test_move_types.py | 28 +++-- shared-data/command/schemas/11.json | 5 + 9 files changed, 180 insertions(+), 20 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/touch_tip.py b/api/src/opentrons/protocol_engine/commands/touch_tip.py index 2d7c507d321..2eefd572d9a 100644 --- a/api/src/opentrons/protocol_engine/commands/touch_tip.py +++ b/api/src/opentrons/protocol_engine/commands/touch_tip.py @@ -7,7 +7,11 @@ from opentrons.types import Point -from ..errors import TouchTipDisabledError, LabwareIsTipRackError +from ..errors import ( + TouchTipDisabledError, + TouchTipIncompatibleArgumentsError, + LabwareIsTipRackError, +) from ..types import DeckPoint from .command import ( AbstractCommandImpl, @@ -45,6 +49,12 @@ class TouchTipParams(PipetteIdMixin, WellLocationMixin): ), ) + mmFromEdge: Optional[float] = Field( + None, + description="Offset away from the the well edge, in millimeters." + "Incompatible when a radius is included as a non 1.0 value.", + ) + speed: Optional[float] = Field( None, description=( @@ -89,6 +99,11 @@ async def execute( labware_id = params.labwareId well_name = params.wellName + if params.radius != 1.0 and params.mmFromEdge is not None: + raise TouchTipIncompatibleArgumentsError( + "Cannot use mmFromEdge with a radius that is not 1.0" + ) + if self._state_view.labware.get_has_quirk(labware_id, "touchTipDisabled"): raise TouchTipDisabledError( f"Touch tip not allowed on labware {labware_id}" @@ -112,11 +127,13 @@ async def execute( pipette_id, params.speed ) + mm_from_edge = params.mmFromEdge if params.mmFromEdge is not None else 0 touch_waypoints = self._state_view.motion.get_touch_tip_waypoints( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, radius=params.radius, + mm_from_edge=mm_from_edge, center_point=Point( center_result.public.position.x, center_result.public.position.y, diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index 8148ce132e6..2b0fb6a6060 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -24,6 +24,7 @@ LabwareIsTipRackError, LabwareIsAdapterError, TouchTipDisabledError, + TouchTipIncompatibleArgumentsError, WellDoesNotExistError, PipetteNotLoadedError, ModuleNotLoadedError, @@ -110,6 +111,7 @@ "LabwareIsTipRackError", "LabwareIsAdapterError", "TouchTipDisabledError", + "TouchTipIncompatibleArgumentsError", "WellDoesNotExistError", "PipetteNotLoadedError", "ModuleNotLoadedError", diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 563a1fb816d..c3fddf99a61 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -361,6 +361,19 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) +class TouchTipIncompatibleArgumentsError(ProtocolEngineError): + """Raised when touch tip is used with both a custom radius and a mmFromEdge argument.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a TouchTipIncompatibleArgumentsError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + class WellDoesNotExistError(ProtocolEngineError): """Raised when referencing a well that does not exist.""" diff --git a/api/src/opentrons/protocol_engine/state/_move_types.py b/api/src/opentrons/protocol_engine/state/_move_types.py index b8dcb28bd8d..94201ffead9 100644 --- a/api/src/opentrons/protocol_engine/state/_move_types.py +++ b/api/src/opentrons/protocol_engine/state/_move_types.py @@ -53,15 +53,19 @@ def get_move_type_to_well( def get_edge_point_list( - center: Point, x_radius: float, y_radius: float, edge_path_type: EdgePathType + center: Point, + x_radius: float, + y_radius: float, + mm_from_edge: float, + edge_path_type: EdgePathType, ) -> List[Point]: """Get list of edge points dependent on edge path type.""" edges = EdgeList( - right=center + Point(x=x_radius, y=0, z=0), - left=center + Point(x=-x_radius, y=0, z=0), + right=center + Point(x=x_radius - mm_from_edge, y=0, z=0), + left=center + Point(x=-x_radius + mm_from_edge, y=0, z=0), center=center, - forward=center + Point(x=0, y=y_radius, z=0), - back=center + Point(x=0, y=-y_radius, z=0), + forward=center + Point(x=0, y=y_radius - mm_from_edge, z=0), + back=center + Point(x=0, y=-y_radius + mm_from_edge, z=0), ) if edge_path_type == EdgePathType.LEFT: diff --git a/api/src/opentrons/protocol_engine/state/motion.py b/api/src/opentrons/protocol_engine/state/motion.py index 0863c42a0c1..855025d01b6 100644 --- a/api/src/opentrons/protocol_engine/state/motion.py +++ b/api/src/opentrons/protocol_engine/state/motion.py @@ -327,6 +327,7 @@ def get_touch_tip_waypoints( labware_id: str, well_name: str, center_point: Point, + mm_from_edge: float = 0, radius: float = 1.0, ) -> List[motion_planning.Waypoint]: """Get a list of touch points for a touch tip operation.""" @@ -346,7 +347,11 @@ def get_touch_tip_waypoints( ) positions = _move_types.get_edge_point_list( - center_point, x_offset, y_offset, edge_path_type + center=center_point, + x_radius=x_offset, + y_radius=y_offset, + mm_from_edge=mm_from_edge, + edge_path_type=edge_path_type, ) critical_point: Optional[CriticalPoint] = None diff --git a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py index 0d4071efd6c..5756810c9ee 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py @@ -102,8 +102,99 @@ async def test_touch_tip_implementation( pipette_id="abc", labware_id="123", well_name="A3", - center_point=Point(x=1, y=2, z=3), radius=0.456, + mm_from_edge=0, + center_point=Point(x=1, y=2, z=3), + ) + ).then_return( + [ + Waypoint( + position=Point(x=11, y=22, z=33), + critical_point=CriticalPoint.XY_CENTER, + ), + Waypoint( + position=Point(x=44, y=55, z=66), + critical_point=CriticalPoint.XY_CENTER, + ), + ] + ) + + decoy.when( + await mock_gantry_mover.move_to( + pipette_id="abc", + waypoints=[ + Waypoint( + position=Point(x=11, y=22, z=33), + critical_point=CriticalPoint.XY_CENTER, + ), + Waypoint( + position=Point(x=44, y=55, z=66), + critical_point=CriticalPoint.XY_CENTER, + ), + ], + speed=9001, + ) + ).then_return(Point(x=4, y=5, z=6)) + + result = await subject.execute(params) + + assert result == SuccessData( + public=TouchTipResult(position=DeckPoint(x=4, y=5, z=6)), + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="abc", + new_location=update_types.Well(labware_id="123", well_name="A3"), + new_deck_point=DeckPoint(x=4, y=5, z=6), + ) + ), + ) + + +async def test_touch_tip_implementation_with_mm_to_edge( + decoy: Decoy, + mock_state_view: StateView, + mock_movement_handler: MovementHandler, + mock_gantry_mover: GantryMover, + subject: TouchTipImplementation, +) -> None: + """A TouchTip command should use mmFromEdge if provided.""" + params = TouchTipParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + mmFromEdge=0.789, + speed=42.0, + ) + + decoy.when( + await mock_movement_handler.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_return(Point(x=1, y=2, z=3)) + + decoy.when( + mock_state_view.pipettes.get_movement_speed( + pipette_id="abc", requested_speed=42.0 + ) + ).then_return(9001) + + decoy.when( + mock_state_view.motion.get_touch_tip_waypoints( + pipette_id="abc", + labware_id="123", + well_name="A3", + radius=1.0, + mm_from_edge=0.789, + center_point=Point(x=1, y=2, z=3), ) ).then_return( [ @@ -183,3 +274,20 @@ async def test_touch_tip_no_tip_racks( with pytest.raises(errors.LabwareIsTipRackError): await subject.execute(params) + + +async def test_touch_tip_incompatible_arguments( + decoy: Decoy, mock_state_view: StateView, subject: TouchTipImplementation +) -> None: + """It should disallow touch tip if radius and mmFromEdge is provided.""" + params = TouchTipParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=WellLocation(), + radius=1.23, + mmFromEdge=4.56, + ) + + with pytest.raises(errors.TouchTipIncompatibleArgumentsError): + await subject.execute(params) diff --git a/api/tests/opentrons/protocol_engine/state/test_motion_view.py b/api/tests/opentrons/protocol_engine/state/test_motion_view.py index 9e7307f29a7..3e9d60da79a 100644 --- a/api/tests/opentrons/protocol_engine/state/test_motion_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_motion_view.py @@ -928,6 +928,7 @@ def test_get_touch_tip_waypoints( x_radius=1.2, y_radius=3.4, edge_path_type=_move_types.EdgePathType.RIGHT, + mm_from_edge=0.456, ) ).then_return([Point(x=11, y=22, z=33), Point(x=44, y=55, z=66)]) @@ -937,6 +938,7 @@ def test_get_touch_tip_waypoints( well_name="B2", center_point=center_point, radius=0.123, + mm_from_edge=0.456, ) assert result == [ diff --git a/api/tests/opentrons/protocol_engine/state/test_move_types.py b/api/tests/opentrons/protocol_engine/state/test_move_types.py index 9d46cb8a1ab..27f43ffbc0d 100644 --- a/api/tests/opentrons/protocol_engine/state/test_move_types.py +++ b/api/tests/opentrons/protocol_engine/state/test_move_types.py @@ -53,43 +53,47 @@ def test_get_move_type_to_well( ( subject.EdgePathType.LEFT, [ - Point(5, 20, 30), + Point(8, 20, 30), Point(10, 20, 30), - Point(10, 30, 30), - Point(10, 10, 30), + Point(10, 27, 30), + Point(10, 13, 30), Point(10, 20, 30), ], ), ( subject.EdgePathType.RIGHT, [ - Point(15, 20, 30), + Point(12, 20, 30), Point(10, 20, 30), - Point(10, 30, 30), - Point(10, 10, 30), + Point(10, 27, 30), + Point(10, 13, 30), Point(10, 20, 30), ], ), ( subject.EdgePathType.DEFAULT, [ - Point(15, 20, 30), - Point(5, 20, 30), + Point(12, 20, 30), + Point(8, 20, 30), Point(10, 20, 30), - Point(10, 30, 30), - Point(10, 10, 30), + Point(10, 27, 30), + Point(10, 13, 30), Point(10, 20, 30), ], ), ], ) -def get_edge_point_list( +def test_get_edge_point_list( edge_path_type: subject.EdgePathType, expected_result: List[Point], ) -> None: """It should get a list of well edge points.""" result = subject.get_edge_point_list( - Point(x=10, y=20, z=30), x_radius=5, y_radius=10, edge_path_type=edge_path_type + Point(x=10, y=20, z=30), + x_radius=5, + y_radius=10, + mm_from_edge=3, + edge_path_type=edge_path_type, ) assert result == expected_result diff --git a/shared-data/command/schemas/11.json b/shared-data/command/schemas/11.json index 955dff87690..5bfd3569c0c 100644 --- a/shared-data/command/schemas/11.json +++ b/shared-data/command/schemas/11.json @@ -3924,6 +3924,11 @@ "default": 1.0, "type": "number" }, + "mmFromEdge": { + "title": "Mmfromedge", + "description": "Offset away from the the well edge, in millimeters.Incompatible when a radius is included as a non 1.0 value.", + "type": "number" + }, "speed": { "title": "Speed", "description": "Override the travel speed in mm/s. This controls the straight linear speed of motion.", From d69ca2b413034c5f1ac3c7053e86829fc999f449 Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Mon, 16 Dec 2024 17:58:59 -0500 Subject: [PATCH 15/25] refactor(app): add any found hardcoded copy to i18n for translation (#17095) fix EXEC-879, EXEC-878 --- app/src/App/DesktopApp.tsx | 22 +- app/src/App/Navbar.tsx | 5 +- app/src/assets/localization/en/anonymous.json | 1 + .../assets/localization/en/app_settings.json | 6 + app/src/assets/localization/en/branded.json | 1 + .../localization/en/device_settings.json | 39 ++ .../localization/en/gripper_wizard_flows.json | 2 - .../localization/en/pipette_wizard_flows.json | 1 + .../en/protocol_command_text.json | 5 + .../localization/en/top_navigation.json | 6 + .../getConfigureNozzleLayoutCommandText.ts | 11 +- .../Desktop/Alerts/U2EDriverOutdatedAlert.tsx | 27 +- .../ConfigurePipette/ConfigMessage.tsx | 19 - .../AdvancedTab/UpdateRobotSoftware.tsx | 2 +- .../ConnectNetwork/ConnectModal/FormModal.tsx | 29 +- .../ConnectModal/KeyFileField.tsx | 13 +- .../ConnectModal/SecurityField.tsx | 9 +- .../ConnectNetwork/ConnectModal/TextField.tsx | 5 +- .../__tests__/form-fields.test.ts | 379 ---------------- .../__tests__/form-fields.test.tsx | 422 ++++++++++++++++++ .../ConnectModal/form-fields.ts | 50 ++- .../ConnectNetwork/ConnectModal/index.tsx | 15 +- .../ConnectNetwork/ResultModal.tsx | 31 +- .../ConnectNetwork/SelectSsid/index.tsx | 21 +- .../RobotSettings/ConnectNetwork/i18n.ts | 103 ----- .../UpdateBuildroot/UpdateRobotModal.tsx | 2 +- .../GripperWizardFlows/BeforeBeginning.tsx | 6 +- .../organisms/GripperWizardFlows/constants.ts | 4 + .../QuickTransferFlow/NameQuickTransfer.tsx | 1 - .../PipetteWizardFlows/BeforeBeginning.tsx | 14 +- .../organisms/PipetteWizardFlows/constants.ts | 5 +- app/src/pages/Desktop/Labware/index.tsx | 1 + .../robot-update/__tests__/selectors.test.ts | 18 +- app/src/redux/robot-update/selectors.ts | 22 +- app/src/redux/system-info/constants.ts | 9 - shared-data/js/fixtures.ts | 2 +- 36 files changed, 649 insertions(+), 659 deletions(-) delete mode 100644 app/src/organisms/Desktop/Devices/ConfigurePipette/ConfigMessage.tsx delete mode 100644 app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.ts create mode 100644 app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.tsx delete mode 100644 app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/i18n.ts diff --git a/app/src/App/DesktopApp.tsx b/app/src/App/DesktopApp.tsx index 029ec99ee26..196a6cf547c 100644 --- a/app/src/App/DesktopApp.tsx +++ b/app/src/App/DesktopApp.tsx @@ -1,4 +1,5 @@ import { useState, Fragment } from 'react' +import { useTranslation } from 'react-i18next' import { Navigate, Route, Routes, useMatch } from 'react-router-dom' import { ErrorBoundary } from 'react-error-boundary' import { @@ -43,6 +44,7 @@ import { useFeatureFlag } from '../redux/config' import type { RouteProps } from './types' export const DesktopApp = (): JSX.Element => { + const { t } = useTranslation('top_navigation') useSoftwareUpdatePoll() const [ isEmergencyStopModalDismissed, @@ -68,55 +70,55 @@ export const DesktopApp = (): JSX.Element => { const desktopRoutes: RouteProps[] = [ { Component: ProtocolsLanding, - name: 'protocols', + name: t('protocols'), navLinkTo: '/protocols', path: '/protocols', }, { Component: ProtocolDetails, - name: 'Protocol Details', + name: t('protocol_details'), path: '/protocols/:protocolKey', }, { Component: ProtocolTimeline, - name: 'Protocol Timeline', + name: t('protocol_timeline'), path: '/protocols/:protocolKey/timeline', }, { Component: Labware, - name: 'labware', + name: t('labware'), navLinkTo: '/labware', path: '/labware', }, { Component: DevicesLanding, - name: 'devices', + name: t('devices'), navLinkTo: '/devices', path: '/devices', }, { Component: DeviceDetails, - name: 'Device', + name: t('device'), path: '/devices/:robotName', }, { Component: RobotSettings, - name: 'Robot Settings', + name: t('robot_settings'), path: '/devices/:robotName/robot-settings/:robotSettingsTab?', }, { Component: CalibrationDashboard, - name: 'Calibration Dashboard', + name: t('calibration_dashboard'), path: '/devices/:robotName/robot-settings/calibration/dashboard', }, { Component: ProtocolRunDetails, - name: 'Run Details', + name: t('run_details'), path: '/devices/:robotName/protocol-runs/:runId/:protocolRunDetailsTab?', }, { Component: AppSettings, - name: 'App Settings', + name: t('app_settings'), path: '/app-settings/:appSettingsTab?', }, ] diff --git a/app/src/App/Navbar.tsx b/app/src/App/Navbar.tsx index 1471ca4c593..1d7711563ae 100644 --- a/app/src/App/Navbar.tsx +++ b/app/src/App/Navbar.tsx @@ -1,5 +1,4 @@ import { useCallback } from 'react' -import { useTranslation } from 'react-i18next' import { NavLink, useNavigate } from 'react-router-dom' import styled from 'styled-components' import debounce from 'lodash/debounce' @@ -111,8 +110,6 @@ const LogoImg = styled('img')` ` export function Navbar({ routes }: { routes: RouteProps[] }): JSX.Element { - const { t } = useTranslation('top_navigation') - const navigate = useNavigate() const navRoutes = routes.filter( ({ navLinkTo }: RouteProps) => navLinkTo != null @@ -151,7 +148,7 @@ export function Navbar({ routes }: { routes: RouteProps[] }): JSX.Element { as="h3" margin={`${SPACING.spacing8} 0 ${SPACING.spacing8} ${SPACING.spacing12}`} > - {t(name)} + {name} ))} diff --git a/app/src/assets/localization/en/anonymous.json b/app/src/assets/localization/en/anonymous.json index 280e602088d..bcfb90dc7eb 100644 --- a/app/src/assets/localization/en/anonymous.json +++ b/app/src/assets/localization/en/anonymous.json @@ -71,6 +71,7 @@ "storage_limit_reached_description": "Your robot has reached the limit of quick transfers that it can store. You must delete an existing quick transfer before creating a new one.", "system_language_preferences_update_description": "Your system’s language was recently updated. Would you like to use the updated language as the default for the app?", "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", + "u2e_driver_description": "The OT-2 uses this adapter for its USB connection to the desktop app.", "unexpected_error": "An unexpected error has occurred. If the issue persists, contact customer support for assistance.", "update_requires_restarting_app": "Updating requires restarting the app.", "update_robot_software_description": "Bypass the auto-update process and update the robot software manually.", diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index b4d5f654291..c6c4b595597 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -37,7 +37,9 @@ "connect_ip_link": "Learn more about connecting a robot manually", "discovery_timeout": "Discovery timed out.", "dont_change": "Don’t change", + "dont_remind_me": "Don't remind me again", "download_update": "Downloading update...", + "driver_out_of_date": "Realtek USB-to-Ethernet Driver Update Available", "enable_dev_tools": "Developer Tools", "enable_dev_tools_description": "Enabling this setting opens Developer Tools on app launch, enables additional logging and gives access to feature flags.", "error_boundary_desktop_app_description": "You need to reload the app. Contact support with the following error message:", @@ -46,6 +48,7 @@ "error_recovery_mode_description": "Pause on protocol errors instead of canceling the run.", "feature_flags": "Feature Flags", "general": "General", + "get_update": "get update", "heater_shaker_attach_description": "Display a reminder to attach the Heater-Shaker properly before running a test shake or using it in a protocol.", "heater_shaker_attach_visible": "Confirm Heater-Shaker Module Attachment", "how_to_restore": "How to Restore a Previous Software Version", @@ -66,6 +69,7 @@ "ot2_advanced_settings": "OT-2 Advanced Settings", "override_path": "override path", "override_path_to_python": "Override Path to Python", + "please_update_driver": "Please update your computer's driver to ensure a reliable connection to your OT-2.", "prevent_robot_caching": "Prevent Robot Caching", "prevent_robot_caching_description": "The app will immediately clear unavailable robots and will not remember unavailable robots while this is enabled. On networks with many robots, preventing caching may improve network performance at the expense of slower and less reliable robot discovery on app launch.", "privacy": "Privacy", @@ -94,6 +98,7 @@ "trash_bin": "Always use trash bin to calibrate", "try_restarting_the_update": "Try restarting the update.", "turn_off_updates": "Turn off software update notifications in App Settings.", + "u2e_driver_outdated_message": "There is an updated Realtek USB-to-Ethernet adapter driver available for your computer.", "up_to_date": "Up to date", "update_alerts": "Software Update Alerts", "update_app_now": "Update app now", @@ -113,5 +118,6 @@ "usb_to_ethernet_unknown_product": "Unknown Adapter", "use_system_language": "Use system language", "view_software_update": "View software update", + "view_adapter_info": "view adapter info", "view_update": "View Update" } diff --git a/app/src/assets/localization/en/branded.json b/app/src/assets/localization/en/branded.json index 0760c3061b4..42c22baa8ad 100644 --- a/app/src/assets/localization/en/branded.json +++ b/app/src/assets/localization/en/branded.json @@ -71,6 +71,7 @@ "storage_limit_reached_description": "Your Opentrons Flex has reached the limit of quick transfers that it can store. You must delete an existing quick transfer before creating a new one.", "system_language_preferences_update_description": "Your system’s language was recently updated. Would you like to use the updated language as the default for the Opentrons App?", "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from Opentrons Support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", + "u2e_driver_description": "The OT-2 uses this adapter for its USB connection to the Opentrons App.", "unexpected_error": "An unexpected error has occurred. If the issue persists, contact Opentrons Support for assistance.", "update_requires_restarting_app": "Updating requires restarting the Opentrons App.", "update_robot_software_description": "Bypass the Opentrons App auto-update process and update the robot software manually.", diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index 79416a09f73..06a67d39d35 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -3,6 +3,7 @@ "about_calibration_description": "For the robot to move accurately and precisely, you need to calibrate it. Positional calibration happens in three parts: deck calibration, pipette offset calibration and tip length calibration.", "about_calibration_description_ot3": "For the robot to move accurately and precisely, you need to calibrate it. Pipette and gripper calibration is an automated process that uses a calibration probe or pin.After calibration is complete, you can save the calibration data to your computer as a JSON file.", "about_calibration_title": "About Calibration", + "add_new": "Add new...", "advanced": "Advanced", "alpha_description": "Warning: alpha releases are feature-complete but may contain significant bugs.", "alternative_security_types": "Alternative security types", @@ -10,10 +11,12 @@ "apply_historic_offsets": "Apply Labware Offsets", "are_you_sure_you_want_to_disconnect": "Are you sure you want to disconnect from {{ssid}}?", "attach_a_pipette_before_calibrating": "Attach a pipette in order to perform calibration", + "authentication": "Authentication", "boot_scripts": "Boot scripts", "both": "Both", "browse_file_system": "Browse file system", "bug_fixes": "Bug Fixes", + "but_we_expected": "but we expected", "calibrate_deck": "Calibrate deck", "calibrate_deck_description": "For pre-2019 robots that do not have crosses etched on the deck.", "calibrate_deck_to_dots": "Calibrate deck to dots", @@ -28,8 +31,10 @@ "change_network": "Change network", "characters_max": "17 characters max", "check_for_updates": "Check for updates", + "check_to_verify_update": "Check your robot's settings page to verify whether or not the update was successful", "checking_for_updates": "Checking for updates", "choose": "Choose...", + "choose_a_network": "Choose a network...", "choose_file": "Choose file", "choose_network_type": "Choose network type", "choose_reset_settings": "Choose reset settings", @@ -56,7 +61,9 @@ "confirm_device_reset_heading": "Are you sure you want to reset your device?", "connect": "Connect", "connect_the_estop_to_continue": "Connect the E-stop to continue", + "connect_to_ssid": "Connect to {{ssid}}", "connect_to_wifi_network": "Connect to Wi-Fi network", + "connect_to_wifi_network_failure": "Your robot was unable to connect to Wi-Fi network {{ssid}}", "connect_via": "Connect via {{type}}", "connect_via_usb_description_1": "1. Connect the USB A-to-B cable to the robot’s USB-B port.", "connect_via_usb_description_2": "2. Connect the cable to an open USB port on your computer.", @@ -65,6 +72,7 @@ "connected_to_ssid": "Connected to {{ssid}}", "connected_via": "Connected via {{networkInterface}}", "connecting_to": "Connecting to {{ssid}}...", + "connecting_to_wifi_network": "Connecting to Wi-Fi network {{ssid}}", "connection_description_ethernet": "Connect to your lab's wired network.", "connection_description_wifi": "Find a network in your lab or enter your own.", "connection_to_robot_lost": "Connection to robot lost", @@ -96,6 +104,7 @@ "display_sleep_settings": "Display Sleep Settings", "do_not_turn_off": "This could take up to {{minutes}} minutes. Don't turn off the robot.", "done": "Done", + "downgrade": "downgrade", "download": "Download", "download_calibration_data": "Download calibration logs", "download_error": "Download error", @@ -109,6 +118,7 @@ "enable_status_light_description": "Turn on or off the strip of color LEDs on the front of the robot.", "engaged": "Engaged", "enter_factory_password": "Enter factory password", + "enter_name_security_type": "Enter the network name and security type.", "enter_network_name": "Enter network name", "enter_password": "Enter password", "estop": "E-stop", @@ -127,6 +137,8 @@ "factory_resets_cannot_be_undone": "Factory resets cannot be undone.", "failed_to_connect_to_ssid": "Failed to connect to {{ssid}}", "feature_flags": "Feature Flags", + "field_is_required": "{{field}} is required", + "find_and_join_network": "Find and join a Wi-Fi network", "finish_setup": "Finish setup", "firmware_version": "Firmware Version", "fully_calibrate_before_checking_health": "Fully calibrate your robot before checking calibration health", @@ -154,6 +166,7 @@ "last_calibrated_label": "Last Calibrated", "launch_jupyter_notebook": "Launch Jupyter Notebook", "legacy_settings": "Legacy Settings", + "likely_incorrect_password": "Likely incorrect network password.", "mac_address": "MAC Address", "manage_oem_settings": "Manage OEM settings", "minutes": "{{minute}} minutes", @@ -171,7 +184,10 @@ "name_your_robot": "Name your robot", "name_your_robot_description": "Don’t worry, you can always change this in your settings.", "need_another_security_type": "Need another security type?", + "network_is_unsecured": "Wi-Fi network {{ssid}} is unsecured", "network_name": "Network Name", + "network_requires_auth": "Wi-Fi network {{ssid}} requires 802.1X authentication", + "network_requires_wpa_password": "Wi-Fi network {{ssid}} requires a WPA2 password", "network_settings": "Network Settings", "networking": "Networking", "never": "Never", @@ -183,6 +199,7 @@ "no_modules_attached": "No modules attached", "no_network_found": "No network found", "no_pipette_attached": "No pipette attached", + "no_update_files": "Unable to retrieve update for this robot. Ensure your computer is connected to the internet and try again later.", "none_description": "Not recommended", "not_calibrated": "Not calibrated yet", "not_calibrated_short": "Not calibrated", @@ -197,8 +214,10 @@ "on": "On", "one_hour": "1 hour", "other_networks": "Other Networks", + "other_robot_updating": "Unable to update because the app is currently updating a different robot.", "password": "Password", "password_error_message": "Must be at least 8 characters", + "password_not_long_enough": "Password must be at least {{minLength}} characters", "pause_protocol": "Pause protocol when robot door opens", "pause_protocol_description": "When enabled, opening the robot door during a run will pause the robot after it has completed its current motion.", "pipette_calibrations_description": "Pipette calibration uses a metal probe to determine the pipette's exact position relative to precision-cut squares on deck slots.", @@ -208,6 +227,7 @@ "pipette_offset_calibration_recommended": "Pipette Offset calibration recommended", "pipette_offset_calibrations_history": "See all Pipette Offset Calibration history", "pipette_offset_calibrations_title": "Pipette Offset Calibrations", + "please_check_credentials": "Please double-check your network credentials", "privacy": "Privacy", "problem_during_update": "This update is taking longer than usual.", "proceed_without_updating": "Proceed without update", @@ -238,9 +258,12 @@ "returns_your_device_to_new_state": "This returns your device to a new state.", "robot_busy_protocol": "This robot cannot be updated while a protocol is running on it", "robot_calibration_data": "Robot Calibration Data", + "robot_has_bad_capabilities": "Robot has incorrect capabilities shape", "robot_initializing": "Initializing robot...", "robot_name": "Robot Name", "robot_operating_update_available": "Robot Operating System Update Available", + "robot_reconnected_with version": "Robot reconnected with version", + "robot_requires_premigration": "This robot must be updated by the system before a custom update can occur", "robot_serial_number": "Robot Serial Number", "robot_server_version": "Robot Server Version", "robot_settings": "Robot Settings", @@ -259,7 +282,9 @@ "select_a_network": "Select a network", "select_a_security_type": "Select a security type", "select_all_settings": "Select all settings", + "select_auth_method_short": "Select authentication method", "select_authentication_method": "Select authentication method for your selected network.", + "select_file": "Select file", "sending_software": "Sending software...", "serial": "Serial", "setup_mode": "Setup mode", @@ -275,6 +300,9 @@ "subnet_mask": "Subnet Mask", "successfully_connected": "Successfully connected!", "successfully_connected_to_network": "Successfully connected to {{ssid}}!", + "successfully_connected_to_ssid": "Your robot has successfully connected to Wi-Fi network {{ssid}}", + "successfully_connected_to_wifi": "Successfully connected to Wi-Fi", + "successfully_disconnected_from_wifi": "Successfully disconnected from Wi-Fi", "supported_protocol_api_versions": "Supported Protocol API Versions", "text_size": "Text Size", "text_size_description": "Text on all screens will adjust to the size you choose below.", @@ -286,6 +314,14 @@ "troubleshooting": "Troubleshooting", "try_again": "Try again", "try_restarting_the_update": "Try restarting the update.", + "unable_to_cancel_update": "Unable to cancel in-progress update session", + "unable_to_commit_update": "Unable to commit update", + "unable_to_connect": "Unable to connect to Wi-Fi", + "unable_to_disconnect": "Unable to disconnect from Wi-Fi", + "unable_to_find_system_file": "Unable to find system file for update", + "unable_to_find_robot_with_name": "Unable to find online robot with name", + "unable_to_restart": "Unable to restart robot", + "unable_to_start_update_session": "Unable to start update session", "up_to_date": "up to date", "update_available": "Update Available", "update_channel_description": "Stable receives the latest stable releases. Beta allows you to try out new in-progress features before they launch in Stable channel, but they have not completed testing yet.", @@ -294,7 +330,10 @@ "update_requires_restarting_robot": "Updating the robot software requires restarting the robot", "update_robot_now": "Update robot now", "update_robot_software": "Update robot software manually with a local file (.zip)", + "update_server_unavailable": "Unable to update because your robot's update server is not responding.", + "update_unavailable": "Update unavailable", "updating": "Updating", + "upgrade": "upgrade", "upload_custom_logo": "Upload custom logo", "upload_custom_logo_description": "Upload a logo for the robot to display during boot up.", "upload_custom_logo_dimensions": "The logo must fit within dimensions 1024 x 600 and be a PNG file (.png).", diff --git a/app/src/assets/localization/en/gripper_wizard_flows.json b/app/src/assets/localization/en/gripper_wizard_flows.json index ff98d8e07f0..70df5688820 100644 --- a/app/src/assets/localization/en/gripper_wizard_flows.json +++ b/app/src/assets/localization/en/gripper_wizard_flows.json @@ -5,7 +5,6 @@ "before_you_begin": "Before you begin", "begin_calibration": "Begin calibration", "calibrate_gripper": "Calibrate Gripper", - "calibration_pin": "Calibration Pin", "calibration_pin_touching": "The calibration pin will touch the calibration square in slot {{slot}} to determine its exact position.", "complete_calibration": "Complete calibration", "continue": "Continue", @@ -17,7 +16,6 @@ "gripper_calibration": "Gripper Calibration", "gripper_recalibration": "Gripper Recalibration", "gripper_successfully_attached": "Gripper successfully attached", - "hex_screwdriver": "2.5 mm Hex Screwdriver", "hold_gripper_and_loosen_screws": "Hold the gripper in place and loosen the top gripper screw first. After that move onto the bottom screw. (The screws are captive and will not come apart from the gripper.) Then carefully remove the gripper.", "insert_pin_into_front_jaw": "Insert calibration pin in front jaw", "insert_pin_into_rear_jaw": "Insert calibration pin in rear jaw", diff --git a/app/src/assets/localization/en/pipette_wizard_flows.json b/app/src/assets/localization/en/pipette_wizard_flows.json index 78dc2b852a6..1154d6f9659 100644 --- a/app/src/assets/localization/en/pipette_wizard_flows.json +++ b/app/src/assets/localization/en/pipette_wizard_flows.json @@ -67,6 +67,7 @@ "pipette_heavy": "The 96-Channel Pipette is heavy ({{weight}}). Ask a labmate for help, if needed.", "please_install_correct_pip": "Install {{pipetteName}} instead", "progress_will_be_lost": "{{flow}} progress will be lost", + "provided_with_robot": "Provided with the robot. Using another size can strip the instruments’s screws.", "reattach_carriage": "reattach z-axis carriage", "recalibrate_pipette": "recalibrate {{mount}} pipette", "remove_cal_probe": "remove calibration probe", diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index 2842f9dc30d..8037b8f2778 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -6,11 +6,13 @@ "adapter_in_mod_in_slot": "{{adapter}} on {{module}} in Slot {{slot}}", "adapter_in_slot": "{{adapter}} in Slot {{slot}}", "air_gap_in_place": "Air gapping {{volume}} µL", + "all_nozzles": "all nozzles", "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", + "column_layout": "column layout", "comment": "Comment", "configure_for_volume": "Configure {{pipette}} to aspirate {{volume}} µL", "configure_nozzle_layout": "Configure {{pipette}} to use {{layout}}", @@ -59,11 +61,13 @@ "opening_tc_lid": "Opening Thermocycler lid", "pause": "Pause", "pause_on": "Pause on {{robot_name}}", + "partial_layout": "partial layout", "pickup_tip": "Picking up tip(s) from {{well_range}} of {{labware}} in {{labware_location}}", "prepare_to_aspirate": "Preparing {{pipette}} to aspirate", "reloading_labware": "Reloading {{labware}}", "return_tip": "Returning tip to {{well_name}} of {{labware}} in {{labware_location}}", "right": "Right", + "row_layout": "row layout", "save_position": "Saving position", "set_and_await_hs_shake": "Setting Heater-Shaker to shake at {{rpm}} rpm and waiting until reached", "setting_hs_temp": "Setting Target Temperature of Heater-Shaker to {{temp}}", @@ -71,6 +75,7 @@ "setting_thermocycler_block_temp": "Setting Thermocycler block temperature to {{temp}} with hold time of {{hold_time_seconds}} seconds after target reached", "setting_thermocycler_lid_temp": "Setting Thermocycler lid temperature to {{temp}}", "single": "single", + "single_nozzle_layout": "single nozzle layout", "slot": "Slot {{slot_name}}", "target_temperature": "target temperature", "tc_awaiting_for_duration": "Waiting for Thermocycler profile to complete", diff --git a/app/src/assets/localization/en/top_navigation.json b/app/src/assets/localization/en/top_navigation.json index 178b02042b9..16d5e2d011d 100644 --- a/app/src/assets/localization/en/top_navigation.json +++ b/app/src/assets/localization/en/top_navigation.json @@ -1,7 +1,10 @@ { + "app_settings": "App Settings", "attached_pipettes_do_not_match": "Attached pipettes do not match pipettes specified in loaded protocol", "calibrate_deck_to_proceed": "Calibrate your deck to proceed", + "calibration_dashboard": "Calibration Dashboard", "deck_setup": "Deck Setup", + "device": "Device", "devices": "Devices", "instruments": "Instruments", "labware": "Labware", @@ -10,10 +13,13 @@ "pipettes": "pipettes", "please_connect_to_a_robot": "Please connect to a robot to proceed", "please_load_a_protocol": "Please load a protocol to proceed", + "protocol_details": "Protocol Details", "protocol_runs": "Protocol Runs", + "protocol_timeline": "Protocol Timeline", "protocols": "Protocols", "quick_transfer": "Quick Transfer", "robot_settings": "Robot Settings", "run": "run", + "run_details": "Run Details", "settings": "Settings" } diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureNozzleLayoutCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureNozzleLayoutCommandText.ts index 8c9e12f3d5b..f6440d69e1d 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureNozzleLayoutCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getConfigureNozzleLayoutCommandText.ts @@ -13,13 +13,12 @@ export function getConfigureNozzleLayoutCommandText({ pip => pip.id === pipetteId )?.pipetteName - // TODO(cb, 2024-09-10): confirm these strings for copy consistency and add them to i18n const ConfigAmount = { - SINGLE: 'single nozzle layout', - COLUMN: 'column layout', - ROW: 'row layout', - QUADRANT: 'partial layout', - ALL: 'all nozzles', + SINGLE: t('single_nozzle_layout'), + COLUMN: t('column_layout'), + ROW: t('row_layout'), + QUADRANT: t('partial_layout'), + ALL: t('all_nozzles'), } return t('configure_nozzle_layout', { diff --git a/app/src/organisms/Desktop/Alerts/U2EDriverOutdatedAlert.tsx b/app/src/organisms/Desktop/Alerts/U2EDriverOutdatedAlert.tsx index fbb79a6b935..a327fce6a31 100644 --- a/app/src/organisms/Desktop/Alerts/U2EDriverOutdatedAlert.tsx +++ b/app/src/organisms/Desktop/Alerts/U2EDriverOutdatedAlert.tsx @@ -1,5 +1,6 @@ import { Link as InternalLink } from 'react-router-dom' import styled from 'styled-components' +import { useTranslation } from 'react-i18next' import { AlertModal, @@ -12,20 +13,9 @@ import { ANALYTICS_U2E_DRIVE_ALERT_DISMISSED, ANALYTICS_U2E_DRIVE_LINK_CLICKED, } from '/app/redux/analytics' -import { - U2E_DRIVER_UPDATE_URL, - U2E_DRIVER_OUTDATED_MESSAGE, - U2E_DRIVER_DESCRIPTION, - U2E_DRIVER_OUTDATED_CTA, -} from '/app/redux/system-info' +import { U2E_DRIVER_UPDATE_URL } from '/app/redux/system-info' import type { AlertProps } from './types' -// TODO(mc, 2020-05-07): i18n -const DRIVER_OUT_OF_DATE = 'Realtek USB-to-Ethernet Driver Update Available' -const VIEW_ADAPTER_INFO = 'view adapter info' -const GET_UPDATE = 'get update' -const DONT_REMIND_ME_AGAIN = "Don't remind me again" - const ADAPTER_INFO_URL = '/more/network-and-system' const LinkButton = styled(Link)` @@ -42,19 +32,20 @@ const IgnoreCheckbox = styled(DeprecatedCheckboxField)` export function U2EDriverOutdatedAlert(props: AlertProps): JSX.Element { const trackEvent = useTrackEvent() + const { t } = useTranslation(['app_settings', 'branded']) const [rememberDismiss, toggleRememberDismiss] = useToggle() const { dismissAlert } = props return ( { dismissAlert(rememberDismiss) trackEvent({ @@ -67,7 +58,7 @@ export function U2EDriverOutdatedAlert(props: AlertProps): JSX.Element { Component: LinkButton, href: U2E_DRIVER_UPDATE_URL, external: true, - children: GET_UPDATE, + children: t('get_update'), onClick: () => { dismissAlert(rememberDismiss) trackEvent({ @@ -79,11 +70,11 @@ export function U2EDriverOutdatedAlert(props: AlertProps): JSX.Element { ]} >

- {U2E_DRIVER_OUTDATED_MESSAGE} {U2E_DRIVER_DESCRIPTION} + {t('u2e_driver_outdated_message')} {t('branded:u2e_driver_description')}

-

{U2E_DRIVER_OUTDATED_CTA}

+

{t('please_update_driver')}

diff --git a/app/src/organisms/Desktop/Devices/ConfigurePipette/ConfigMessage.tsx b/app/src/organisms/Desktop/Devices/ConfigurePipette/ConfigMessage.tsx deleted file mode 100644 index d6a32fa6d4b..00000000000 --- a/app/src/organisms/Desktop/Devices/ConfigurePipette/ConfigMessage.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import styles from './styles.module.css' - -// TODO (ka 2019-2-12): Add intercom onClick to assistance text -export function ConfigMessage(): JSX.Element { - return ( -
-

Warning:

-

- These are advanced settings. Please do not attempt to adjust without - assistance from an Opentrons support team member, as doing - so may affect the lifespan of your pipette. -

-

- Note that these settings will not override any pipette settings - pre-defined in protocols. -

-
- ) -} diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx index 55c64ee648a..01917e5b483 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx @@ -112,7 +112,7 @@ export function UpdateRobotSoftware({ {updateFromFileDisabledReason != null && ( - {updateFromFileDisabledReason} + {t(updateFromFileDisabledReason)} )}
diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormModal.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormModal.tsx index 60ce3d2a88e..73f6004eb73 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormModal.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormModal.tsx @@ -1,17 +1,17 @@ import { Controller } from 'react-hook-form' import styled, { css } from 'styled-components' - +import { useTranslation } from 'react-i18next' import { FONT_SIZE_BODY_1, BUTTON_TYPE_SUBMIT, Flex, } from '@opentrons/components' +import { SECURITY_WPA_PSK, SECURITY_WPA_EAP } from '/app/redux/networking' import { ScrollableAlertModal } from '/app/molecules/modals' import { TextField } from './TextField' import { KeyFileField } from './KeyFileField' import { SecurityField } from './SecurityField' import { FIELD_TYPE_KEY_FILE, FIELD_TYPE_SECURITY } from '../constants' -import * as Copy from '../i18n' import type { Control } from 'react-hook-form' import type { ConnectFormField, ConnectFormValues, WifiNetwork } from '../types' @@ -53,16 +53,23 @@ export interface FormModalProps { export const FormModal = (props: FormModalProps): JSX.Element => { const { id, network, fields, isValid, onCancel, control } = props + const { t } = useTranslation(['device_settings', 'shared']) const heading = network !== null - ? Copy.CONNECT_TO_SSID(network.ssid) - : Copy.FIND_AND_JOIN_A_NETWORK + ? t('connect_to_ssid', { ssid: network.ssid }) + : t('find_and_join_network') - const body = - network !== null - ? Copy.NETWORK_REQUIRES_SECURITY(network) - : Copy.ENTER_NAME_AND_SECURITY_TYPE + let bodyText = t('enter_name_security_type') + if (network != null) { + if (network.securityType === SECURITY_WPA_PSK) { + bodyText = t('network_requires_wpa_password', { ssid: network.ssid }) + } else if (network.securityType === SECURITY_WPA_EAP) { + bodyText = t('network_requires_auth', { ssid: network.ssid }) + } else { + bodyText = t('network_is_unsecured', { ssid: network.ssid }) + } + } return ( { iconName="wifi" onCloseClick={onCancel} buttons={[ - { children: Copy.CANCEL, onClick: props.onCancel }, + { children: t('shared:cancel'), onClick: props.onCancel }, { - children: Copy.CONNECT, + children: t('connect'), type: BUTTON_TYPE_SUBMIT, form: id, disabled: !isValid, }, ]} > - {body} + {bodyText} {fields.map(fieldProps => { const { name } = fieldProps diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/KeyFileField.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/KeyFileField.tsx index 376048ba420..b7ea68d5e36 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/KeyFileField.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/KeyFileField.tsx @@ -1,9 +1,9 @@ import { useRef } from 'react' +import { useTranslation } from 'react-i18next' import { SelectField } from '@opentrons/components' import { FormRow } from './FormRow' import { UploadKeyInput } from './UploadKeyInput' -import { LABEL_ADD_NEW_KEY } from '../i18n' import { useConnectFormField } from './form-state' import type { WifiKey } from '../types' @@ -27,10 +27,6 @@ export interface KeyFileFieldProps { const ADD_NEW_KEY_VALUE = '__addNewKey__' -const ADD_NEW_KEY_OPTION_GROUP = { - options: [{ value: ADD_NEW_KEY_VALUE, label: LABEL_ADD_NEW_KEY }], -} - const makeKeyOptions = ( keys: WifiKey[] ): { options: Array<{ value: string; label: string }> } => ({ @@ -48,6 +44,11 @@ export const KeyFileField = (props: KeyFileFieldProps): JSX.Element => { field, fieldState, } = props + const { t } = useTranslation('device_settings') + const ADD_NEW_KEY_OPTION_GROUP = { + options: [{ value: ADD_NEW_KEY_VALUE, label: t('add_new') }], + } + const { value, error, setValue, setTouched } = useConnectFormField( field, fieldState @@ -81,7 +82,7 @@ export const KeyFileField = (props: KeyFileFieldProps): JSX.Element => { diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/SecurityField.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/SecurityField.tsx index c9fa4e0c069..cb7c2cbf615 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/SecurityField.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/SecurityField.tsx @@ -1,7 +1,7 @@ +import { useTranslation } from 'react-i18next' import { SelectField } from '@opentrons/components' import { SECURITY_NONE, SECURITY_WPA_PSK } from '../constants' -import { LABEL_SECURITY_NONE, LABEL_SECURITY_PSK } from '../i18n' import { useConnectFormField } from './form-state' import { FormRow } from './FormRow' @@ -25,8 +25,8 @@ export interface SecurityFieldProps { } const ALL_SECURITY_OPTIONS = [ - { options: [{ value: SECURITY_NONE, label: LABEL_SECURITY_NONE }] }, - { options: [{ value: SECURITY_WPA_PSK, label: LABEL_SECURITY_PSK }] }, + { options: [{ value: SECURITY_NONE, label: 'shared:none' }] }, + { options: [{ value: SECURITY_WPA_PSK, label: 'wpa2_personal' }] }, ] const makeEapOptionsGroup = ( @@ -39,6 +39,7 @@ const makeEapOptionsGroup = ( }) export const SecurityField = (props: SecurityFieldProps): JSX.Element => { + const { t } = useTranslation(['device_settings', 'shared']) const { id, name, @@ -62,7 +63,7 @@ export const SecurityField = (props: SecurityFieldProps): JSX.Element => { ] return ( - + { + const { t } = useTranslation('device_settings') const { id, name, label, isPassword, className, field, fieldState } = props const { value, error, onChange, onBlur } = useConnectFormField( field, @@ -42,7 +43,7 @@ export const TextField = (props: TextFieldProps): JSX.Element => { /> {isPassword && ( diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.ts b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.ts deleted file mode 100644 index 80336fb0139..00000000000 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.ts +++ /dev/null @@ -1,379 +0,0 @@ -import * as Fixtures from '/app/redux/networking/__fixtures__' -import { describe, it, expect } from 'vitest' - -import { - CONFIGURE_FIELD_SSID, - CONFIGURE_FIELD_PSK, - CONFIGURE_FIELD_SECURITY_TYPE, - SECURITY_WPA_EAP, - SECURITY_WPA_PSK, - SECURITY_NONE, -} from '/app/redux/networking' - -import { - FIELD_TYPE_TEXT, - FIELD_TYPE_KEY_FILE, - FIELD_TYPE_SECURITY, -} from '../../constants' - -import { - LABEL_SECURITY, - LABEL_SSID, - LABEL_PSK, - SELECT_AUTHENTICATION_METHOD, - SELECT_FILE, -} from '../../i18n' - -import { - getConnectFormFields, - validateConnectFormFields, - connectFormToConfigureRequest, -} from '../form-fields' - -describe('getConnectFormFields', () => { - it('should add a string field for SSID if network is unknown', () => { - const fields = getConnectFormFields(null, 'robot-name', [], [], {}) - - expect(fields).toContainEqual({ - type: FIELD_TYPE_TEXT, - name: CONFIGURE_FIELD_SSID, - label: `* ${LABEL_SSID}`, - isPassword: false, - }) - }) - - it('should add a security dropdown field if network is unknown', () => { - const eapOptions = [Fixtures.mockEapOption] - const fields = getConnectFormFields(null, 'robot-name', eapOptions, [], {}) - - expect(fields).toContainEqual({ - type: FIELD_TYPE_SECURITY, - name: CONFIGURE_FIELD_SECURITY_TYPE, - label: `* ${LABEL_SECURITY}`, - eapOptions, - showAllOptions: true, - placeholder: SELECT_AUTHENTICATION_METHOD, - }) - }) - - it('should add a security dropdown field if known network has EAP security', () => { - const eapOptions = [Fixtures.mockEapOption] - const network = { - ...Fixtures.mockWifiNetwork, - securityType: SECURITY_WPA_EAP, - } - const fields = getConnectFormFields( - network, - 'robot-name', - eapOptions, - [], - {} - ) - - expect(fields).toContainEqual({ - type: FIELD_TYPE_SECURITY, - name: CONFIGURE_FIELD_SECURITY_TYPE, - label: `* ${LABEL_SECURITY}`, - eapOptions, - showAllOptions: false, - placeholder: SELECT_AUTHENTICATION_METHOD, - }) - }) - - it('should add a password field for PSK if known network as PSK security', () => { - const network = { - ...Fixtures.mockWifiNetwork, - securityType: SECURITY_WPA_PSK, - } - const fields = getConnectFormFields(network, 'robot-name', [], [], {}) - - expect(fields).toContainEqual({ - type: FIELD_TYPE_TEXT, - name: CONFIGURE_FIELD_PSK, - label: `* ${LABEL_PSK}`, - isPassword: true, - }) - }) - - it('should add a password field for PSK if unknown network and user selects PSK', () => { - const fields = getConnectFormFields(null, 'robot-name', [], [], { - securityType: SECURITY_WPA_PSK, - }) - - expect(fields).toContainEqual({ - type: FIELD_TYPE_TEXT, - name: CONFIGURE_FIELD_PSK, - label: `* ${LABEL_PSK}`, - isPassword: true, - }) - }) - - it('should add EAP options based on the selected eapType if network is unknown', () => { - const eapOptions = [ - { ...Fixtures.mockEapOption, name: 'someEapType', options: [] }, - { ...Fixtures.mockEapOption, name: 'someOtherEapType' }, - ] - const wifiKeys = [Fixtures.mockWifiKey] - const fields = getConnectFormFields( - null, - 'robot-name', - eapOptions, - wifiKeys, - { - securityType: 'someOtherEapType', - } - ) - - expect(fields).toEqual( - expect.arrayContaining([ - { - type: FIELD_TYPE_TEXT, - name: 'eapConfig.stringField', - label: '* String Field', - isPassword: false, - }, - { - type: FIELD_TYPE_TEXT, - name: 'eapConfig.passwordField', - label: 'Password Field', - isPassword: true, - }, - { - type: FIELD_TYPE_KEY_FILE, - name: 'eapConfig.fileField', - label: '* File Field', - robotName: 'robot-name', - wifiKeys, - placeholder: SELECT_FILE, - }, - ]) - ) - }) - - it('should add EAP options based on the selected eapType if network is EAP', () => { - const network = { - ...Fixtures.mockWifiNetwork, - securityType: SECURITY_WPA_EAP, - } - const eapOptions = [ - { ...Fixtures.mockEapOption, name: 'someEapType' }, - { ...Fixtures.mockEapOption, name: 'someOtherEapType', options: [] }, - ] - const wifiKeys = [Fixtures.mockWifiKey] - const fields = getConnectFormFields( - network, - 'robot-name', - eapOptions, - wifiKeys, - { securityType: 'someEapType' } - ) - - expect(fields).toEqual( - expect.arrayContaining([ - { - type: FIELD_TYPE_TEXT, - name: 'eapConfig.stringField', - label: '* String Field', - isPassword: false, - }, - { - type: FIELD_TYPE_TEXT, - name: 'eapConfig.passwordField', - label: 'Password Field', - isPassword: true, - }, - { - type: FIELD_TYPE_KEY_FILE, - name: 'eapConfig.fileField', - label: '* File Field', - robotName: 'robot-name', - wifiKeys, - placeholder: SELECT_FILE, - }, - ]) - ) - }) -}) - -describe('validateConnectFormFields', () => { - it('should error if network is hidden and ssid is blank', () => { - const errors = validateConnectFormFields( - null, - [], - { - securityType: SECURITY_WPA_PSK, - psk: '12345678', - }, - {} - ) - - expect(errors).toEqual({ - ssid: { message: `${LABEL_SSID} is required`, type: 'ssidError' }, - }) - }) - - it('should error if network is hidden and securityType is blank', () => { - const errors = validateConnectFormFields(null, [], { ssid: 'foobar' }, {}) - - expect(errors).toEqual({ - securityType: { - message: `${LABEL_SECURITY} is required`, - type: 'securityTypeError', - }, - }) - }) - - it('should error if network is PSK and psk is blank', () => { - const network = { - ...Fixtures.mockWifiNetwork, - securityType: SECURITY_WPA_PSK, - } - const errors = validateConnectFormFields(network, [], { psk: '' }, {}) - - expect(errors).toEqual({ - psk: { - message: `${LABEL_PSK} must be at least 8 characters`, - type: 'pskError', - }, - }) - }) - - it('should error if selected security is PSK and psk is blank', () => { - const values = { ssid: 'foobar', securityType: SECURITY_WPA_PSK } - const errors = validateConnectFormFields(null, [], values, {}) - - expect(errors).toEqual({ - psk: { - message: `${LABEL_PSK} must be at least 8 characters`, - type: 'pskError', - }, - }) - }) - - it('should error if network is EAP and securityType is blank', () => { - const network = { - ...Fixtures.mockWifiNetwork, - securityType: SECURITY_WPA_EAP, - } - const errors = validateConnectFormFields(network, [], {}, {}) - - expect(errors).toEqual({ - securityType: { - message: `${LABEL_SECURITY} is required`, - type: 'securityTypeError', - }, - }) - }) - - it('should error if any required EAP fields are missing', () => { - const network = { - ...Fixtures.mockWifiNetwork, - securityType: SECURITY_WPA_EAP, - } - const eapOptions = [ - { ...Fixtures.mockEapOption, name: 'someEapType', options: [] }, - { ...Fixtures.mockEapOption, name: 'someOtherEapType' }, - ] - const values = { - securityType: 'someOtherEapType', - eapConfig: { fileField: '123' }, - } - const errors = validateConnectFormFields(network, eapOptions, values, {}) - - expect(errors).toEqual({ - 'eapConfig.stringField': { - message: `String Field is required`, - type: 'eapError', - }, - }) - }) -}) - -describe('connectFormToConfigureRequest', () => { - it('should return null if unknown network and no ssid', () => { - const values = { securityType: SECURITY_NONE } - const result = connectFormToConfigureRequest(null, values) - - expect(result).toEqual(null) - }) - - it('should set ssid and securityType from values if unknown network', () => { - const values = { ssid: 'foobar', securityType: SECURITY_NONE } - const result = connectFormToConfigureRequest(null, values) - - expect(result).toEqual({ - ssid: 'foobar', - securityType: SECURITY_NONE, - hidden: true, - }) - }) - - it('should set ssid from network if known', () => { - const network = { - ...Fixtures.mockWifiNetwork, - ssid: 'foobar', - securityType: SECURITY_NONE, - } - const values = {} - const result = connectFormToConfigureRequest(network, values) - - expect(result).toEqual({ - ssid: 'foobar', - securityType: SECURITY_NONE, - hidden: false, - }) - }) - - it('should set psk from values', () => { - const network = { - ...Fixtures.mockWifiNetwork, - ssid: 'foobar', - securityType: SECURITY_WPA_PSK, - } - const values = { psk: '12345678' } - const result = connectFormToConfigureRequest(network, values) - - expect(result).toEqual({ - ssid: 'foobar', - securityType: SECURITY_WPA_PSK, - hidden: false, - psk: '12345678', - }) - }) - - it('should set eapConfig from values with known network', () => { - const network = { - ...Fixtures.mockWifiNetwork, - ssid: 'foobar', - securityType: SECURITY_WPA_EAP, - } - const values = { - securityType: 'someEapType', - eapConfig: { option1: 'fizzbuzz' }, - } - const result = connectFormToConfigureRequest(network, values) - - expect(result).toEqual({ - ssid: 'foobar', - securityType: SECURITY_WPA_EAP, - hidden: false, - eapConfig: { eapType: 'someEapType', option1: 'fizzbuzz' }, - }) - }) - - it('should set eapConfig from values with unknown network', () => { - const values = { - ssid: 'foobar', - securityType: 'someEapType', - eapConfig: { option1: 'fizzbuzz' }, - } - const result = connectFormToConfigureRequest(null, values) - - expect(result).toEqual({ - ssid: 'foobar', - securityType: SECURITY_WPA_EAP, - hidden: true, - eapConfig: { eapType: 'someEapType', option1: 'fizzbuzz' }, - }) - }) -}) diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.tsx new file mode 100644 index 00000000000..638d9f1b76c --- /dev/null +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/__tests__/form-fields.test.tsx @@ -0,0 +1,422 @@ +import * as Fixtures from '/app/redux/networking/__fixtures__' +import { describe, it, expect } from 'vitest' +import { screen } from '@testing-library/react' +import { useTranslation } from 'react-i18next' + +import { + SECURITY_WPA_EAP, + SECURITY_WPA_PSK, + SECURITY_NONE, +} from '/app/redux/networking' +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' + +import { + getConnectFormFields, + validateConnectFormFields, + connectFormToConfigureRequest, +} from '../form-fields' + +import type { FieldError } from 'react-hook-form' +import type { + WifiNetwork, + WifiKey, + EapOption, + ConnectFormValues, +} from '../../types' +import type { ComponentProps } from 'react' + +const TestWrapperConnectFormFields = ({ + network, + robotName, + eapOptions, + wifiKeys, + values, +}: { + network: WifiNetwork | null + robotName: string + eapOptions: EapOption[] + wifiKeys: WifiKey[] + values: ConnectFormValues +}) => { + const { t } = useTranslation('device_settings') + const fields = getConnectFormFields( + network, + robotName, + eapOptions, + wifiKeys, + values, + t + ) + return
{JSON.stringify(fields)}
+} + +const renderConnectFormFields = ( + props: ComponentProps +) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('getConnectFormFields', () => { + it('should add a string field for SSID if network is unknown', () => { + const props = { + network: null, + robotName: 'robot-name', + eapOptions: [], + wifiKeys: [], + values: {}, + } + renderConnectFormFields(props) + screen.getByText(/text/) + screen.getByText(/ssid/) + screen.getByText(/ * Network Name/) + }) + + it('should add a security dropdown field if network is unknown', () => { + const props = { + network: null, + robotName: 'robot-name', + eapOptions: [Fixtures.mockEapOption], + wifiKeys: [], + values: {}, + } + renderConnectFormFields(props) + screen.getByText(/security/) + screen.getByText(/ * Authentication/) + screen.getByText(/Select authentication method/) + }) + + it('should add a security dropdown field if known network has EAP security', () => { + const network = { + ...Fixtures.mockWifiNetwork, + securityType: SECURITY_WPA_EAP, + } + const props = { + network: network, + robotName: 'robot-name', + eapOptions: [Fixtures.mockEapOption], + wifiKeys: [], + values: {}, + } + renderConnectFormFields(props) + + screen.getByText(/security/) + screen.getByText(/ * Authentication/) + screen.getByText(/Select authentication method/) + screen.getByText(/EAP Option/) + screen.getByText(/String Field/) + screen.getByText(/Password Field/) + screen.getByText(/File Field/) + }) + + it('should add a password field for PSK if known network as PSK security', () => { + const network = { + ...Fixtures.mockWifiNetwork, + securityType: SECURITY_WPA_PSK, + } + const props = { + network: network, + robotName: 'robot-name', + eapOptions: [], + wifiKeys: [], + values: {}, + } + renderConnectFormFields(props) + + screen.getByText(/psk/) + screen.getByText(/ * Password/) + }) + + it('should add a password field for PSK if unknown network and user selects PSK', () => { + const props = { + network: null, + robotName: 'robot-name', + eapOptions: [], + wifiKeys: [], + values: { securityType: SECURITY_WPA_PSK }, + } + + renderConnectFormFields(props) + screen.getByText(/psk/) + screen.getByText(/ * Password/) + }) + + it('should add EAP options based on the selected eapType if network is unknown', () => { + const eapOptions = [ + { ...Fixtures.mockEapOption, name: 'someEapType', options: [] }, + { ...Fixtures.mockEapOption, name: 'someOtherEapType' }, + ] + const wifiKeys = [Fixtures.mockWifiKey] + const props = { + network: null, + robotName: 'robot-name', + eapOptions: eapOptions, + wifiKeys: wifiKeys, + values: {}, + } + renderConnectFormFields(props) + + screen.getByText(/someEapType/) + screen.getByText(/someOtherEapType/) + screen.getByText(/stringField/) + screen.getByText(/String Field/) + screen.getByText(/passwordField/) + screen.getByText(/Password Field/) + screen.getByText(/fileField/) + screen.getByText(/File Field/) + }) + + it('should add EAP options based on the selected eapType if network is EAP', () => { + const network = { + ...Fixtures.mockWifiNetwork, + securityType: SECURITY_WPA_EAP, + } + const eapOptions = [ + { ...Fixtures.mockEapOption, name: 'someEapType' }, + { ...Fixtures.mockEapOption, name: 'someOtherEapType', options: [] }, + ] + const wifiKeys = [Fixtures.mockWifiKey] + const props = { + network: network, + robotName: 'robot-name', + eapOptions: eapOptions, + wifiKeys: wifiKeys, + values: { securityType: 'someEapType' }, + } + renderConnectFormFields(props) + screen.getByText(/ * String Field/) + screen.getByText(/Password Field/) + screen.getByText(/ * File Field/) + }) +}) + +const TestWrapperValidateFormFields = ({ + network, + eapOptions, + values, + errors, +}: { + network: WifiNetwork | null + eapOptions: EapOption[] + values: ConnectFormValues + errors: Record +}) => { + const { t } = useTranslation('device_settings') + const validationErrors = validateConnectFormFields( + network, + eapOptions, + values, + errors, + t + ) + return
{JSON.stringify(validationErrors)}
+} + +const renderValidateFormFields = ( + props: ComponentProps +) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('validateConnectFormFields', () => { + it('should error if network is hidden and ssid is blank', () => { + const props = { + network: null, + eapOptions: [], + values: { + securityType: SECURITY_WPA_PSK, + psk: '12345678', + }, + errors: {}, + } + renderValidateFormFields(props) + screen.getByText(/ssid/) + screen.getByText(/ssidError/) + screen.getByText(/Network Name is required/) + }) + + it('should error if network is hidden and securityType is blank', () => { + const props = { + network: null, + eapOptions: [], + values: { + ssid: 'foobar', + }, + errors: {}, + } + renderValidateFormFields(props) + screen.getByText(/securityType/) + screen.getByText(/securityTypeError/) + screen.getByText(/Authentication is required/) + }) + + it('should error if network is PSK and psk is blank', () => { + const network = { + ...Fixtures.mockWifiNetwork, + securityType: SECURITY_WPA_PSK, + } + const props = { + network: network, + eapOptions: [], + values: { + psk: '', + }, + errors: {}, + } + renderValidateFormFields(props) + screen.getByText(/psk/) + screen.getByText(/pskError/) + screen.getByText(/Password must be at least 8 characters/) + }) + + it('should error if selected security is PSK and psk is blank', () => { + const values = { ssid: 'foobar', securityType: SECURITY_WPA_PSK } + const props = { + network: null, + eapOptions: [], + values: values, + errors: {}, + } + renderValidateFormFields(props) + screen.getByText(/psk/) + screen.getByText(/pskError/) + screen.getByText(/Password must be at least 8 characters/) + }) + + it('should error if network is EAP and securityType is blank', () => { + const network = { + ...Fixtures.mockWifiNetwork, + securityType: SECURITY_WPA_EAP, + } + const props = { + network: network, + eapOptions: [], + values: {}, + errors: {}, + } + + renderValidateFormFields(props) + screen.getByText(/securityType/) + screen.getByText(/securityTypeError/) + screen.getByText(/Authentication is required/) + }) + + it('should error if any required EAP fields are missing', () => { + const network = { + ...Fixtures.mockWifiNetwork, + securityType: SECURITY_WPA_EAP, + } + const eapOptions = [ + { ...Fixtures.mockEapOption, name: 'someEapType', options: [] }, + { ...Fixtures.mockEapOption, name: 'someOtherEapType' }, + ] + const values = { + securityType: 'someOtherEapType', + eapConfig: { fileField: '123' }, + } + const props = { + network: network, + eapOptions: eapOptions, + values: values, + errors: {}, + } + + renderValidateFormFields(props) + screen.getByText(/eapConfig.stringField/) + screen.getByText(/eapError/) + screen.getByText(/String Field is required/) + }) +}) + +describe('connectFormToConfigureRequest', () => { + it('should return null if unknown network and no ssid', () => { + const values = { securityType: SECURITY_NONE } + const result = connectFormToConfigureRequest(null, values) + + expect(result).toEqual(null) + }) + + it('should set ssid and securityType from values if unknown network', () => { + const values = { ssid: 'foobar', securityType: SECURITY_NONE } + const result = connectFormToConfigureRequest(null, values) + + expect(result).toEqual({ + ssid: 'foobar', + securityType: SECURITY_NONE, + hidden: true, + }) + }) + + it('should set ssid from network if known', () => { + const network = { + ...Fixtures.mockWifiNetwork, + ssid: 'foobar', + securityType: SECURITY_NONE, + } + const values = {} + const result = connectFormToConfigureRequest(network, values) + + expect(result).toEqual({ + ssid: 'foobar', + securityType: SECURITY_NONE, + hidden: false, + }) + }) + + it('should set psk from values', () => { + const network = { + ...Fixtures.mockWifiNetwork, + ssid: 'foobar', + securityType: SECURITY_WPA_PSK, + } + const values = { psk: '12345678' } + const result = connectFormToConfigureRequest(network, values) + + expect(result).toEqual({ + ssid: 'foobar', + securityType: SECURITY_WPA_PSK, + hidden: false, + psk: '12345678', + }) + }) + + it('should set eapConfig from values with known network', () => { + const network = { + ...Fixtures.mockWifiNetwork, + ssid: 'foobar', + securityType: SECURITY_WPA_EAP, + } + const values = { + securityType: 'someEapType', + eapConfig: { option1: 'fizzbuzz' }, + } + const result = connectFormToConfigureRequest(network, values) + + expect(result).toEqual({ + ssid: 'foobar', + securityType: SECURITY_WPA_EAP, + hidden: false, + eapConfig: { eapType: 'someEapType', option1: 'fizzbuzz' }, + }) + }) + + it('should set eapConfig from values with unknown network', () => { + const values = { + ssid: 'foobar', + securityType: 'someEapType', + eapConfig: { option1: 'fizzbuzz' }, + } + const result = connectFormToConfigureRequest(null, values) + + expect(result).toEqual({ + ssid: 'foobar', + securityType: SECURITY_WPA_EAP, + hidden: true, + eapConfig: { eapType: 'someEapType', option1: 'fizzbuzz' }, + }) + }) +}) diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/form-fields.ts b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/form-fields.ts index 1a91d2ac994..b8caeb3824c 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/form-fields.ts +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/form-fields.ts @@ -1,9 +1,9 @@ import get from 'lodash/get' import * as Constants from '../constants' -import * as Copy from '../i18n' import type { FieldError } from 'react-hook-form' +import type { TFunction } from 'i18next' import type { WifiNetwork, WifiKey, @@ -24,28 +24,29 @@ type Errors = Record export const renderLabel = (label: string, required: boolean): string => `${required ? '* ' : ''}${label}` -const FIELD_SSID: ConnectFormTextField = { +const makeFieldSsid = (t: TFunction): ConnectFormTextField => ({ type: Constants.FIELD_TYPE_TEXT, name: Constants.CONFIGURE_FIELD_SSID, - label: renderLabel(Copy.LABEL_SSID, true), + label: renderLabel(t('network_name'), true), isPassword: false, -} +}) -const FIELD_PSK: ConnectFormTextField = { +const makeFieldPsk = (t: TFunction): ConnectFormTextField => ({ type: Constants.FIELD_TYPE_TEXT, name: Constants.CONFIGURE_FIELD_PSK, - label: renderLabel(Copy.LABEL_PSK, true), + label: renderLabel(t('password'), true), isPassword: true, -} +}) const makeSecurityField = ( eapOptions: EapOption[], - showAllOptions: boolean + showAllOptions: boolean, + t: TFunction ): ConnectFormSecurityField => ({ type: Constants.FIELD_TYPE_SECURITY, name: Constants.CONFIGURE_FIELD_SECURITY_TYPE, - label: renderLabel(Copy.LABEL_SECURITY, true), - placeholder: Copy.SELECT_AUTHENTICATION_METHOD, + label: renderLabel(t('authentication'), true), + placeholder: t('select_auth_method_short'), eapOptions, showAllOptions, }) @@ -77,21 +78,22 @@ export function getConnectFormFields( robotName: string, eapOptions: EapOption[], wifiKeys: WifiKey[], - values: ConnectFormValues + values: ConnectFormValues, + t: TFunction ): ConnectFormField[] { const { securityType: formSecurityType } = values const fields = [] // if the network is unknown, display a field to enter the SSID if (network === null) { - fields.push(FIELD_SSID) + fields.push(makeFieldSsid(t)) } // if the network is unknown or the known network is EAP, display a // security dropdown; security dropdown will handle which options to // display based on known or unknown network if (!network || network.securityType === Constants.SECURITY_WPA_EAP) { - fields.push(makeSecurityField(eapOptions, !network)) + fields.push(makeSecurityField(eapOptions, !network, t)) } // if known network is PSK or network is unknown and user has selected PSK @@ -100,7 +102,7 @@ export function getConnectFormFields( network?.securityType === Constants.SECURITY_WPA_PSK || formSecurityType === Constants.SECURITY_WPA_PSK ) { - fields.push(FIELD_PSK) + fields.push(makeFieldPsk(t)) } // if known network is EAP or user selected EAP, map eap options to fields @@ -121,7 +123,7 @@ export function getConnectFormFields( label, robotName, wifiKeys, - placeholder: Copy.SELECT_FILE, + placeholder: t('select_file'), } } @@ -142,7 +144,8 @@ export function validateConnectFormFields( network: WifiNetwork | null, eapOptions: EapOption[], values: ConnectFormValues, - errors: Errors + errors: Errors, + t: TFunction ): Errors { const { ssid: formSsid, @@ -152,7 +155,7 @@ export function validateConnectFormFields( let errorMessage: string | undefined if (network === null && (formSsid == null || formSsid.length === 0)) { - errorMessage = Copy.FIELD_IS_REQUIRED(Copy.LABEL_SSID) + errorMessage = t('field_is_required', { field: t('network_name') }) return errorMessage != null ? { ...errors, @@ -168,7 +171,7 @@ export function validateConnectFormFields( (network === null || network.securityType === Constants.SECURITY_WPA_EAP) && !formSecurityType ) { - errorMessage = Copy.FIELD_IS_REQUIRED(Copy.LABEL_SECURITY) + errorMessage = t('field_is_required', { field: t('authentication') }) return errorMessage != null ? { ...errors, @@ -185,10 +188,9 @@ export function validateConnectFormFields( formSecurityType === Constants.SECURITY_WPA_PSK) && (!formPsk || formPsk.length < Constants.CONFIGURE_PSK_MIN_LENGTH) ) { - errorMessage = Copy.FIELD_NOT_LONG_ENOUGH( - Copy.LABEL_PSK, - Constants.CONFIGURE_PSK_MIN_LENGTH - ) + errorMessage = t('password_not_long_enough', { + minLength: Constants.CONFIGURE_PSK_MIN_LENGTH, + }) return errorMessage != null ? { ...errors, @@ -215,7 +217,9 @@ export function validateConnectFormFields( ) => { const fieldName = getEapFieldName(name) const errorMessage = - displayName != null ? Copy.FIELD_IS_REQUIRED(displayName) : '' + displayName != null + ? t('field_is_required', { field: displayName }) + : '' if (errorMessage != null) { acc[fieldName] = { diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/index.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/index.tsx index 3e1c731d33e..2b5d228c12b 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/index.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/index.tsx @@ -1,4 +1,5 @@ import { useForm } from 'react-hook-form' +import { useTranslation } from 'react-i18next' import { useResetFormOnSecurityChange } from './form-state' import { @@ -10,6 +11,7 @@ import { import { FormModal } from './FormModal' import type { Control, Resolver } from 'react-hook-form' +import type { TFunction } from 'i18next' import type { ConnectFormValues, WifiConfigureRequest, @@ -35,6 +37,7 @@ interface ConnectModalComponentProps extends ConnectModalProps { } export const ConnectModal = (props: ConnectModalProps): JSX.Element => { + const { t } = useTranslation(['device_settings', 'shared']) const { network, eapOptions, onConnect } = props const onSubmit = (values: ConnectFormValues): void => { @@ -45,7 +48,13 @@ export const ConnectModal = (props: ConnectModalProps): JSX.Element => { const handleValidate: Resolver = values => { let errors = {} - errors = validateConnectFormFields(network, eapOptions, values, errors) + errors = validateConnectFormFields( + network, + eapOptions, + values, + errors, + t as TFunction + ) return { values, errors } } @@ -78,6 +87,7 @@ export const ConnectModal = (props: ConnectModalProps): JSX.Element => { export const ConnectModalComponent = ( props: ConnectModalComponentProps ): JSX.Element => { + const { t } = useTranslation(['device_settings', 'shared']) const { robotName, network, @@ -95,7 +105,8 @@ export const ConnectModalComponent = ( robotName, eapOptions, wifiKeys, - values + values, + t as TFunction ) useResetFormOnSecurityChange() diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx index 6628c35dfc5..3a372c6df66 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx @@ -1,6 +1,6 @@ +import { useTranslation } from 'react-i18next' import { AlertModal, SpinnerModal } from '@opentrons/components' -import * as Copy from './i18n' import { ErrorModal } from '/app/molecules/modals' import { DISCONNECT } from './constants' import { PENDING, FAILURE } from '/app/redux/robot-api' @@ -18,29 +18,32 @@ export interface ResultModalProps { export const ResultModal = (props: ResultModalProps): JSX.Element => { const { type, ssid, requestStatus, error, onClose } = props + const { t } = useTranslation(['device_settings', 'shared']) const isDisconnect = type === DISCONNECT if (requestStatus === PENDING) { const message = isDisconnect - ? Copy.DISCONNECTING_FROM_NETWORK(ssid) - : Copy.CONNECTING_TO_NETWORK(ssid) + ? t('disconnecting_from_wifi_network', { ssid: ssid }) + : t('connecting_to_wifi_network', { ssid: ssid }) return } if (error || requestStatus === FAILURE) { const heading = isDisconnect - ? Copy.UNABLE_TO_DISCONNECT - : Copy.UNABLE_TO_CONNECT + ? t('unable_to_disconnect') + : t('unable_to_connect') const message = isDisconnect - ? Copy.YOUR_ROBOT_WAS_UNABLE_TO_DISCONNECT(ssid) - : Copy.YOUR_ROBOT_WAS_UNABLE_TO_CONNECT(ssid) + ? t('disconnect_from_wifi_network_failure', { ssid: ssid }) + : t('connect_to_wifi_network_failure', { ssid: ssid }) - const retryMessage = !isDisconnect ? ` ${Copy.CHECK_YOUR_CREDENTIALS}.` : '' + const retryMessage = !isDisconnect ? t('please_check_credentials') : '' const placeholderError = { - message: `Likely incorrect network password. ${Copy.CHECK_YOUR_CREDENTIALS}.`, + message: `${t('likely_incorrect_password')} ${t( + 'please_check_credentials' + )}.`, } return ( @@ -54,12 +57,12 @@ export const ResultModal = (props: ResultModalProps): JSX.Element => { } const heading = isDisconnect - ? Copy.SUCCESSFULLY_DISCONNECTED - : Copy.SUCCESSFULLY_CONNECTED + ? t('successfully_disconnected') + : t('successfully_connected_to_wifi') const message = isDisconnect - ? Copy.YOUR_ROBOT_HAS_DISCONNECTED(ssid) - : Copy.YOUR_ROBOT_HAS_CONNECTED(ssid) + ? t('disconnect_from_wifi_network_success') + : t('successfully_connected_to_ssid', { ssid: ssid }) return ( { iconName="wifi" heading={heading} onCloseClick={props.onClose} - buttons={[{ children: Copy.CLOSE, onClick: onClose }]} + buttons={[{ children: t('shared:close'), onClick: onClose }]} > {message} diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/SelectSsid/index.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/SelectSsid/index.tsx index b85cc72d563..a8b04feba4b 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/SelectSsid/index.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/SelectSsid/index.tsx @@ -1,9 +1,10 @@ import type * as React from 'react' +import { useTranslation } from 'react-i18next' import { CONTEXT_MENU } from '@opentrons/components' import { SelectField } from '/app/atoms/SelectField' -import * as Copy from '../i18n' import { NetworkOptionLabel, NetworkActionLabel } from './NetworkOptionLabel' +import type { TFunction } from 'i18next' import type { SelectOptionOrGroup } from '@opentrons/components' import type { WifiNetwork } from '../types' @@ -20,11 +21,16 @@ const FIELD_NAME = '__SelectSsid__' const JOIN_OTHER_VALUE = '__join-other-network__' -const SELECT_JOIN_OTHER_GROUP = { - options: [{ value: JOIN_OTHER_VALUE, label: Copy.LABEL_JOIN_OTHER_NETWORK }], -} +const formatOptions = ( + list: WifiNetwork[], + t: TFunction +): SelectOptionOrGroup[] => { + const SELECT_JOIN_OTHER_GROUP = { + options: [ + { value: JOIN_OTHER_VALUE, label: `${t('join_other_network')}...` }, + ], + } -const formatOptions = (list: WifiNetwork[]): SelectOptionOrGroup[] => { const ssidOptionsList = { options: list?.map(({ ssid }) => ({ value: ssid })), } @@ -34,6 +40,7 @@ const formatOptions = (list: WifiNetwork[]): SelectOptionOrGroup[] => { } export function SelectSsid(props: SelectSsidProps): JSX.Element { + const { t } = useTranslation('device_settings') const { list, value, onConnect, onJoinOther, isRobotBusy } = props const handleValueChange = (_: string, value: string): void => { @@ -69,8 +76,8 @@ export function SelectSsid(props: SelectSsidProps): JSX.Element { disabled={isRobotBusy} name={FIELD_NAME} value={value} - options={formatOptions(list)} - placeholder={Copy.SELECT_NETWORK} + options={formatOptions(list, t as TFunction)} + placeholder={t('choose_a_network')} onValueChange={handleValueChange} formatOptionLabel={formatOptionLabel} width="16rem" diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/i18n.ts b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/i18n.ts deleted file mode 100644 index cfee1e77d89..00000000000 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/i18n.ts +++ /dev/null @@ -1,103 +0,0 @@ -// TODO(mc, 2020-03-11): i18n -import { - SECURITY_WPA_PSK, - SECURITY_WPA_EAP, - SECURITY_NONE, -} from '/app/redux/networking' - -import type { WifiNetwork } from './types' - -const SECURITY_DESC = { - [SECURITY_WPA_PSK]: 'requires a WPA2 password', - [SECURITY_WPA_EAP]: 'requires 802.1X authentication', - [SECURITY_NONE]: 'is unsecured', -} - -export const FIND_AND_JOIN_A_NETWORK = 'Find and join a Wi-Fi network' - -export const ENTER_NAME_AND_SECURITY_TYPE = - 'Enter the network name and security type.' - -const WIFI_NETWORK = 'Wi-Fi network' - -export const CANCEL = 'cancel' - -export const CONNECT = 'connect' - -export const CLOSE = 'close' - -export const DISCONNECT = 'disconnect' - -export const LABEL_SSID = 'Network Name (SSID)' - -export const LABEL_PSK = 'Password' - -export const LABEL_SECURITY = 'Authentication' - -export const LABEL_SECURITY_NONE = 'None' - -export const LABEL_SECURITY_PSK = 'WPA2 Personal' - -export const LABEL_ADD_NEW_KEY = 'Add new...' - -export const LABEL_SHOW_PASSWORD = 'Show password' - -export const LABEL_JOIN_OTHER_NETWORK = 'Join other network...' - -export const SELECT_AUTHENTICATION_METHOD = 'Select authentication method' - -export const SELECT_FILE = 'Select file' - -export const SELECT_NETWORK = 'Choose a network...' - -export const SUCCESSFULLY_DISCONNECTED = 'Successfully disconnected from Wi-Fi' - -export const SUCCESSFULLY_CONNECTED = 'Successfully connected to Wi-Fi' - -export const UNABLE_TO_DISCONNECT = 'Unable to disconnect from Wi-Fi' - -export const UNABLE_TO_CONNECT = 'Unable to connect to Wi-Fi' - -export const CHECK_YOUR_CREDENTIALS = - 'Please double-check your network credentials' - -export const CONNECT_TO_SSID = (ssid: string): string => `Connect to ${ssid}` - -export const DISCONNECT_FROM_SSID = (ssid: string): string => - `Disconnect from ${ssid}` - -export const ARE_YOU_SURE_YOU_WANT_TO_DISCONNECT = (ssid: string): string => - `Are you sure you want to disconnect from ${ssid}?` - -export const NETWORK_REQUIRES_SECURITY = (network: WifiNetwork): string => - `${WIFI_NETWORK} ${network.ssid} ${SECURITY_DESC[network.securityType]}` - -export const FIELD_IS_REQUIRED = (name: string): string => `${name} is required` - -export const FIELD_NOT_LONG_ENOUGH = ( - name: string, - minLength: number -): string => `${name} must be at least ${minLength} characters` - -const renderMaybeSsid = (ssid: string | null): string => - ssid !== null ? ` network ${ssid}` : '' - -export const CONNECTING_TO_NETWORK = (ssid: string | null): string => - `Connecting to Wi-Fi${renderMaybeSsid(ssid)}` - -export const DISCONNECTING_FROM_NETWORK = (ssid: string | null): string => - `Disconnecting from Wi-Fi${renderMaybeSsid(ssid)}` - -export const YOUR_ROBOT_WAS_UNABLE_TO_CONNECT = (ssid: string | null): string => - `Your robot was unable to connect to Wi-Fi${renderMaybeSsid(ssid)}` - -export const YOUR_ROBOT_WAS_UNABLE_TO_DISCONNECT = ( - ssid: string | null -): string => - `Your robot was unable to disconnect from Wi-Fi${renderMaybeSsid(ssid)}` - -export const YOUR_ROBOT_HAS_DISCONNECTED = (ssid: string | null): string => - `Your robot has successfully disconnected from Wi-Fi${renderMaybeSsid(ssid)}` - -export const YOUR_ROBOT_HAS_CONNECTED = (ssid: string | null): string => - `Your robot has successfully connected to Wi-Fi${renderMaybeSsid(ssid)}` diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx index 4b2225fe868..da77dee89c9 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx @@ -88,7 +88,7 @@ export function UpdateRobotModal({ let disabledReason: string = '' if (updateFromFileDisabledReason) - disabledReason = updateFromFileDisabledReason + disabledReason = t(updateFromFileDisabledReason) else if (isRobotBusy) disabledReason = t('robot_busy_protocol') useEffect(() => { diff --git a/app/src/organisms/GripperWizardFlows/BeforeBeginning.tsx b/app/src/organisms/GripperWizardFlows/BeforeBeginning.tsx index 89395aeee10..316a3a5526a 100644 --- a/app/src/organisms/GripperWizardFlows/BeforeBeginning.tsx +++ b/app/src/organisms/GripperWizardFlows/BeforeBeginning.tsx @@ -13,6 +13,8 @@ import { SCREWDRIVER_LOADNAME, GRIPPER_LOADNAME, CAL_PIN_LOADNAME, + CALIBRATION_PIN_DISPLAY_NAME, + HEX_SCREWDRIVER_DISPLAY_NAME, } from './constants' import type { UseMutateFunction } from 'react-query' @@ -105,9 +107,9 @@ export const BeforeBeginning = ( const equipmentInfoByLoadName: { [loadName: string]: { displayName: string; subtitle?: string } } = { - calibration_pin: { displayName: t('calibration_pin') }, + calibration_pin: { displayName: CALIBRATION_PIN_DISPLAY_NAME }, hex_screwdriver: { - displayName: t('hex_screwdriver'), + displayName: HEX_SCREWDRIVER_DISPLAY_NAME, subtitle: t('provided_with_robot_use_right_size'), }, [GRIPPER_LOADNAME]: { displayName: t('branded:gripper') }, diff --git a/app/src/organisms/GripperWizardFlows/constants.ts b/app/src/organisms/GripperWizardFlows/constants.ts index db06adc340f..a5b633bab14 100644 --- a/app/src/organisms/GripperWizardFlows/constants.ts +++ b/app/src/organisms/GripperWizardFlows/constants.ts @@ -13,6 +13,10 @@ export const GRIPPER_FLOW_TYPES = { RECALIBRATE: 'RECALIBRATE', } as const +// note: we will not translate these item titles to be consistent with manuals +export const CALIBRATION_PIN_DISPLAY_NAME = 'Calibration Pin' +export const HEX_SCREWDRIVER_DISPLAY_NAME = '2.5 mm Hex Screwdriver' + // pin movements export const MOVE_PIN_TO_FRONT_JAW = 'movePinToFrontJaw' as const diff --git a/app/src/organisms/ODD/QuickTransferFlow/NameQuickTransfer.tsx b/app/src/organisms/ODD/QuickTransferFlow/NameQuickTransfer.tsx index 8bff060ac38..2ef4592f568 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/NameQuickTransfer.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/NameQuickTransfer.tsx @@ -33,7 +33,6 @@ export function NameQuickTransfer(props: NameQuickTransferProps): JSX.Element { if (name.length > 60) { error = t('character_limit_error') } - // TODO add error handling for quick transfer name replication return createPortal( diff --git a/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx b/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx index ffcb72dae18..a69e095a674 100644 --- a/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx +++ b/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx @@ -107,6 +107,10 @@ export const BeforeBeginning = ( let equipmentList = [CALIBRATION_PROBE] const proceedButtonText = t('move_gantry_to_front') + const hexScrewdriverWithSubtitle = { + ...HEX_SCREWDRIVER, + subtitle: t('provided_with_robot'), + } let bodyTranslationKey: string = '' switch (flowType) { @@ -124,7 +128,7 @@ export const BeforeBeginning = ( equipmentList = [ { ...PIPETTE, displayName: displayName ?? PIPETTE.displayName }, CALIBRATION_PROBE, - HEX_SCREWDRIVER, + hexScrewdriverWithSubtitle, ] } else { equipmentList = [ @@ -133,7 +137,7 @@ export const BeforeBeginning = ( displayName: displayName ?? NINETY_SIX_CHANNEL_PIPETTE.displayName, }, CALIBRATION_PROBE, - HEX_SCREWDRIVER, + hexScrewdriverWithSubtitle, NINETY_SIX_CHANNEL_MOUNTING_PLATE, ] } @@ -148,19 +152,19 @@ export const BeforeBeginning = ( equipmentList = [ { ...NINETY_SIX_CHANNEL_PIPETTE, displayName }, CALIBRATION_PROBE, - HEX_SCREWDRIVER, + hexScrewdriverWithSubtitle, NINETY_SIX_CHANNEL_MOUNTING_PLATE, ] } else { equipmentList = [ { ...PIPETTE, displayName }, CALIBRATION_PROBE, - HEX_SCREWDRIVER, + hexScrewdriverWithSubtitle, ] } } else { bodyTranslationKey = 'get_started_detach' - equipmentList = [HEX_SCREWDRIVER] + equipmentList = [hexScrewdriverWithSubtitle] } break } diff --git a/app/src/organisms/PipetteWizardFlows/constants.ts b/app/src/organisms/PipetteWizardFlows/constants.ts index e4ddd762d95..1d0b94878c1 100644 --- a/app/src/organisms/PipetteWizardFlows/constants.ts +++ b/app/src/organisms/PipetteWizardFlows/constants.ts @@ -18,6 +18,8 @@ export const FLOWS = { DETACH: 'DETACH', CALIBRATE: 'CALIBRATE', } + +// note: we will not be translating these item titles to be consistent with manuals export const CALIBRATION_PROBE_DISPLAY_NAME = 'Calibration Probe' export const HEX_SCREWDRIVER_DISPLAY_NAME = '2.5 mm Hex Screwdriver' export const PIPETTE_DISPLAY_NAME = '1- or 8-Channel Pipette' @@ -33,9 +35,6 @@ export const CALIBRATION_PROBE = { export const HEX_SCREWDRIVER = { loadName: 'hex_screwdriver', displayName: HEX_SCREWDRIVER_DISPLAY_NAME, - // TODO(jr, 4/3/23): add this subtitle to i18n - subtitle: - 'Provided with the robot. Using another size can strip the instruments’s screws.', } export const PIPETTE = { loadName: 'flex_pipette', diff --git a/app/src/pages/Desktop/Labware/index.tsx b/app/src/pages/Desktop/Labware/index.tsx index 83f9dd94f3f..5d8959a5861 100644 --- a/app/src/pages/Desktop/Labware/index.tsx +++ b/app/src/pages/Desktop/Labware/index.tsx @@ -63,6 +63,7 @@ const labwareDisplayCategoryFilters: LabwareFilter[] = [ 'wellPlate', ] +// note: we've decided not to translate these categories const FILTER_OPTIONS: DropdownOption[] = labwareDisplayCategoryFilters.map( category => ({ name: startCase(category), diff --git a/app/src/redux/robot-update/__tests__/selectors.test.ts b/app/src/redux/robot-update/__tests__/selectors.test.ts index e4f3e8f8283..079244b5f32 100644 --- a/app/src/redux/robot-update/__tests__/selectors.test.ts +++ b/app/src/redux/robot-update/__tests__/selectors.test.ts @@ -474,10 +474,10 @@ describe('robot update selectors', () => { expect(result).toMatchObject({ autoUpdateDisabledReason: expect.stringMatching( - /update server is not responding/ + /update_server_unavailable/ ), updateFromFileDisabledReason: expect.stringMatching( - /update server is not responding/ + /update_server_unavailable/ ), }) }) @@ -495,10 +495,10 @@ describe('robot update selectors', () => { expect(result).toMatchObject({ autoUpdateDisabledReason: expect.stringMatching( - /update server is not responding/ + /update_server_unavailable/ ), updateFromFileDisabledReason: expect.stringMatching( - /update server is not responding/ + /update_server_unavailable/ ), }) }) @@ -526,11 +526,9 @@ describe('robot update selectors', () => { const result = selectors.getRobotUpdateDisplayInfo(state, robotName) expect(result).toMatchObject({ - autoUpdateDisabledReason: expect.stringMatching( - /updating a different robot/ - ), + autoUpdateDisabledReason: expect.stringMatching(/other_robot_updating/), updateFromFileDisabledReason: expect.stringMatching( - /updating a different robot/ + /other_robot_updating/ ), }) }) @@ -547,9 +545,7 @@ describe('robot update selectors', () => { expect(result).toEqual({ autoUpdateAction: expect.stringMatching(/unavailable/i), - autoUpdateDisabledReason: expect.stringMatching( - /unable to retrieve update/i - ), + autoUpdateDisabledReason: expect.stringMatching(/no_update_files/i), updateFromFileDisabledReason: null, }) }) diff --git a/app/src/redux/robot-update/selectors.ts b/app/src/redux/robot-update/selectors.ts index 0570a9dd2c8..a2427dfefb5 100644 --- a/app/src/redux/robot-update/selectors.ts +++ b/app/src/redux/robot-update/selectors.ts @@ -19,15 +19,6 @@ import type { RobotUpdateTarget, } from './types' -// TODO(mc, 2020-08-02): i18n -const UPDATE_SERVER_UNAVAILABLE = - "Unable to update because your robot's update server is not responding." -const OTHER_ROBOT_UPDATING = - 'Unable to update because the app is currently updating a different robot.' -const NO_UPDATE_FILES = - 'Unable to retrieve update for this robot. Ensure your computer is connected to the internet and try again later.' -const UNAVAILABLE = 'Update unavailable' - export const getRobotUpdateTarget: ( state: State, robotName: string @@ -198,6 +189,7 @@ export function getRobotUpdateAvailable( : getRobotUpdateType(currentVersion, updateVersion) } +// this util returns i18n keys in device_settings export const getRobotUpdateDisplayInfo: ( state: State, robotName: string @@ -212,21 +204,21 @@ export const getRobotUpdateDisplayInfo: ( (robot, currentUpdatingRobot, updateVersion) => { const robotVersion = robot ? getRobotApiVersion(robot) : null const autoUpdateType = getRobotUpdateType(robotVersion, updateVersion) - const autoUpdateAction = autoUpdateType ?? UNAVAILABLE + const autoUpdateAction = autoUpdateType ?? 'update_unavailable' let autoUpdateDisabledReason = null let updateFromFileDisabledReason = null if (robot?.serverHealthStatus !== HEALTH_STATUS_OK) { - autoUpdateDisabledReason = UPDATE_SERVER_UNAVAILABLE - updateFromFileDisabledReason = UPDATE_SERVER_UNAVAILABLE + autoUpdateDisabledReason = 'update_server_unavailable' + updateFromFileDisabledReason = 'update_server_unavailable' } else if ( currentUpdatingRobot !== null && currentUpdatingRobot.name !== robot?.name ) { - autoUpdateDisabledReason = OTHER_ROBOT_UPDATING - updateFromFileDisabledReason = OTHER_ROBOT_UPDATING + autoUpdateDisabledReason = 'other_robot_updating' + updateFromFileDisabledReason = 'other_robot_updating' } else if (autoUpdateType === null) { - autoUpdateDisabledReason = NO_UPDATE_FILES + autoUpdateDisabledReason = 'no_update_files' } return { diff --git a/app/src/redux/system-info/constants.ts b/app/src/redux/system-info/constants.ts index 1502d4bba07..a79b6ffb7bb 100644 --- a/app/src/redux/system-info/constants.ts +++ b/app/src/redux/system-info/constants.ts @@ -22,12 +22,3 @@ export const USB_DEVICE_REMOVED: 'systemInfo:USB_DEVICE_REMOVED' = export const NETWORK_INTERFACES_CHANGED: 'systemInfo:NETWORK_INTERFACES_CHANGED' = 'systemInfo:NETWORK_INTERFACES_CHANGED' - -// copy -// TODO(mc, 2020-05-11): i18n -export const U2E_DRIVER_OUTDATED_MESSAGE = - 'There is an updated Realtek USB-to-Ethernet adapter driver available for your computer.' -export const U2E_DRIVER_DESCRIPTION = - 'The OT-2 uses this adapter for its USB connection to the Opentrons App.' -export const U2E_DRIVER_OUTDATED_CTA = - "Please update your computer's driver to ensure a reliable connection to your OT-2." diff --git a/shared-data/js/fixtures.ts b/shared-data/js/fixtures.ts index 4f13a8b0321..905429cd111 100644 --- a/shared-data/js/fixtures.ts +++ b/shared-data/js/fixtures.ts @@ -238,7 +238,7 @@ export function getAddressableAreaNamesFromLoadedModule( return [...acc, ...providedAddressableAreas] }, []) } - +// note: we've decided not to translate these strings export function getFixtureDisplayName( cutoutFixtureId: CutoutFixtureId | null, usbPortNumber?: number From 98afdcfd84af983608e57aff8f005ea2dfc3341e Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Tue, 17 Dec 2024 08:50:24 -0500 Subject: [PATCH 16/25] fix(app): Fix run start/finish protocol analytics (#17118) Closes EXEC-850 and EXEC-805 --- .../useRunHeaderModalContainer.ts | 12 ++++++++++++ .../ProtocolRunHeader/hooks/useRunAnalytics.ts | 7 ++----- .../ProtocolSetup/__tests__/ProtocolSetup.test.tsx | 1 - app/src/pages/ODD/ProtocolSetup/index.tsx | 8 ++++++++ .../__tests__/useTrackProtocolRunEvent.test.tsx | 8 ++------ .../analytics/hooks/useTrackProtocolRunEvent.ts | 3 +++ 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts index 48eda0ebfa5..1e0d1e5c073 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts @@ -16,9 +16,15 @@ import { useProtocolDetailsForRun } from '/app/resources/runs' import { getFallbackRobotSerialNumber } from '../utils' import { ANALYTICS_PROTOCOL_PROCEED_TO_RUN, + ANALYTICS_PROTOCOL_RUN_ACTION, useTrackEvent, } from '/app/redux/analytics' +import { + useRobotAnalyticsData, + useTrackProtocolRunEvent, +} from '/app/redux-resources/analytics' import { useRobot, useRobotType } from '/app/redux-resources/robots' + import type { AttachedModule, RunStatus, Run } from '@opentrons/api-client' import type { UseErrorRecoveryResult } from '/app/organisms/ErrorRecoveryFlows' import type { @@ -71,7 +77,9 @@ export function useRunHeaderModalContainer({ const robot = useRobot(robotName) const robotSerialNumber = getFallbackRobotSerialNumber(robot) const trackEvent = useTrackEvent() + const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) const robotType = useRobotType(robotName) + const robotAnalyticsData = useRobotAnalyticsData(robotName) function handleProceedToRunClick(): void { navigate(`/devices/${robotName}/protocol-runs/${runId}/run-preview`) @@ -79,6 +87,10 @@ export function useRunHeaderModalContainer({ name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, properties: { robotSerialNumber }, }) + trackProtocolRunEvent({ + name: ANALYTICS_PROTOCOL_RUN_ACTION.START, + properties: robotAnalyticsData ?? {}, + }) protocolRunControls.play() } diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunAnalytics.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunAnalytics.ts index 31399cbc541..95658999f4a 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunAnalytics.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/hooks/useRunAnalytics.ts @@ -28,15 +28,12 @@ export function useRunAnalytics({ useEffect(() => { const areReportConditionsValid = - isRunCurrent && - runId != null && - robotAnalyticsData != null && - isTerminalRunStatus(runStatus) + isRunCurrent && runId != null && isTerminalRunStatus(runStatus) if (areReportConditionsValid) { trackProtocolRunEvent({ name: ANALYTICS_PROTOCOL_RUN_ACTION.FINISH, - properties: robotAnalyticsData, + properties: robotAnalyticsData ?? undefined, }) } }, [runStatus, isRunCurrent, runId, robotAnalyticsData]) diff --git a/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index 5f7d1f8cfc8..5863d70ba93 100644 --- a/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -576,7 +576,6 @@ describe('ProtocolSetup', () => { render(`/runs/${RUN_ID}/setup/`) fireEvent.click(screen.getByRole('button', { name: 'play' })) - expect(mockTrackProtocolRunEvent).toBeCalledTimes(1) expect(mockTrackProtocolRunEvent).toHaveBeenCalledWith({ name: ANALYTICS_PROTOCOL_RUN_ACTION.START, properties: {}, diff --git a/app/src/pages/ODD/ProtocolSetup/index.tsx b/app/src/pages/ODD/ProtocolSetup/index.tsx index 25c978e6717..1df659c633b 100644 --- a/app/src/pages/ODD/ProtocolSetup/index.tsx +++ b/app/src/pages/ODD/ProtocolSetup/index.tsx @@ -741,11 +741,19 @@ export function ProtocolSetup(): JSX.Element { robotType, protocolName ) + + const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) + const robotAnalyticsData = useRobotAnalyticsData(robotName) + const handleProceedToRunClick = (): void => { trackEvent({ name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, properties: { robotSerialNumber }, }) + trackProtocolRunEvent({ + name: ANALYTICS_PROTOCOL_RUN_ACTION.START, + properties: robotAnalyticsData ?? {}, + }) play() } const configBypassHeaterShakerAttachmentConfirmation = useSelector( diff --git a/app/src/redux-resources/analytics/hooks/__tests__/useTrackProtocolRunEvent.test.tsx b/app/src/redux-resources/analytics/hooks/__tests__/useTrackProtocolRunEvent.test.tsx index 3172c8d1fbc..f769dc005c4 100644 --- a/app/src/redux-resources/analytics/hooks/__tests__/useTrackProtocolRunEvent.test.tsx +++ b/app/src/redux-resources/analytics/hooks/__tests__/useTrackProtocolRunEvent.test.tsx @@ -2,7 +2,7 @@ import type * as React from 'react' import { createStore } from 'redux' import { Provider } from 'react-redux' import { QueryClient, QueryClientProvider } from 'react-query' -import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' +import { vi, it, expect, describe, beforeEach } from 'vitest' import { when } from 'vitest-when' import { waitFor, renderHook } from '@testing-library/react' @@ -63,10 +63,6 @@ describe('useTrackProtocolRunEvent hook', () => { }) }) - afterEach(() => { - vi.resetAllMocks() - }) - it('returns trackProtocolRunEvent function', () => { const { result } = renderHook( () => useTrackProtocolRunEvent(RUN_ID, ROBOT_NAME), @@ -92,7 +88,7 @@ describe('useTrackProtocolRunEvent hook', () => { ) expect(mockTrackEvent).toHaveBeenCalledWith({ name: ANALYTICS_PROTOCOL_RUN_ACTION.START, - properties: PROTOCOL_PROPERTIES, + properties: { ...PROTOCOL_PROPERTIES, transactionId: RUN_ID }, }) }) diff --git a/app/src/redux-resources/analytics/hooks/useTrackProtocolRunEvent.ts b/app/src/redux-resources/analytics/hooks/useTrackProtocolRunEvent.ts index 2f9f085fd64..05c3ce16746 100644 --- a/app/src/redux-resources/analytics/hooks/useTrackProtocolRunEvent.ts +++ b/app/src/redux-resources/analytics/hooks/useTrackProtocolRunEvent.ts @@ -34,6 +34,9 @@ export function useTrackProtocolRunEvent( ...properties, ...protocolRunAnalyticsData, runTime, + // It's sometimes unavoidable (namely on the desktop app) to prevent sending an event multiple times. + // In these circumstances, we need an idempotency key to accurately filter events in Mixpanel. + transactionId: runId, }, }) }) From 5ff3efdabff4d63d1b47f2f76dba69638ac1da15 Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 17 Dec 2024 10:50:04 -0500 Subject: [PATCH 17/25] fix(protocol-designer): add gridgap to between error and warning (#17116) * fix(protocol-designer): add gridgap to between error and warning --- protocol-designer/src/organisms/Alerts/FormAlerts.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/protocol-designer/src/organisms/Alerts/FormAlerts.tsx b/protocol-designer/src/organisms/Alerts/FormAlerts.tsx index b6080a05a73..8348b5bd798 100644 --- a/protocol-designer/src/organisms/Alerts/FormAlerts.tsx +++ b/protocol-designer/src/organisms/Alerts/FormAlerts.tsx @@ -184,6 +184,7 @@ function FormAlertsComponent(props: FormAlertsProps): JSX.Element | null { {showFormErrors ? formErrors.map((error, key) => makeAlert('error', error, key)) From c7521976766e0412250ac1e5c3c0e40de96d13d8 Mon Sep 17 00:00:00 2001 From: Shlok Amin Date: Tue, 17 Dec 2024 14:17:37 -0500 Subject: [PATCH 18/25] feat(opentrons-ai-client): add shared-data as dependency (#16663) --- opentrons-ai-client/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/opentrons-ai-client/package.json b/opentrons-ai-client/package.json index 9f636be46d9..a75473bd680 100644 --- a/opentrons-ai-client/package.json +++ b/opentrons-ai-client/package.json @@ -22,6 +22,7 @@ "@auth0/auth0-react": "2.2.4", "@fontsource/public-sans": "5.0.3", "@opentrons/components": "link:../components", + "@opentrons/shared-data": "link:../shared-data", "axios": "^0.21.1", "i18next": "^19.8.3", "jotai": "2.8.0", From f8def7767d6a237c64cb18560fd24c02c45c0a17 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:35:48 -0500 Subject: [PATCH 19/25] feat(protocol-designer,-shared-data): add liquid class scaffolding to PD (#17126) This PR introduces initial work for liquid class support in protocol designer. I create a full liquid class interface based on the API's Pydantic model. I also add utilities for getting all liquid class definitions and a feature flag to hide liquid classes from the UI. Lastly, I add some of the basic functionality for assigning a liquid class to a defined liquid, and displaying that liquid class in LiquidCard. A liquid class assigned to a defined liquid will be accessible through extended existing selectors (allIngredientNamesIds) for step forms. closes AUTH-965 closes AUTH-968 --- .../assets/localization/en/feature_flags.json | 4 + .../src/assets/localization/en/liquids.json | 4 + .../src/feature-flags/reducers.ts | 2 + .../src/feature-flags/selectors.ts | 4 + protocol-designer/src/feature-flags/types.ts | 2 + .../__tests__/ingredients.test.ts | 2 + .../src/labware-ingred/selectors.ts | 1 + protocol-designer/src/labware-ingred/types.ts | 19 ++- protocol-designer/src/liquid-defs/utils.ts | 15 +++ .../AssignLiquidsModal/LiquidCard.tsx | 17 ++- .../AssignLiquidsModal/LiquidToolbox.tsx | 5 + .../organisms/DefineLiquidsModal/index.tsx | 65 +++++++++- .../__tests__/MaterialsListModal.test.tsx | 1 + .../__tests__/LiquidsOverflowMenu.test.tsx | 1 + .../ProtocolOverview/LiquidDefinitions.tsx | 111 ++++++++++++------ .../__tests__/LiquidDefinitions.test.tsx | 5 + shared-data/js/index.ts | 1 + shared-data/js/liquidClasses.ts | 8 ++ shared-data/js/types.ts | 100 ++++++++++++++++ 19 files changed, 312 insertions(+), 55 deletions(-) create mode 100644 protocol-designer/src/liquid-defs/utils.ts create mode 100644 shared-data/js/liquidClasses.ts diff --git a/protocol-designer/src/assets/localization/en/feature_flags.json b/protocol-designer/src/assets/localization/en/feature_flags.json index 9a13b327be8..92a074088ba 100644 --- a/protocol-designer/src/assets/localization/en/feature_flags.json +++ b/protocol-designer/src/assets/localization/en/feature_flags.json @@ -31,5 +31,9 @@ "OT_PD_ENABLE_REACT_SCAN": { "title": "Enable React Scan", "description": "Enable React Scan support for components rendering check" + }, + "OT_PD_ENABLE_LIQUID_CLASSES": { + "title": "Enable liquid classes", + "description": "Enable liquid classes support" } } diff --git a/protocol-designer/src/assets/localization/en/liquids.json b/protocol-designer/src/assets/localization/en/liquids.json index ebd4800dacb..36a31cf8116 100644 --- a/protocol-designer/src/assets/localization/en/liquids.json +++ b/protocol-designer/src/assets/localization/en/liquids.json @@ -10,6 +10,10 @@ "display_color": "Color", "liquid_volume": "Liquid volume by well", "liquid": "Liquid", + "liquid_class": { + "title": "Liquid class", + "tooltip": "Applies predefined pipetting settings to transfer and mix steps using this liquid" + }, "liquids_added": "Liquids added", "liquids": "Liquids", "microliters": "µL", diff --git a/protocol-designer/src/feature-flags/reducers.ts b/protocol-designer/src/feature-flags/reducers.ts index 42782c88479..bcffd39c5e7 100644 --- a/protocol-designer/src/feature-flags/reducers.ts +++ b/protocol-designer/src/feature-flags/reducers.ts @@ -30,6 +30,8 @@ const initialFlags: Flags = { OT_PD_ENABLE_HOT_KEYS_DISPLAY: process.env.OT_PD_ENABLE_HOT_KEYS_DISPLAY === '1' || true, OT_PD_ENABLE_REACT_SCAN: process.env.OT_PD_ENABLE_REACT_SCAN === '1' || false, + OT_PD_ENABLE_LIQUID_CLASSES: + process.env.OT_PD_ENABLE_REACT_SCAN === '1' || false, } // @ts-expect-error(sa, 2021-6-10): cannot use string literals as action type // TODO IMMEDIATELY: refactor this to the old fashioned way if we cannot have type safety: https://github.com/redux-utilities/redux-actions/issues/282#issuecomment-595163081 diff --git a/protocol-designer/src/feature-flags/selectors.ts b/protocol-designer/src/feature-flags/selectors.ts index 72b25fca895..6b8a70f8b30 100644 --- a/protocol-designer/src/feature-flags/selectors.ts +++ b/protocol-designer/src/feature-flags/selectors.ts @@ -45,3 +45,7 @@ export const getEnableReactScan: Selector = createSelector( getFeatureFlagData, flags => flags.OT_PD_ENABLE_REACT_SCAN ?? false ) +export const getEnableLiquidClasses: Selector = createSelector( + getFeatureFlagData, + flags => flags.OT_PD_ENABLE_LIQUID_CLASSES ?? false +) diff --git a/protocol-designer/src/feature-flags/types.ts b/protocol-designer/src/feature-flags/types.ts index 84bab18e474..6840786d149 100644 --- a/protocol-designer/src/feature-flags/types.ts +++ b/protocol-designer/src/feature-flags/types.ts @@ -36,6 +36,7 @@ export type FlagTypes = | 'OT_PD_ENABLE_RETURN_TIP' | 'OT_PD_ENABLE_HOT_KEYS_DISPLAY' | 'OT_PD_ENABLE_REACT_SCAN' + | 'OT_PD_ENABLE_LIQUID_CLASSES' // flags that are not in this list only show in prerelease mode export const userFacingFlags: FlagTypes[] = [ 'OT_PD_DISABLE_MODULE_RESTRICTIONS', @@ -49,5 +50,6 @@ export const allFlags: FlagTypes[] = [ 'OT_PD_ENABLE_COMMENT', 'OT_PD_ENABLE_RETURN_TIP', 'OT_PD_ENABLE_REACT_SCAN', + 'OT_PD_ENABLE_LIQUID_CLASSES', ] export type Flags = Partial> diff --git a/protocol-designer/src/labware-ingred/__tests__/ingredients.test.ts b/protocol-designer/src/labware-ingred/__tests__/ingredients.test.ts index b771cd16bbf..c81883001ac 100644 --- a/protocol-designer/src/labware-ingred/__tests__/ingredients.test.ts +++ b/protocol-designer/src/labware-ingred/__tests__/ingredients.test.ts @@ -19,6 +19,7 @@ describe('DUPLICATE_LABWARE action', () => { wellDetailsByLocation: null, concentration: '50 mol/ng', description: '', + liquidClass: null, displayColor: '#b925ff', serialize: false, }, @@ -27,6 +28,7 @@ describe('DUPLICATE_LABWARE action', () => { wellDetailsByLocation: null, concentration: '100%', description: '', + liquidClass: null, displayColor: '#ffd600', serialize: false, }, diff --git a/protocol-designer/src/labware-ingred/selectors.ts b/protocol-designer/src/labware-ingred/selectors.ts index eb7767af225..25ee8bc0966 100644 --- a/protocol-designer/src/labware-ingred/selectors.ts +++ b/protocol-designer/src/labware-ingred/selectors.ts @@ -113,6 +113,7 @@ const allIngredientNamesIds: Selector< ingredientId: ingredId, name: ingreds[ingredId].name, displayColor: ingreds[ingredId].displayColor, + liquidClass: ingreds[ingredId].liquidClass, })) }) const getLabwareSelectionMode: Selector = createSelector( diff --git a/protocol-designer/src/labware-ingred/types.ts b/protocol-designer/src/labware-ingred/types.ts index 6e9567722f3..6b7628735d8 100644 --- a/protocol-designer/src/labware-ingred/types.ts +++ b/protocol-designer/src/labware-ingred/types.ts @@ -19,23 +19,22 @@ export interface WellContents { selected?: boolean maxVolume?: number } -export type ContentsByWell = { - [wellName: string]: WellContents -} | null -export interface WellContentsByLabware { - [labwareId: string]: ContentsByWell -} +export type ContentsByWell = Record | null +export type WellContentsByLabware = Record // ==== INGREDIENTS ==== +// TODO(ND: 12/17/2024): add migration for liquids in >8.3.0 export type OrderedLiquids = Array<{ ingredientId: string - name: string | null | undefined - displayColor: string | null | undefined + name?: string | null + displayColor?: string | null + liquidClass?: string | null }> // TODO: Ian 2018-10-15 audit & rename these confusing types export interface LiquidGroup { - name: string | null | undefined - description: string | null | undefined + name: string | null + description: string | null displayColor: string + liquidClass: string | null serialize: boolean } export type IngredInputs = LiquidGroup & { diff --git a/protocol-designer/src/liquid-defs/utils.ts b/protocol-designer/src/liquid-defs/utils.ts new file mode 100644 index 00000000000..cb9bc9398c9 --- /dev/null +++ b/protocol-designer/src/liquid-defs/utils.ts @@ -0,0 +1,15 @@ +import { getAllLiquidClassDefs } from '@opentrons/shared-data' + +const liquidClassDefs = getAllLiquidClassDefs() +export const getLiquidClassDisplayName = ( + liquidClass: string | null +): string | null => { + if (liquidClass == null) { + return null + } + if (!(liquidClass in liquidClassDefs)) { + console.warn(`Liquid class ${liquidClass} not found`) + return null + } + return liquidClassDefs[liquidClass].displayName +} diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidCard.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidCard.tsx index a422b4b210e..4f33b968db4 100644 --- a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidCard.tsx +++ b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidCard.tsx @@ -20,6 +20,7 @@ import { } from '@opentrons/components' import { LINE_CLAMP_TEXT_STYLE } from '../../atoms' +import { getEnableLiquidClasses } from '../../feature-flags/selectors' import { removeWellsContents } from '../../labware-ingred/actions' import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' import { getLabwareEntities } from '../../step-forms/selectors' @@ -34,7 +35,7 @@ interface LiquidCardProps { export function LiquidCard(props: LiquidCardProps): JSX.Element { const { info } = props - const { name, color, liquidIndex } = info + const { name, color, liquidClassDisplayName, liquidIndex } = info const { t } = useTranslation('liquids') const dispatch = useDispatch() const [isExpanded, setIsExpanded] = useState(false) @@ -49,6 +50,7 @@ export function LiquidCard(props: LiquidCardProps): JSX.Element { const allWellContentsForActiveItem = useSelector( wellContentsSelectors.getAllWellContentsForActiveItem ) + const enableLiquidClasses = useSelector(getEnableLiquidClasses) const wellContents = allWellContentsForActiveItem != null && labwareId != null ? allWellContentsForActiveItem[labwareId] @@ -104,13 +106,24 @@ export function LiquidCard(props: LiquidCardProps): JSX.Element { > - + {name} + {liquidClassDisplayName != null && enableLiquidClasses ? ( + + ) : null} { if (liquidGroupId != null) { @@ -107,13 +117,14 @@ export function DefineLiquidsModal( const initialValues: LiquidEditFormValues = { name: selectedIngredFields?.name ?? '', displayColor: selectedIngredFields?.displayColor ?? swatchColors(liquidId), + liquidClass: selectedIngredFields?.liquidClass ?? '', description: selectedIngredFields?.description ?? '', serialize: selectedIngredFields?.serialize ?? false, } const { handleSubmit, - formState: { errors, touchedFields }, + formState, control, watch, setValue, @@ -125,12 +136,15 @@ export function DefineLiquidsModal( }) const name = watch('name') const color = watch('displayColor') + const liquidClass = watch('liquidClass') + const { errors, touchedFields } = formState const handleLiquidEdits = (values: LiquidEditFormValues): void => { saveForm({ name: values.name, displayColor: values.displayColor, - description: values.description ?? null, + liquidClass: values.liquidClass ? values.liquidClass : null, + description: values.description ? values.description : null, serialize: values.serialize ?? false, }) } @@ -142,6 +156,15 @@ export function DefineLiquidsModal( return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(alpha)}` } + const liquidClassOptions = [ + { name: 'Choose an option', value: '' }, + ...Object.entries(liquidClassDefs).map( + ([liquidClassDefName, { displayName }]) => { + return { name: displayName, value: liquidClassDefName } + } + ), + ] + return ( { @@ -182,6 +205,7 @@ export function DefineLiquidsModal( left="4.375rem" top="4.6875rem" ref={chooseColorWrapperRef} + zIndex={2} > - + + {enableLiquidClasses ? ( + + ( + value === liquidClass + ) ?? liquidClassOptions[0] + } + onClick={value => { + field.onChange(value) + setValue('liquidClass', value) + }} + /> + )} + /> + + ) : null} { ingredientId: mockId, name: 'mockName', displayColor: 'mockDisplayColor', + liquidClass: null, }, ], } diff --git a/protocol-designer/src/pages/Designer/__tests__/LiquidsOverflowMenu.test.tsx b/protocol-designer/src/pages/Designer/__tests__/LiquidsOverflowMenu.test.tsx index 45fbebe5494..6e646fb6db7 100644 --- a/protocol-designer/src/pages/Designer/__tests__/LiquidsOverflowMenu.test.tsx +++ b/protocol-designer/src/pages/Designer/__tests__/LiquidsOverflowMenu.test.tsx @@ -42,6 +42,7 @@ describe('SlotOverflowMenu', () => { displayColor: 'mockColor', name: 'mockname', ingredientId: '0', + liquidClass: null, }, ]) }) diff --git a/protocol-designer/src/pages/ProtocolOverview/LiquidDefinitions.tsx b/protocol-designer/src/pages/ProtocolOverview/LiquidDefinitions.tsx index 378f14e13ad..e9da36c4bae 100644 --- a/protocol-designer/src/pages/ProtocolOverview/LiquidDefinitions.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/LiquidDefinitions.tsx @@ -1,4 +1,5 @@ import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' import { ALIGN_CENTER, DIRECTION_COLUMN, @@ -9,10 +10,42 @@ import { ListItemDescriptor, SPACING, StyledText, + Tag, } from '@opentrons/components' import { LINE_CLAMP_TEXT_STYLE } from '../../atoms' +import { getEnableLiquidClasses } from '../../feature-flags/selectors' +import { getLiquidClassDisplayName } from '../../liquid-defs/utils' -import type { AllIngredGroupFields } from '../../labware-ingred/types' +import type { + AllIngredGroupFields, + IngredInputs, +} from '../../labware-ingred/types' + +const getLiquidDescription = ( + liquid: IngredInputs, + enableLiquidClasses: boolean +): JSX.Element | null => { + const { description, liquidClass } = liquid + const liquidClassDisplayName = getLiquidClassDisplayName(liquidClass) + const liquidClassInfo = + !enableLiquidClasses || liquidClassDisplayName == null ? null : ( + + ) + + return liquidClassInfo == null && !description ? null : ( + + {description ? ( + + {description} + + ) : null} + {liquidClassInfo} + + ) +} interface LiquidDefinitionsProps { allIngredientGroupFields: AllIngredGroupFields @@ -22,6 +55,7 @@ export function LiquidDefinitions({ allIngredientGroupFields, }: LiquidDefinitionsProps): JSX.Element { const { t } = useTranslation('protocol_overview') + const enableLiquidClasses = useSelector(getEnableLiquidClasses) return ( @@ -30,43 +64,46 @@ export function LiquidDefinitions({ {Object.keys(allIngredientGroupFields).length > 0 ? ( - Object.values(allIngredientGroupFields).map((liquid, index) => ( - - - - { + console.log(getLiquidDescription(liquid, enableLiquidClasses)) + return ( + + - {liquid.name} - - - } - content={ - - {liquid.description != null && liquid.description !== '' - ? liquid.description - : t('na')} - - } - /> - - )) + + + {liquid.name} + + + } + content={ + getLiquidDescription(liquid, enableLiquidClasses) ?? ( + + {t('na')} + + ) + } + /> + + ) + }) ) : ( )} diff --git a/protocol-designer/src/pages/ProtocolOverview/__tests__/LiquidDefinitions.test.tsx b/protocol-designer/src/pages/ProtocolOverview/__tests__/LiquidDefinitions.test.tsx index 832cea4d800..ff4af37fa4a 100644 --- a/protocol-designer/src/pages/ProtocolOverview/__tests__/LiquidDefinitions.test.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/__tests__/LiquidDefinitions.test.tsx @@ -8,6 +8,8 @@ import { LiquidDefinitions } from '../LiquidDefinitions' import type { ComponentProps } from 'react' import type { InfoScreen } from '@opentrons/components' +vi.mock('../../../feature-flags/selectors') + vi.mock('@opentrons/components', async importOriginal => { const actual = await importOriginal() return { @@ -21,6 +23,7 @@ const mockAllIngredientGroupFields = { name: 'EtOH', displayColor: '#b925ff', description: 'Immer fisch Hergestllter EtOH', + liquidClass: null, serialize: false, liquidGroupId: '0', }, @@ -28,6 +31,7 @@ const mockAllIngredientGroupFields = { name: '10mM Tris pH8,5', displayColor: '#ffd600', description: null, + liquidClass: null, serialize: false, liquidGroupId: '1', }, @@ -35,6 +39,7 @@ const mockAllIngredientGroupFields = { name: 'Amplicon PCR sample + AMPure XP beads', displayColor: '#9dffd8', description: '25µl Amplicon PCR + 20 µl AMPure XP beads', + liquidClass: 'Water', serialize: false, liquidGroupId: '2', }, diff --git a/shared-data/js/index.ts b/shared-data/js/index.ts index 9a8d2e6c39f..50be81a940e 100644 --- a/shared-data/js/index.ts +++ b/shared-data/js/index.ts @@ -10,6 +10,7 @@ export * from './gripper' export * from './helpers' export * from './labware' export * from './labwareTools' +export * from './liquidClasses' export * from './modules' export * from './pipettes' export * from './protocols' diff --git a/shared-data/js/liquidClasses.ts b/shared-data/js/liquidClasses.ts new file mode 100644 index 00000000000..d97db8afa47 --- /dev/null +++ b/shared-data/js/liquidClasses.ts @@ -0,0 +1,8 @@ +import waterV1Uncasted from '../liquid-class/definitions/1/water.json' +import type { LiquidClass } from '.' + +const waterV1 = waterV1Uncasted as LiquidClass + +const defs = { waterV1 } + +export const getAllLiquidClassDefs = (): Record => defs diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 71c932a35d4..6abe511bb8f 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -693,6 +693,106 @@ export interface Liquid { displayColor?: string } +// TODO(ND, 12/17/2024): investigate why typescript doesn't allow Array<[number, number]> +type LiquidHandlingPropertyByVolume = number[][] +type PositionReference = + | 'well-bottom' + | 'well-top' + | 'well-center' + | 'liquid-meniscus' +type BlowoutLocation = 'source' | 'destination' | 'trash' +interface DelayParams { + duration: number +} +interface DelayProperties { + enable: boolean + params?: DelayParams +} +interface TouchTipParams { + zOffset: number + mmToEdge: number + speed: number +} +interface TouchTipProperties { + enable: boolean + params?: TouchTipParams +} + +interface MixParams { + repetitions: number + volume: number +} +interface MixProperties { + enable: boolean + params?: MixParams +} +interface BlowoutParams { + location: BlowoutLocation + flowRate: number +} +interface BlowoutProperties { + enable: boolean + params?: BlowoutParams +} +interface Submerge { + positionReference: PositionReference + offset: Coordinates + speed: number + delay: DelayProperties +} +interface BaseRetract { + positionReference: PositionReference + offset: Coordinates + speed: number + airGapByVolume: LiquidHandlingPropertyByVolume + touchTip: TouchTipProperties + delay: DelayProperties +} +type RetractAspirate = BaseRetract +interface RetractDispense extends BaseRetract { + blowout: BlowoutProperties +} +interface BaseLiquidHandlingProperties { + submerge: Submerge + retract: RetractType + positionReference: PositionReference + offset: Coordinates + flowRateByVolume: LiquidHandlingPropertyByVolume + correctionByVolume: LiquidHandlingPropertyByVolume + delay: DelayProperties +} +interface AspirateProperties + extends BaseLiquidHandlingProperties { + preWet: boolean + mix: MixProperties +} +interface SingleDispenseProperties + extends BaseLiquidHandlingProperties { + mix: MixProperties + pushOutByVolume: LiquidHandlingPropertyByVolume +} +interface MultiDispenseProperties { + conditioningByVolume: LiquidHandlingPropertyByVolume + disposalByVolume: LiquidHandlingPropertyByVolume +} +interface ByTipTypeSetting { + tiprack: string + aspirate: AspirateProperties + singleDispense: SingleDispenseProperties + multiDispense?: MultiDispenseProperties +} +interface ByPipetteSetting { + pipetteModel: string + byTipType: ByTipTypeSetting[] +} +export interface LiquidClass { + liquidClassName: string + displayName: string + schemaVersion: number + namespace: string + byPipette: ByPipetteSetting[] +} + export interface AnalysisError { id: string detail: string From 20841218483ba7b9ec590a340c27a72bc8149b6e Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Wed, 18 Dec 2024 09:41:14 -0500 Subject: [PATCH 20/25] fix(app): fix back to back manual move commands on desktop app (#17129) Closes RQA-3728 Since the introduction of the move labware intervention deck map, chaining back to back move interventions has posed a challenge, see comments in #13005. The setTimeout solution proposed in the linked PR effectively worked, and this code looks like it got dropped at some point. However, there are other ways of doing the same thing that are more React Spring-esque, using the API's reset property. --- .../MoveLabwareInterventionContent.tsx | 1 - .../hardware-sim/Deck/MoveLabwareOnDeck.tsx | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx index af561b6c15d..556c051d32d 100644 --- a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx +++ b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx @@ -191,7 +191,6 @@ export function MoveLabwareInterventionContent({ { + if (shouldReset) { + setShouldReset(false) + return + } + + const isNewPosition = + previousInitialRef.current?.x !== initialPosition.x || + previousInitialRef.current?.y !== initialPosition.y || + previousFinalRef.current?.x !== finalPosition.x || + previousFinalRef.current?.y !== finalPosition.y + + if (isNewPosition) { + setShouldReset(true) + } + + previousInitialRef.current = initialPosition + previousFinalRef.current = finalPosition + }, [initialPosition, finalPosition]) + + const previousInitialRef = React.useRef(initialPosition) + const previousFinalRef = React.useRef(finalPosition) + + return shouldReset +} /** * These animated components needs to be split out because react-spring and styled-components don't play nice * @see https://github.com/pmndrs/react-spring/issues/1515 */ From c24cdfb3d1fb65756cc681435f51f97eec65e310 Mon Sep 17 00:00:00 2001 From: Anthony Ngumah <68346382+AnthonyNASC20@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:59:46 -0500 Subject: [PATCH 21/25] feat(abr-testing): pull protocol from failing robot (#17125) Added feature to pull python protocol file from specifed robot with run error, as well as additional error handling # Overview Pulls python protocol file from robot with error and attaches to JIRA ticket. ## Test Plan and Hands on Testing Tested manually --- .../data_collection/abr_robot_error.py | 73 ++++++++++++++++--- 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index 73cf12c6253..75e6bcde34e 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -11,11 +11,35 @@ import sys import json import re +from pathlib import Path import pandas as pd from statistics import mean, StatisticsError from abr_testing.tools import plate_reader +def retrieve_protocol_file( + protocol_id: str, + robot_ip: str, + storage: str, +) -> Path | str: + """Find and copy protocol file on robot with error.""" + protocol_dir = f"/var/lib/opentrons-robot-server/7.1/protocols/{protocol_id}" + + print(f"FILE TO FIND: {protocol_dir}/{protocol_id}") + # Copy protocol file found in robot oto host computer + save_dir = Path(f"{storage}/protocol_errors") + command = ["scp", "-r", f"root@{robot_ip}:{protocol_dir}", save_dir] + try: + # If file found and copied return path to file + subprocess.run(command, check=True) # type: ignore + print("File transfer successful!") + return save_dir + except subprocess.CalledProcessError as e: + print(f"Error during file transfer: {e}") + # Return empty string if file can't be copied + return "" + + def compare_current_trh_to_average( robot: str, start_time: Any, @@ -38,9 +62,13 @@ def compare_current_trh_to_average( # Find average conditions of errored time period df_all_trh = pd.DataFrame(all_trh_data) # Convert timestamps to datetime objects - df_all_trh["Timestamp"] = pd.to_datetime( - df_all_trh["Timestamp"], format="mixed", utc=True - ).dt.tz_localize(None) + print(f'TIMESTAMP: {df_all_trh["Timestamp"]}') + try: + df_all_trh["Timestamp"] = pd.to_datetime( + df_all_trh["Timestamp"], format="mixed", utc=True + ).dt.tz_localize(None) + except Exception: + print(f'The following timestamp is invalid: {df_all_trh["Timestamp"]}') # Ensure start_time is timezone-naive start_time = start_time.replace(tzinfo=None) relevant_temp_rhs = df_all_trh[ @@ -245,9 +273,10 @@ def get_user_id(user_file_path: str, assignee_name: str) -> str: return assignee_id -def get_error_runs_from_robot(ip: str) -> List[str]: +def get_error_runs_from_robot(ip: str) -> Tuple[List[str], List[str]]: """Get runs that have errors from robot.""" error_run_ids = [] + protocol_ids = [] response = requests.get( f"http://{ip}:31950/runs", headers={"opentrons-version": "3"} ) @@ -255,10 +284,13 @@ def get_error_runs_from_robot(ip: str) -> List[str]: run_list = run_data.get("data", []) for run in run_list: run_id = run["id"] + protocol_id = run["protocolId"] num_of_errors = len(run["errors"]) if not run["current"] and num_of_errors > 0: error_run_ids.append(run_id) - return error_run_ids + # Protocol ID will identify the correct folder on the robot of the protocol file + protocol_ids.append(protocol_id) + return (error_run_ids, protocol_ids) def get_robot_state( @@ -335,7 +367,7 @@ def get_robot_state( def get_run_error_info_from_robot( - ip: str, one_run: str, storage_directory: str + ip: str, one_run: str, storage_directory: str, protocol_found: bool ) -> Tuple[str, str, str, List[str], List[str], str, str]: """Get error information from robot to fill out ticket.""" description = dict() @@ -369,6 +401,9 @@ def get_run_error_info_from_robot( description["protocol_name"] = results["protocol"]["metadata"].get( "protocolName", "" ) + + # If Protocol was successfully retrieved from the robot + description["protocol_found_on_robot"] = protocol_found # Get start and end time of run start_time = datetime.strptime( results.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" @@ -511,12 +546,21 @@ def get_run_error_info_from_robot( users_file_path = ticket.get_jira_users(storage_directory) assignee_id = get_user_id(users_file_path, assignee) run_log_file_path = "" + protocol_found = False try: - error_runs = get_error_runs_from_robot(ip) + error_runs, protocol_ids = get_error_runs_from_robot(ip) except requests.exceptions.InvalidURL: print("Invalid IP address.") sys.exit() if len(run_or_other) < 1: + # Retrieve the most recently run protocol file + protocol_files_path = retrieve_protocol_file( + protocol_ids[-1], ip, storage_directory + ) + # Set protocol_found to true if python protocol was successfully copied over + if protocol_files_path: + protocol_found = True + one_run = error_runs[-1] # Most recent run with error. ( summary, @@ -526,7 +570,9 @@ def get_run_error_info_from_robot( labels, whole_description_str, run_log_file_path, - ) = get_run_error_info_from_robot(ip, one_run, storage_directory) + ) = get_run_error_info_from_robot( + ip, one_run, storage_directory, protocol_found + ) else: ( summary, @@ -566,8 +612,15 @@ def get_run_error_info_from_robot( # OPEN TICKET issue_url = ticket.open_issue(issue_key) # MOVE FILES TO ERROR FOLDER. + print(protocol_files_path) error_files = [saved_file_path_calibration, run_log_file_path] + file_paths - error_folder_path = os.path.join(storage_directory, issue_key) + + # Move protocol file(s) to error folder + if protocol_files_path: + for file in os.listdir(protocol_files_path): + error_files.append(os.path.join(protocol_files_path, file)) + + error_folder_path = os.path.join(storage_directory, "issue_key") os.makedirs(error_folder_path, exist_ok=True) for source_file in error_files: try: @@ -577,7 +630,7 @@ def get_run_error_info_from_robot( shutil.move(source_file, destination_file) except shutil.Error: continue - # POST FILES TO TICKET + # POST ALL FILES TO TICKET list_of_files = os.listdir(error_folder_path) for file in list_of_files: file_to_attach = os.path.join(error_folder_path, file) From bc17aa4666eabc1736126c89bee1af161d5deae6 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:16:00 -0500 Subject: [PATCH 22/25] fix(protocol-designer): highlight used wells when selected on step (#17136) closes AUTH-1190 --- .../ProtocolSteps/Timeline/TerminalItemStep.tsx | 2 +- .../src/top-selectors/substep-highlight.ts | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/TerminalItemStep.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/TerminalItemStep.tsx index 39fd70a9f78..ffb13d31a7d 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/TerminalItemStep.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/TerminalItemStep.tsx @@ -1,4 +1,5 @@ import { useSelector, useDispatch } from 'react-redux' +import { useTranslation } from 'react-i18next' import { useConditionalConfirm } from '@opentrons/components' import { getHoveredTerminalItemId, @@ -28,7 +29,6 @@ import type { } from '../../../../ui/steps' import type { TerminalItemId } from '../../../../steplist' import type { ThunkDispatch } from '../../../../types' -import { useTranslation } from 'react-i18next' export interface TerminalItemStepProps { id: TerminalItemId diff --git a/protocol-designer/src/top-selectors/substep-highlight.ts b/protocol-designer/src/top-selectors/substep-highlight.ts index 049bd59e7df..8724f86a909 100644 --- a/protocol-designer/src/top-selectors/substep-highlight.ts +++ b/protocol-designer/src/top-selectors/substep-highlight.ts @@ -4,7 +4,11 @@ import { ALL, COLUMN, getWellNamePerMultiTip } from '@opentrons/shared-data' import * as StepGeneration from '@opentrons/step-generation' import { selectors as stepFormSelectors } from '../step-forms' import { selectors as fileDataSelectors } from '../file-data' -import { getHoveredStepId, getHoveredSubstep } from '../ui/steps' +import { + getHoveredStepId, + getHoveredSubstep, + getSelectedStepId, +} from '../ui/steps' import { getWellSetForMultichannel } from '../utils' import type { WellGroup } from '@opentrons/components' import type { @@ -275,6 +279,7 @@ export const wellHighlightsByLabwareId: Selector< getHoveredSubstep, fileDataSelectors.getSubsteps, stepFormSelectors.getOrderedStepIds, + getSelectedStepId, ( robotStateTimeline, invariantContext, @@ -282,10 +287,11 @@ export const wellHighlightsByLabwareId: Selector< hoveredStepId, hoveredSubstep, substepsById, - orderedStepIds + orderedStepIds, + selectedStepId ) => { const timeline = robotStateTimeline.timeline - const stepId = hoveredStepId + const stepId = hoveredStepId || selectedStepId const timelineIndex = orderedStepIds.findIndex(i => i === stepId) const frame = timeline[timelineIndex] const robotState = frame && frame.robotState From 82aef48e50a9c57f9127d7f5794755dc5a31374a Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:31:56 -0500 Subject: [PATCH 23/25] feat (abr-testing): Lengthen ABR Protocols, Add Protocol Versioning, Add Liquid Waste Probing and Recording (#17137) # Overview ABR Protocol Improvements: Lengthen protocols, Add Version numbers, Add Liquid Waste Probing ## Test Plan and Hands on Testing Protocols have been running for ~ 2-5 days depending on the robot to ensure functionality ## Changelog - The protocol version number is commented at the beginning of the protocol and recorded in the ABR google sheet - All protocols now have a clean up step to move all liquids into liquid waste - At the end of the protocol the liquid waste liquid height is measured in the ABR google sheet ## Review requests ## Risk assessment --------- Co-authored-by: Tony Ngumah --- .../data_collection/abr_google_drive.py | 31 +- .../data_collection/abr_robot_error.py | 14 +- .../data_collection/read_robot_logs.py | 70 +- .../data_collection/single_run_log_reader.py | 5 +- .../10_ZymoBIOMICS_Magbead_DNA_Cells_Flex.py | 222 ++-- .../11_Dynabeads_IP_Flex_96well_RIT.py | 212 ++-- .../12_KAPA HyperPlus Library Prep.py | 300 ++--- .../1_Simple Normalize Long Right.py | 288 +++-- .../active_protocols/2_BMS_PCR_Protocol.py | 108 +- .../active_protocols/3_Tartrazine Protocol.py | 206 +++- .../4_Illumina DNA Enrichment.py | 1016 +++++++++++++++++ ...omplex protocol with single tip Pick Up.py | 188 +-- .../6_Omega_HDQ_DNA_Cells-Flex_96_channel.py | 301 +++-- .../7_HDQ_DNA_Bacteria_Flex.py | 115 +- .../8_Illumina and Plate Reader.py | 158 ++- .../9_Magmax_RNA_Cells_Flex.py | 257 +++-- .../protocols/csv_parameters/1_samplevols.csv | 97 ++ .../protocols/csv_parameters/2_samplevols.csv | 96 +- abr-testing/abr_testing/protocols/helpers.py | 180 ++- .../10_ZymoBIOMICS Magbead Liquid Setup.py | 31 +- .../11_Dynabeads RIT Liquid Setup.py | 25 +- ...APA HyperPlus Library Prep Liquid Setup.py | 2 +- .../1_Simple normalize long Liquid Setup.py | 14 +- .../2_BMS_PCR_protocol Liquid Setup.py | 10 +- .../3_Tartrazine Liquid Setup.py | 91 +- .../4_Illumina DNA Enrichment Liquid Setup.py | 25 +- .../5_96ch Complex Protocol Liquid Setup.py | 10 +- ...DQ DNA Bacteria Extraction Liquid Setup.py | 34 +- ...ermo MagMax RNA Extraction Liquid Setup.py | 49 +- .../test_protocols/tc_lid_x_offset_test.py | 14 +- abr-testing/abr_testing/tools/abr_scale.py | 14 +- abr-testing/abr_testing/tools/abr_setup.py | 8 +- 32 files changed, 2993 insertions(+), 1198 deletions(-) create mode 100644 abr-testing/abr_testing/protocols/active_protocols/4_Illumina DNA Enrichment.py create mode 100644 abr-testing/abr_testing/protocols/csv_parameters/1_samplevols.csv diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index 8f82567a7d1..655200745a9 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -34,16 +34,13 @@ def create_data_dictionary( runs_to_save: Union[Set[str], str], storage_directory: str, issue_url: str, - plate: str, - accuracy: Any, hellma_plate_standards: List[Dict[str, Any]], -) -> Tuple[List[List[Any]], List[str], List[List[Any]], List[str], List[List[Any]]]: +) -> Tuple[List[List[Any]], List[str], List[List[Any]], List[str]]: """Pull data from run files and format into a dictionary.""" runs_and_robots: List[Any] = [] runs_and_lpc: List[Dict[str, Any]] = [] headers: List[str] = [] headers_lpc: List[str] = [] - list_of_heights: List[List[Any]] = [[], [], [], [], [], [], [], []] hellma_plate_orientation = False # default hellma plate is not rotated. for filename in os.listdir(storage_directory): file_path = os.path.join(storage_directory, filename) @@ -103,12 +100,15 @@ def create_data_dictionary( run_time_min = run_time.total_seconds() / 60 except ValueError: pass # Handle datetime parsing errors if necessary + # Get protocol version # + version_number = read_robot_logs.get_protocol_version_number(file_results) if run_time_min > 0: run_row = { "Robot": robot, "Run_ID": run_id, "Protocol_Name": protocol_name, + "Protocol Version": version_number, "Software Version": software_version, "Date": start_date, "Start_Time": start_time_str, @@ -130,13 +130,10 @@ def create_data_dictionary( plate_reader_dict = read_robot_logs.plate_reader_commands( file_results, hellma_plate_standards, hellma_plate_orientation ) - list_of_heights = read_robot_logs.liquid_height_commands( - file_results, list_of_heights - ) notes = {"Note1": "", "Jira Link": issue_url} + liquid_height = read_robot_logs.get_liquid_waste_height(file_results) plate_measure = { - "Plate Measured": plate, - "End Volume Accuracy (%)": accuracy, + "Liquid Waste Height (mm)": liquid_height, "Average Temp (oC)": "", "Average RH(%)": "", } @@ -173,7 +170,6 @@ def create_data_dictionary( headers, transposed_runs_and_lpc, headers_lpc, - list_of_heights, ) @@ -211,26 +207,15 @@ def run( headers, transposed_runs_and_lpc, headers_lpc, - list_of_heights, ) = create_data_dictionary( missing_runs_from_gs, storage_directory, "", - "", - "", - hellma_plate_standards=file_values, + file_values, ) start_row = google_sheet.get_index_row() + 1 google_sheet.batch_update_cells(transposed_runs_and_robots, "A", start_row, "0") - # Record Liquid Heights Found - google_sheet_ldf = google_sheets_tool.google_sheet( - credentials_path, google_sheet_name, 2 - ) - google_sheet_ldf.get_row(1) - start_row_lhd = google_sheet_ldf.get_index_row() + 1 - google_sheet_ldf.batch_update_cells( - list_of_heights, "A", start_row_lhd, "2075262446" - ) + # Add LPC to google sheet google_sheet_lpc = google_sheets_tool.google_sheet(credentials_path, "ABR-LPC", 0) start_row_lpc = google_sheet_lpc.get_index_row() + 1 diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index 75e6bcde34e..c2dadaae54c 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -667,28 +667,16 @@ def get_run_error_info_from_robot( headers, runs_and_lpc, headers_lpc, - list_of_heights, ) = abr_google_drive.create_data_dictionary( run_id, error_folder_path, issue_url, - "", - "", - hellma_plate_standards=file_values, + file_values, ) start_row = google_sheet.get_index_row() + 1 google_sheet.batch_update_cells(runs_and_robots, "A", start_row, "0") print("Wrote run to ABR-run-data") - # Record Liquid Heights Found - google_sheet_ldf = google_sheets_tool.google_sheet( - credentials_path, google_sheet_name, 4 - ) - start_row_lhd = google_sheet_ldf.get_index_row() + 1 - google_sheet_ldf.batch_update_cells( - list_of_heights, "A", start_row_lhd, "1795535088" - ) - print("wrote liquid heights found.") # Add LPC to google sheet google_sheet_lpc = google_sheets_tool.google_sheet( credentials_path, "ABR-LPC", 0 diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index 7bc83e0a54b..0a098835cf7 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -134,7 +134,8 @@ def match_pipette_to_action( left_pipette_add = 0 for command in commandTypes: command_type = command_dict["commandType"] - command_pipette = command_dict.get("pipetteId", "") + command_params = command_dict.get("params", "") + command_pipette = command_params.get("pipetteId", "") if command_type == command and command_pipette == right_pipette: right_pipette_add = 1 elif command_type == command and command_pipette == left_pipette: @@ -213,6 +214,35 @@ def instrument_commands( return pipette_dict +def get_comment_result_by_string(file_results: Dict[str, Any], key_phrase: str) -> str: + """Get comment string based off ky phrase.""" + commandData = file_results.get("commands", "") + result_str = command_str = "" + for command in commandData: + commandType = command["commandType"] + if commandType == "comment": + command_str = command["params"].get("message", "") + try: + result_str = command_str.split(key_phrase)[1] + except IndexError: + continue + return result_str + + +def get_protocol_version_number(file_results: Dict[str, Any]) -> str: + """Get protocol version number.""" + return get_comment_result_by_string(file_results, "Protocol Version: ") + + +def get_liquid_waste_height(file_results: Dict[str, Any]) -> float: + """Find liquid waste height.""" + result_str = get_comment_result_by_string( + file_results, "Liquid Waste Total Height: " + ) + height = float(result_str) + return height + + def liquid_height_commands( file_results: Dict[str, Any], all_heights_list: List[List[Any]] ) -> List[List[Any]]: @@ -220,6 +250,9 @@ def liquid_height_commands( commandData = file_results.get("commands", "") robot = file_results.get("robot_name", "") run_id = file_results.get("run_id", "") + list_of_heights = [] + print(robot) + liquid_waste_height = 0.0 for command in commandData: commandType = command["commandType"] if commandType == "comment": @@ -236,16 +269,24 @@ def liquid_height_commands( well_location = str(entry.split(", ")[1].split(" ")[0]) slot_location = str(entry.split("slot ")[1].split(")")[0]) labware_name = str(entry.split("of ")[1].split(" on")[0]) - all_heights_list[0].append(robot) - all_heights_list[1].append(run_id) - all_heights_list[2].append(comment_time) - all_heights_list[3].append(labware_type) - all_heights_list[4].append(labware_name) - all_heights_list[5].append(slot_location) - all_heights_list[6].append(well_location) - all_heights_list[7].append(height) + if labware_name == "Liquid Waste": + liquid_waste_height += height + one_entry = { + "Timestamp": comment_time, + "Labware Name": labware_name, + "Labware Type": labware_type, + "Slot Location": slot_location, + "Well Location": well_location, + "All Heights (mm)": height, + } + list_of_heights.append(one_entry) except (IndexError, ValueError): continue + if len(list_of_heights) > 0: + all_heights_list[0].append(robot) + all_heights_list[1].append(run_id) + all_heights_list[2].append(list_of_heights) + all_heights_list[3].append(liquid_waste_height) return all_heights_list @@ -281,10 +322,13 @@ def plate_reader_commands( read = "yes" elif read == "yes" and commandType == "comment": result = command["params"].get("message", "") - if "result:" in result: - plate_name = result.split("result:")[0] - formatted_result = result.split("result: ")[1] - print(formatted_result) + if "result:" in result or "Result:" in result: + try: + plate_name = result.split("result:")[0] + formatted_result = result.split("result: ")[1] + except IndexError: + plate_name = result.split("Result:")[0] + formatted_result = result.split("Result: ")[1] result_dict = eval(formatted_result) result_dict_keys = list(result_dict.keys()) if len(result_dict_keys) > 1: diff --git a/abr-testing/abr_testing/data_collection/single_run_log_reader.py b/abr-testing/abr_testing/data_collection/single_run_log_reader.py index a61670e6d12..bf10347faff 100644 --- a/abr-testing/abr_testing/data_collection/single_run_log_reader.py +++ b/abr-testing/abr_testing/data_collection/single_run_log_reader.py @@ -34,14 +34,11 @@ header, runs_and_lpc, lpc_headers, - list_of_heights, ) = abr_google_drive.create_data_dictionary( run_ids_in_storage, run_log_file_path, "", - "", - "", - hellma_plate_standards=file_values, + file_values, ) print("list_of_heights not recorded.") transposed_list = list(zip(*runs_and_robots)) diff --git a/abr-testing/abr_testing/protocols/active_protocols/10_ZymoBIOMICS_Magbead_DNA_Cells_Flex.py b/abr-testing/abr_testing/protocols/active_protocols/10_ZymoBIOMICS_Magbead_DNA_Cells_Flex.py index 9631b442694..9e93e597bbd 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/10_ZymoBIOMICS_Magbead_DNA_Cells_Flex.py +++ b/abr-testing/abr_testing/protocols/active_protocols/10_ZymoBIOMICS_Magbead_DNA_Cells_Flex.py @@ -17,7 +17,7 @@ "protocolName": "Flex ZymoBIOMICS Magbead DNA Extraction: Cells", } -requirements = {"robotType": "OT-3", "apiLevel": "2.21"} +requirements = {"robotType": "Flex", "apiLevel": "2.21"} """ Slot A1: Tips 1000 Slot A2: Tips 1000 @@ -41,11 +41,12 @@ Wells 1-12 - 9,000 ul """ -whichwash = 1 +whichwash = 0 +wash_volume_tracker = 0.0 sample_max = 48 tip1k = 0 -tip200 = 0 drop_count = 0 +m1000_tips = 0 def add_parameters(parameters: protocol_api.ParameterContext) -> None: @@ -53,28 +54,40 @@ def add_parameters(parameters: protocol_api.ParameterContext) -> None: helpers.create_hs_speed_parameter(parameters) helpers.create_single_pipette_mount_parameter(parameters) helpers.create_dot_bottom_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) -def run(ctx: protocol_api.ProtocolContext) -> None: +def run(protocol: protocol_api.ProtocolContext) -> None: """Protocol Set Up.""" - heater_shaker_speed = ctx.params.heater_shaker_speed # type: ignore[attr-defined] - mount = ctx.params.pipette_mount # type: ignore[attr-defined] - dot_bottom = ctx.params.dot_bottom # type: ignore[attr-defined] + heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] + mount = protocol.params.pipette_mount # type: ignore[attr-defined] + dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") + dry_run = False TIP_TRASH = ( False # True = Used tips go in Trash, False = Used tips go back into rack ) res_type = "nest_12_reservoir_15ml" - - num_samples = 8 - wash1_vol = 500.0 - wash2_vol = wash3_vol = 900.0 - lysis_vol = 200.0 + global m1000_tips + num_samples = 96 + wash1_vol = wash2_vol = wash3_vol = 400.0 + lysis_vol = 90.0 sample_vol = 10.0 # Sample should be pelleted tissue/bacteria/cells bind_vol = 600.0 bind2_vol = 500.0 elution_vol = 75.0 + def tipcheck(m1000: InstrumentContext) -> None: + """Tip tracking function.""" + global m1000_tips + if m1000_tips >= 3 * 96: + m1000.reset_tipracks() + m1000_tips == 0 + m1000.pick_up_tip() + m1000_tips += 8 + # Protocol Parameters deepwell_type = "nest_96_wellplate_2ml_deep" @@ -97,37 +110,41 @@ def run(ctx: protocol_api.ProtocolContext) -> None: bead_vol = 25.0 starting_vol = lysis_vol + sample_vol binding_buffer_vol = bind_vol + bead_vol - ctx.load_trash_bin("A3") - h_s: HeaterShakerContext = ctx.load_module(helpers.hs_str, "D1") # type: ignore[assignment] + protocol.load_trash_bin("A3") + h_s: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "D1" + ) # type: ignore[assignment] labware_name = "Samples" sample_plate, h_s_adapter = helpers.load_hs_adapter_and_labware( deepwell_type, h_s, labware_name ) h_s.close_labware_latch() - temp: TemperatureModuleContext = ctx.load_module( + temp: TemperatureModuleContext = protocol.load_module( helpers.temp_str, "D3" ) # type: ignore[assignment] elutionplate, temp_adapter = helpers.load_temp_adapter_and_labware( "armadillo_96_wellplate_200ul_pcr_full_skirt", temp, "Elution Plate" ) - magblock: MagneticBlockContext = ctx.load_module( + magblock: MagneticBlockContext = protocol.load_module( helpers.mag_str, "C1" ) # type: ignore[assignment] - waste_reservoir = ctx.load_labware("nest_1_reservoir_195ml", "B3", "Liquid Waste") + waste_reservoir = protocol.load_labware( + "nest_1_reservoir_290ml", "B3", "Liquid Waste" + ) waste = waste_reservoir.wells()[0].top() - res1 = ctx.load_labware(res_type, "D2", "reagent reservoir 1") - res2 = ctx.load_labware(res_type, "C2", "reagent reservoir 2") + res1 = protocol.load_labware(res_type, "D2", "reagent reservoir 1") + res2 = protocol.load_labware(res_type, "C2", "reagent reservoir 2") + res3 = protocol.load_labware(res_type, "B2", "reagent reservoir 3") num_cols = math.ceil(num_samples / 8) # Load tips and combine all similar boxes - tips1000 = ctx.load_labware("opentrons_flex_96_tiprack_1000ul", "A1", "Tips 1") - tips1001 = ctx.load_labware("opentrons_flex_96_tiprack_1000ul", "A2", "Tips 2") - tips1002 = ctx.load_labware("opentrons_flex_96_tiprack_1000ul", "B1", "Tips 3") - tips = [*tips1000.wells()[num_samples:96], *tips1001.wells(), *tips1002.wells()] + tips1000 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "A1", "Tips 1") + tips1001 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "A2", "Tips 2") + tips1002 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "B1", "Tips 3") tips_sn = tips1000.wells()[:num_samples] # load instruments - m1000 = ctx.load_instrument( + m1000 = protocol.load_instrument( "flex_8channel_1000", mount, tip_racks=[tips1000, tips1001, tips1002] ) @@ -135,50 +152,32 @@ def run(ctx: protocol_api.ProtocolContext) -> None: Here is where you can define the locations of your reagents. """ lysis_ = res1.wells()[0] - binding_buffer = res1.wells()[1:4] - bind2_res = res1.wells()[4:8] - wash1 = res1.wells()[6:8] - elution_solution = res1.wells()[-1] - wash2 = res2.wells()[:6] - wash3 = res2.wells()[6:] - + binding_buffer = res1.wells()[1:8] + bind2_res = res1.wells()[8:12] + all_washes = res2.wells()[1:] + elution_solution = res2.wells()[0] + all_washes.extend(res3.wells()[:2]) samples_m = sample_plate.rows()[0][:num_cols] elution_samples_m = elutionplate.rows()[0][:num_cols] # Redefine per well for liquid definitions samps = sample_plate.wells()[: (8 * num_cols)] - liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { "Lysis and PK": [{"well": lysis_, "volume": 12320.0}], "Beads and Binding": [{"well": binding_buffer, "volume": 11875.0}], "Binding 2": [{"well": bind2_res, "volume": 13500.0}], - "Final Elution": [{"well": elution_solution, "volume": 52000}], + "Final Elution": [{"well": elution_solution, "volume": 1200.0}], "Samples": [{"well": samps, "volume": 0.0}], - "Reagents": [{"well": res2.wells(), "volume": 9000.0}], + "Reagents": [{"well": all_washes, "volume": 9800.0}], } - flattened_list_of_wells = helpers.find_liquid_height_of_loaded_liquids( - ctx, liquid_vols_and_wells, m1000 - ) + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, m1000) m1000.flow_rate.aspirate = 300 m1000.flow_rate.dispense = 300 m1000.flow_rate.blow_out = 300 - def tiptrack(tipbox: List[Well]) -> None: - """Track Tips.""" - global tip1k - global drop_count - if tipbox == tips: - m1000.pick_up_tip(tipbox[int(tip1k)]) - tip1k = tip1k + 8 - - drop_count = drop_count + 8 - if drop_count >= 150: - drop_count = 0 - ctx.pause("Please empty the waste bin of all the tips before continuing.") - def remove_supernatant(vol: float) -> None: """Remove supernatant.""" - ctx.comment("-----Removing Supernatant-----") + protocol.comment("-----Removing Supernatant-----") m1000.flow_rate.aspirate = 30 num_trans = math.ceil(vol / 980) vol_per_trans = vol / num_trans @@ -198,7 +197,7 @@ def remove_supernatant(vol: float) -> None: m1000.flow_rate.aspirate = 300 # Transfer from Magdeck plate to H-S - helpers.move_labware_to_hs(ctx, sample_plate, h_s, h_s_adapter) + helpers.move_labware_to_hs(protocol, sample_plate, h_s, h_s_adapter) def bead_mixing( well: Well, pip: InstrumentContext, mvol: float, reps: int = 8 @@ -261,6 +260,7 @@ def mixing(well: Well, pip: InstrumentContext, mvol: float, reps: int = 8) -> No dispensing at the top and 2 cycles of aspirating from middle, dispensing at the bottom """ + pip.liquid_presence_detection = False center = well.top(5) asp = well.bottom(1) disp = well.top(-8) @@ -290,15 +290,18 @@ def mixing(well: Well, pip: InstrumentContext, mvol: float, reps: int = 8) -> No def lysis(vol: float, source: Well) -> None: """Lysis.""" - ctx.comment("-----Beginning Lysis Steps-----") - ctx.pause(msg="\n Hello \n - step 1 \n - step 2") + protocol.comment("-----Beginning Lysis Steps-----") num_transfers = math.ceil(vol / 980) - tiptrack(tips) + tipcheck(m1000) + total_lysis_aspirated = 0.0 for i in range(num_cols): src = source tvol = vol / num_transfers # Mix Shield and PK before transferring first time if i == 0: + m1000.liquid_presence_detection = ( + False # turn off liquid detection during mixing + ) for x in range(lysis_rep_1): m1000.aspirate(vol, src.bottom(1)) m1000.dispense(vol, src.bottom(8)) @@ -308,15 +311,14 @@ def lysis(vol: float, source: Well) -> None: m1000.aspirate(tvol, src.bottom(1)) m1000.air_gap(10) m1000.dispense(m1000.current_volume, samples_m[i].top()) - + total_lysis_aspirated += tvol * 8 # Mix shield and pk with samples for i in range(num_cols): if i != 0: - tiptrack(tips) + tipcheck(m1000) mixing(samples_m[i], m1000, tvol, reps=lysis_rep_2) m1000.drop_tip() if TIP_TRASH else m1000.return_tip() - - helpers.set_hs_speed(ctx, h_s, heater_shaker_speed, lysis_incubation, True) + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, lysis_incubation, True) def bind(vol1: float, vol2: float) -> None: """Binding. @@ -334,9 +336,9 @@ def bind(vol1: float, vol2: float) -> None: supernatant to the final clean elutions PCR plate. """ - ctx.comment("-----Beginning Binding Steps-----") + protocol.comment("-----Beginning Binding Steps-----") for i, well in enumerate(samples_m): - tiptrack(tips) + tipcheck(m1000) num_trans = math.ceil(vol1 / 980) vol_per_trans = vol1 / num_trans source = binding_buffer[i // 2] @@ -360,15 +362,19 @@ def bind(vol1: float, vol2: float) -> None: m1000.air_gap(10) m1000.drop_tip() if TIP_TRASH else m1000.return_tip() - helpers.set_hs_speed(ctx, h_s, heater_shaker_speed * 0.9, bind_time_1, True) + helpers.set_hs_speed( + protocol, h_s, heater_shaker_speed * 0.9, bind_time_1, True + ) # Transfer from H-S plate to Magdeck plate - helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) for bindi in np.arange( settling_time + 1, 0, -0.5 ): # Settling time delay with countdown timer - ctx.delay( + protocol.delay( minutes=0.5, msg="There are " + str(bindi) + " minutes left in the incubation.", ) @@ -376,8 +382,8 @@ def bind(vol1: float, vol2: float) -> None: # remove initial supernatant remove_supernatant(vol1 + starting_vol) - ctx.comment("-----Beginning Bind #2 Steps-----") - tiptrack(tips) + protocol.comment("-----Beginning Bind #2 Steps-----") + tipcheck(m1000) for i, well in enumerate(samples_m): num_trans = math.ceil(vol2 / 980) vol_per_trans = vol2 / num_trans @@ -402,20 +408,22 @@ def bind(vol1: float, vol2: float) -> None: for i in range(num_cols): if i != 0: - tiptrack(tips) + tipcheck(m1000) bead_mixing( samples_m[i], m1000, vol_per_trans, reps=3 if not dry_run else 1 ) m1000.drop_tip() if TIP_TRASH else m1000.return_tip() - helpers.set_hs_speed(ctx, h_s, heater_shaker_speed, bind_time_2, True) + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, bind_time_2, True) # Transfer from H-S plate to Magdeck plate - helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) for bindi in np.arange( settling_time + 1, 0, -0.5 ): # Settling time delay with countdown timer - ctx.delay( + protocol.delay( minutes=0.5, msg="There are " + str(bindi) + " minutes left in the incubation.", ) @@ -426,52 +434,43 @@ def bind(vol1: float, vol2: float) -> None: def wash(vol: float, source: List[Well]) -> None: """Wash Steps.""" global whichwash # Defines which wash the protocol is on to log on the app - - if source == wash1: - whichwash = 1 - const = 6 // len(source) - if source == wash2: - whichwash = 2 - const = 6 // len(source) - height = 1 - if source == wash3: - whichwash = 3 - const = 6 // len(source) - height = 1 - - ctx.comment("-----Wash #" + str(whichwash) + " is starting now------") + protocol.comment("-----Now starting Wash #" + str(whichwash) + "-----") + global wash_volume_tracker num_trans = math.ceil(vol / 980) vol_per_trans = vol / num_trans - tiptrack(tips) + tipcheck(m1000) for i, m in enumerate(samples_m): - if source == wash1: - if i == 0 or i == 3: - height = 10 - else: - height = 1 - src = source[i // const] + src = source[whichwash] for n in range(num_trans): if m1000.current_volume > 0: m1000.dispense(m1000.current_volume, src.top()) m1000.require_liquid_presence(src) m1000.transfer( vol_per_trans, - src.bottom(height), + src.bottom(dot_bottom), m.top(), air_gap=20, new_tip="never", ) + wash_volume_tracker += vol_per_trans * 8 + if wash_volume_tracker >= 9600: + whichwash += 1 + src = source[whichwash] + protocol.comment(f"new wash source {whichwash}") + wash_volume_tracker = 0.0 m1000.drop_tip() if TIP_TRASH else m1000.return_tip() - helpers.set_hs_speed(ctx, h_s, heater_shaker_speed * 0.9, wash_time, True) + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed * 0.9, wash_time, True) - helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) for washi in np.arange( settling_time, 0, -0.5 ): # settling time timer for washes - ctx.delay( + protocol.delay( minutes=0.5, msg="There are " + str(washi) @@ -483,27 +482,31 @@ def wash(vol: float, source: List[Well]) -> None: remove_supernatant(vol) def elute(vol: float) -> None: - tiptrack(tips) + tipcheck(m1000) + total_elution_vol = 0.0 for i, m in enumerate(samples_m): m1000.require_liquid_presence(elution_solution) m1000.aspirate(vol, elution_solution) m1000.air_gap(20) m1000.dispense(m1000.current_volume, m.top(-3)) + total_elution_vol += vol m1000.drop_tip() if TIP_TRASH else m1000.return_tip() - helpers.set_hs_speed(ctx, h_s, heater_shaker_speed * 0.9, wash_time, True) + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed * 0.9, wash_time, True) # Transfer back to magnet - helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) for elutei in np.arange(settling_time, 0, -0.5): - ctx.delay( + protocol.delay( minutes=0.5, msg="Incubating on MagDeck for " + str(elutei) + " more minutes.", ) for i, (m, e) in enumerate(zip(samples_m, elution_samples_m)): - tiptrack(tips) + tipcheck(m1000) m1000.flow_rate.dispense = 100 m1000.flow_rate.aspirate = 25 m1000.transfer( @@ -521,16 +524,23 @@ def elute(vol: float) -> None: """ lysis(lysis_vol, lysis_) bind(binding_buffer_vol, bind2_vol) - wash(wash1_vol, wash1) - wash(wash2_vol, wash2) - wash(wash3_vol, wash3) + wash(wash1_vol, all_washes) + wash(wash2_vol, all_washes) + wash(wash3_vol, all_washes) h_s.set_and_wait_for_temperature(55) for beaddry in np.arange(drybeads, 0, -0.5): - ctx.delay( + protocol.delay( minutes=0.5, msg="There are " + str(beaddry) + " minutes left in the drying step.", ) elute(elution_vol) h_s.deactivate_heater() - flattened_list_of_wells.extend([waste_reservoir["A1"], elutionplate["A1"]]) - helpers.find_liquid_height_of_all_wells(ctx, m1000, flattened_list_of_wells) + helpers.clean_up_plates( + m1000, + [elutionplate, sample_plate, res1, res3, res2], + waste_reservoir["A1"], + 1000, + ) + helpers.find_liquid_height_of_all_wells(protocol, m1000, [waste_reservoir["A1"]]) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/11_Dynabeads_IP_Flex_96well_RIT.py b/abr-testing/abr_testing/protocols/active_protocols/11_Dynabeads_IP_Flex_96well_RIT.py index e885ec45a5e..44db654cc1f 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/11_Dynabeads_IP_Flex_96well_RIT.py +++ b/abr-testing/abr_testing/protocols/active_protocols/11_Dynabeads_IP_Flex_96well_RIT.py @@ -1,5 +1,5 @@ """Immunoprecipitation by Dynabeads.""" -from opentrons.protocol_api import ProtocolContext, ParameterContext, Well +from opentrons.protocol_api import ProtocolContext, ParameterContext, Well, Labware from opentrons.protocol_api.module_contexts import ( HeaterShakerContext, TemperatureModuleContext, @@ -15,7 +15,7 @@ } requirements = { - "robotType": "OT-3", + "robotType": "Flex", "apiLevel": "2.21", } @@ -25,6 +25,7 @@ def add_parameters(parameters: ParameterContext) -> None: helpers.create_hs_speed_parameter(parameters) helpers.create_two_pipette_mount_parameters(parameters) helpers.create_dot_bottom_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) NUM_COL = 12 @@ -34,7 +35,7 @@ def add_parameters(parameters: ParameterContext) -> None: BEADS_VOL = 50 AB_VOL = 50 SAMPLE_VOL = 200 -WASH_TIMES = 3 +WASH_TIMES = 6 WASH_VOL = 200 ELUTION_VOL = 50 @@ -48,64 +49,78 @@ def add_parameters(parameters: ParameterContext) -> None: TIP_TRASH = False -def run(ctx: ProtocolContext) -> None: +def run(protocol: ProtocolContext) -> None: """Protocol.""" # defining variables inside def run - heater_shaker_speed = ctx.params.heater_shaker_speed # type: ignore[attr-defined] - ASP_HEIGHT = ctx.params.dot_bottom # type: ignore[attr-defined] - single_channel_mount = ctx.params.pipette_mount_1 # type: ignore[attr-defined] - eight_channel_mount = ctx.params.pipette_mount_2 # type: ignore[attr-defined] + heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] + ASP_HEIGHT = protocol.params.dot_bottom # type: ignore[attr-defined] + single_channel_mount = protocol.params.pipette_mount_1 # type: ignore[attr-defined] + eight_channel_mount = protocol.params.pipette_mount_2 # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") + MIX_SPEED = heater_shaker_speed MIX_SEC = 10 # if on deck: - INCUBATION_SPEEND = heater_shaker_speed * 0.5 + INCUBATION_SPEED = heater_shaker_speed * 0.5 INCUBATION_MIN = 60 # load labware - sample_plate = ctx.load_labware("nest_96_wellplate_2ml_deep", "B2", "samples") - wash_res = ctx.load_labware("nest_12_reservoir_15ml", "B1", "wash") - reagent_res = ctx.load_labware( + sample_plate_1 = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "B2", "sample plate 1" + ) + sample_plate_2 = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "C4", "sample plate 2" + ) + + wash_res = protocol.load_labware("nest_12_reservoir_15ml", "B1", "wash") + reagent_res = protocol.load_labware( "opentrons_15_tuberack_nest_15ml_conical", "C3", "reagents" ) - waste_res = ctx.load_labware("nest_1_reservoir_290ml", "D2", "waste") + waste_res = protocol.load_labware("nest_1_reservoir_290ml", "D2", "Liquid Waste") - tips = ctx.load_labware("opentrons_flex_96_tiprack_1000ul", "B3") - tips_sample = ctx.load_labware( + tips = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "B3") + tips_sample = protocol.load_labware( "opentrons_flex_96_tiprack_1000ul", "A2", "sample tips" ) tips_sample_loc = tips_sample.wells()[:95] if READY_FOR_SDSPAGE == 0: - tips_elu = ctx.load_labware( + tips_elu = protocol.load_labware( "opentrons_flex_96_tiprack_1000ul", "A1", "elution tips" ) tips_elu_loc = tips_elu.wells()[:95] - tips_reused = ctx.load_labware( + tips_reused = protocol.load_labware( "opentrons_flex_96_tiprack_1000ul", "C2", "reused tips" ) tips_reused_loc = tips_reused.wells()[:95] - p1000 = ctx.load_instrument( + p1000 = protocol.load_instrument( "flex_8channel_1000", eight_channel_mount, tip_racks=[tips] ) - p1000_single = ctx.load_instrument( + p1000_single = protocol.load_instrument( "flex_1channel_1000", single_channel_mount, tip_racks=[tips] ) - h_s: HeaterShakerContext = ctx.load_module(helpers.hs_str, "D1") # type: ignore[assignment] + h_s: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "D1" + ) # type: ignore[assignment] working_plate, h_s_adapter = helpers.load_hs_adapter_and_labware( "nest_96_wellplate_2ml_deep", h_s, "Working Plate" ) if READY_FOR_SDSPAGE == 0: - temp: TemperatureModuleContext = ctx.load_module( + temp: TemperatureModuleContext = protocol.load_module( helpers.temp_str, "D3" ) # type: ignore[assignment] final_plate, temp_adapter = helpers.load_temp_adapter_and_labware( "nest_96_wellplate_2ml_deep", temp, "Final Plate" ) - mag: MagneticBlockContext = ctx.load_module(helpers.mag_str, "C1") # type: ignore[assignment] + mag: MagneticBlockContext = protocol.load_module( + helpers.mag_str, "C1" + ) # type: ignore[assignment] # liquids - samples = sample_plate.rows()[0][:NUM_COL] # 1 + samples1 = sample_plate_1.rows()[0][:NUM_COL] # 1 + samples2 = sample_plate_2.rows()[0][:NUM_COL] # 1 beads = reagent_res.wells()[0] # 2 ab = reagent_res.wells()[1] # 3 elu = reagent_res.wells()[2] # 4 @@ -119,14 +134,15 @@ def run(ctx: ProtocolContext) -> None: liquid_vols_and_wells: Dict[ str, List[Dict[str, Union[Well, List[Well], float]]] ] = { - "Beads": [{"well": beads, "volume": 4900.0}], - "AB": [{"well": ab, "volume": 4900.0}], - "Elution": [{"well": elu, "volume": 4900.0}], - "Wash": [{"well": wash, "volume": 750.0}], - "Samples": [{"well": samples, "volume": 250.0}], + "Beads": [{"well": beads, "volume": 9800.0}], + "AB": [{"well": ab, "volume": 9800.0}], + "Elution": [{"well": elu, "volume": 9800.0}], + "Wash": [{"well": wash, "volume": 1500.0}], + "Samples 1": [{"well": samples1, "volume": 250.0}], + "Samples 2": [{"well": samples2, "volume": 250.0}], } helpers.find_liquid_height_of_loaded_liquids( - ctx, liquid_vols_and_wells, p1000_single + protocol, liquid_vols_and_wells, p1000_single ) def transfer_plate_to_plate( @@ -193,9 +209,6 @@ def discard(vol3: float, start: List[Well]) -> None: """Discard function.""" global waste_vol global waste_vol_chk - if waste_vol_chk >= WASTE_VOL_MAX: - ctx.pause("Empty Liquid Waste") - waste_vol_chk = 0 waste_vol = 0.0 for k in range(NUM_COL): p1000.pick_up_tip(tips_reused_loc[k * 8]) @@ -210,64 +223,77 @@ def discard(vol3: float, start: List[Well]) -> None: waste_vol_chk = waste_vol_chk + waste_vol # protocol - - # Add beads, samples and antibody solution - h_s.close_labware_latch() - transfer_well_to_plate(BEADS_VOL, beads, working_wells, 2) - - helpers.move_labware_from_hs_to_destination(ctx, working_plate, h_s, mag) - - ctx.delay(minutes=MAG_DELAY_MIN) - discard(BEADS_VOL * 1.1, working_cols) - - helpers.move_labware_to_hs(ctx, working_plate, h_s, h_s_adapter) - - transfer_plate_to_plate(SAMPLE_VOL, samples, working_cols, 1) - transfer_well_to_plate(AB_VOL, ab, working_wells, 3) - - h_s.set_and_wait_for_shake_speed(rpm=MIX_SPEED) - ctx.delay(seconds=MIX_SEC) - - h_s.set_and_wait_for_shake_speed(rpm=INCUBATION_SPEEND) - ctx.delay(seconds=INCUBATION_MIN * 60) - h_s.deactivate_shaker() - - helpers.move_labware_from_hs_to_destination(ctx, working_plate, h_s, mag) - - ctx.delay(minutes=MAG_DELAY_MIN) - vol_total = SAMPLE_VOL + AB_VOL - discard(vol_total * 1.1, working_cols) - - # Wash - for _ in range(WASH_TIMES): - helpers.move_labware_to_hs(ctx, working_plate, h_s, h_s_adapter) - - transfer_well_to_plate(WASH_VOL, wash, working_cols, 5) - helpers.set_hs_speed(ctx, h_s, MIX_SPEED, MIX_SEC / 60, True) - helpers.move_labware_from_hs_to_destination(ctx, working_plate, h_s, mag) - ctx.delay(minutes=MAG_DELAY_MIN) - discard(WASH_VOL * 1.1, working_cols) - - # Elution - helpers.move_labware_to_hs(ctx, working_plate, h_s, h_s_adapter) - - transfer_well_to_plate(ELUTION_VOL, elu, working_wells, 4) - if READY_FOR_SDSPAGE == 1: - ctx.pause("Seal the Working Plate") - h_s.set_and_wait_for_temperature(70) - helpers.set_hs_speed(ctx, h_s, MIX_SPEED, (MIX_SEC / 60) + 10, True) - h_s.deactivate_heater() - h_s.open_labware_latch() - ctx.pause("Protocol Complete") - - elif READY_FOR_SDSPAGE == 0: - helpers.set_hs_speed(ctx, h_s, MIX_SPEED, (MIX_SEC / 60) + 2, True) - - temp.set_temperature(4) - helpers.move_labware_from_hs_to_destination(ctx, working_plate, h_s, mag) - ctx.delay(minutes=MAG_DELAY_MIN) - transfer_plate_to_plate(ELUTION_VOL * 1.1, working_cols, final_cols, 6) - temp.deactivate() - end_wells_to_probe = [reagent_res["A1"], reagent_res["B1"], reagent_res["C1"]] - end_wells_to_probe.extend(wash_res.wells()) - helpers.find_liquid_height_of_all_wells(ctx, p1000_single, end_wells_to_probe) + def run(sample_plate: Labware) -> None: + """Protocol.""" + # Add beads, samples and antibody solution + samples = sample_plate.rows()[0][:NUM_COL] # 1 + h_s.close_labware_latch() + transfer_well_to_plate(BEADS_VOL, beads, working_wells, 2) + + helpers.move_labware_from_hs_to_destination(protocol, working_plate, h_s, mag) + + protocol.delay(minutes=MAG_DELAY_MIN) + discard(BEADS_VOL * 1.1, working_cols) + + helpers.move_labware_to_hs(protocol, working_plate, h_s, h_s_adapter) + + transfer_plate_to_plate(SAMPLE_VOL, samples, working_cols, 1) + transfer_well_to_plate(AB_VOL, ab, working_wells, 3) + + h_s.set_and_wait_for_shake_speed(rpm=MIX_SPEED) + protocol.delay(seconds=MIX_SEC) + + h_s.set_and_wait_for_shake_speed(rpm=INCUBATION_SPEED) + protocol.delay(seconds=INCUBATION_MIN * 60) + h_s.deactivate_shaker() + + helpers.move_labware_from_hs_to_destination(protocol, working_plate, h_s, mag) + + protocol.delay(minutes=MAG_DELAY_MIN) + vol_total = SAMPLE_VOL + AB_VOL + discard(vol_total * 1.1, working_cols) + + # Wash + for _ in range(WASH_TIMES): + helpers.move_labware_to_hs(protocol, working_plate, h_s, h_s_adapter) + + transfer_well_to_plate(WASH_VOL, wash, working_cols, 5) + helpers.set_hs_speed(protocol, h_s, MIX_SPEED, MIX_SEC / 60, True) + helpers.move_labware_from_hs_to_destination( + protocol, working_plate, h_s, mag + ) + protocol.delay(minutes=MAG_DELAY_MIN) + discard(WASH_VOL * 1.1, working_cols) + # Elution + helpers.move_labware_to_hs(protocol, working_plate, h_s, h_s_adapter) + transfer_well_to_plate(ELUTION_VOL, elu, working_wells, 4) + if READY_FOR_SDSPAGE == 1: + protocol.pause("Seal the Working Plate") + h_s.set_and_wait_for_temperature(70) + helpers.set_hs_speed(protocol, h_s, MIX_SPEED, (MIX_SEC / 60) + 10, True) + h_s.deactivate_heater() + h_s.open_labware_latch() + protocol.pause("Protocol Complete") + elif READY_FOR_SDSPAGE == 0: + helpers.set_hs_speed(protocol, h_s, MIX_SPEED, (MIX_SEC / 60) + 2, True) + + temp.set_temperature(4) + helpers.move_labware_from_hs_to_destination( + protocol, working_plate, h_s, mag + ) + protocol.delay(minutes=MAG_DELAY_MIN) + transfer_plate_to_plate(ELUTION_VOL * 1.1, working_cols, final_cols, 6) + temp.deactivate() + helpers.clean_up_plates(p1000_single, [sample_plate], waste, 1000) + helpers.move_labware_to_hs(protocol, working_plate, h_s, h_s_adapter) + + run(sample_plate_1) + # swap plates + protocol.move_labware(sample_plate_1, "B4", True) + protocol.move_labware(sample_plate_2, "B2", True) + run(sample_plate_2) + + helpers.clean_up_plates(p1000_single, [wash_res], waste, 1000) + helpers.find_liquid_height_of_all_wells(protocol, p1000_single, [waste_res["A1"]]) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/12_KAPA HyperPlus Library Prep.py b/abr-testing/abr_testing/protocols/active_protocols/12_KAPA HyperPlus Library Prep.py index 75658f11438..ff38e8cf7c7 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/12_KAPA HyperPlus Library Prep.py +++ b/abr-testing/abr_testing/protocols/active_protocols/12_KAPA HyperPlus Library Prep.py @@ -18,7 +18,7 @@ metadata = { "protocolName": "KAPA HyperPlus Library Preparation", - "author": "Your Name ", + "author": "Tony Ngumah ", } requirements = {"robotType": "Flex", "apiLevel": "2.21"} @@ -41,6 +41,7 @@ def add_parameters(parameters: ParameterContext) -> None: helpers.create_disposable_lid_parameter(parameters) helpers.create_tc_lid_deck_riser_parameter(parameters) helpers.create_two_pipette_mount_parameters(parameters) + helpers.create_deactivate_modules_parameter(parameters) parameters.add_int( variable_name="num_samples", display_name="number of samples", @@ -68,22 +69,25 @@ def add_parameters(parameters: ParameterContext) -> None: ) -def run(ctx: ProtocolContext) -> None: +def run(protocol: ProtocolContext) -> None: """Protocol.""" USE_GRIPPER = True - trash_tips = ctx.params.trash_tips # type: ignore[attr-defined] - dry_run = ctx.params.dry_run # type: ignore[attr-defined] - pipette_1000_mount = ctx.params.pipette_mount_1 # type: ignore[attr-defined] - pipette_50_mount = ctx.params.pipette_mount_2 # type: ignore[attr-defined] - deck_riser = ctx.params.deck_riser # type: ignore[attr-defined] + deactivate_mods = protocol.params.deactivate_modules # type: ignore[attr-defined] + trash_tips = protocol.params.trash_tips # type: ignore[attr-defined] + dry_run = protocol.params.dry_run # type: ignore[attr-defined] + pipette_1000_mount = protocol.params.pipette_mount_1 # type: ignore[attr-defined] + pipette_50_mount = protocol.params.pipette_mount_2 # type: ignore[attr-defined] + deck_riser = protocol.params.deck_riser # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") + REUSE_ETOH_TIPS = True REUSE_RSB_TIPS = ( True # Reuse tips for RSB buffer (adding RSB, mixing, and transferring) ) REUSE_REMOVE_TIPS = True # Reuse tips for supernatant removal - num_samples = ctx.params.num_samples # type: ignore[attr-defined] - PCRCYCLES = ctx.params.PCR_CYCLES # type: ignore[attr-defined] - disposable_lid = ctx.params.disposable_lid # type: ignore[attr-defined] + num_samples = protocol.params.num_samples # type: ignore[attr-defined] + PCRCYCLES = protocol.params.PCR_CYCLES # type: ignore[attr-defined] + disposable_lid = protocol.params.disposable_lid # type: ignore[attr-defined] Fragmentation_time = 10 ligation_tc_time = 15 used_lids: List[Labware] = [] @@ -111,10 +115,10 @@ def run(ctx: ProtocolContext) -> None: # etoh_vol = 400.0 # Importing Labware, Modules and Instruments - magblock: MagneticBlockContext = ctx.load_module( + magblock: MagneticBlockContext = protocol.load_module( helpers.mag_str, "D2" ) # type: ignore[assignment] - temp_mod: TemperatureModuleContext = ctx.load_module( + temp_mod: TemperatureModuleContext = protocol.load_module( helpers.temp_str, "B3" ) # type: ignore[assignment] temp_plate, temp_adapter = helpers.load_temp_adapter_and_labware( @@ -125,7 +129,7 @@ def run(ctx: ProtocolContext) -> None: if not dry_run: temp_mod.set_temperature(4) - tc_mod: ThermocyclerContext = ctx.load_module(helpers.tc_str) # type: ignore[assignment] + tc_mod: ThermocyclerContext = protocol.load_module(helpers.tc_str) # type: ignore[assignment] # Just in case tc_mod.open_lid() @@ -134,32 +138,32 @@ def run(ctx: ProtocolContext) -> None: ) samples_flp = FLP_plate.rows()[0][:num_cols] - sample_plate = ctx.load_labware( + sample_plate = protocol.load_labware( "armadillo_96_wellplate_200ul_pcr_full_skirt", "D1", "Sample Plate 1" ) - sample_plate_2 = ctx.load_labware( + sample_plate_2 = protocol.load_labware( "armadillo_96_wellplate_200ul_pcr_full_skirt", "B2", "Sample Plate 2" ) samples_2 = sample_plate_2.rows()[0][:num_cols] samples = sample_plate.rows()[0][:num_cols] - reservoir = ctx.load_labware( + reservoir = protocol.load_labware( "nest_96_wellplate_2ml_deep", "C2", "Beads + Buffer + Ethanol" ) # Load tipracks - tiprack_50_1 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "A3") - tiprack_50_2 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "A2") + tiprack_50_1 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "A3") + tiprack_50_2 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "A2") - tiprack_200_1 = ctx.load_labware("opentrons_flex_96_tiprack_200ul", "C1") - tiprack_200_2 = ctx.load_labware("opentrons_flex_96_tiprack_200ul", "C3") + tiprack_200_1 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "C1") + tiprack_200_2 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "C3") if trash_tips: - ctx.load_waste_chute() + protocol.load_waste_chute() unused_lids: List[Labware] = [] # Load TC Lids if disposable_lid: - unused_lids = helpers.load_disposable_lids(ctx, 5, ["C4"], deck_riser) + unused_lids = helpers.load_disposable_lids(protocol, 5, ["C4"], deck_riser) # Import Global Variables global tip50 @@ -168,12 +172,12 @@ def run(ctx: ProtocolContext) -> None: global p200_rack_count tip_count = {1000: 0, 50: 0} - p200 = ctx.load_instrument( + p200 = protocol.load_instrument( "flex_8channel_1000", pipette_1000_mount, tip_racks=[tiprack_200_1, tiprack_200_2], ) - p50 = ctx.load_instrument( + p50 = protocol.load_instrument( "flex_8channel_50", pipette_50_mount, tip_racks=[tiprack_50_1, tiprack_50_2] ) @@ -224,7 +228,7 @@ def run(ctx: ProtocolContext) -> None: waste2 = reservoir.columns()[7] waste2_res = waste2[0] - helpers.find_liquid_height_of_loaded_liquids(ctx, liquid_vols_and_wells, p50) + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, p50) def tip_track(pipette: InstrumentContext, tip_count: Dict) -> None: """Track tip usage.""" @@ -248,19 +252,19 @@ def run_tag_profile( ) -> Tuple[List[Labware], List[Labware]]: """Run Tag Profile.""" # Presetting Thermocycler Temps - ctx.comment( + protocol.comment( "****Starting Fragmentation Profile (37C for 10 minutes with 100C lid)****" ) tc_mod.set_lid_temperature(100) tc_mod.set_block_temperature(37) # Move Plate to TC - ctx.comment("****Moving Plate to Pre-Warmed TC Module Block****") - ctx.move_labware(sample_plate, tc_mod, use_gripper=USE_GRIPPER) + protocol.comment("****Moving Plate to Pre-Warmed TC Module Block****") + protocol.move_labware(sample_plate, tc_mod, use_gripper=USE_GRIPPER) if disposable_lid: lid_on_plate, unused_lids, used_lids = helpers.use_disposable_lid_with_tc( - ctx, unused_lids, used_lids, sample_plate, tc_mod + protocol, unused_lids, used_lids, sample_plate, tc_mod ) else: tc_mod.close_lid() @@ -271,13 +275,13 @@ def run_tag_profile( if disposable_lid: if len(used_lids) <= 1: - ctx.move_labware(lid_on_plate, "D4", use_gripper=True) + protocol.move_labware(lid_on_plate, "D4", use_gripper=True) else: - ctx.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) # #Move Plate to H-S - ctx.comment("****Moving Plate off of TC****") + protocol.comment("****Moving Plate off of TC****") - ctx.move_labware(sample_plate, "D1", use_gripper=USE_GRIPPER) + protocol.move_labware(sample_plate, "D1", use_gripper=USE_GRIPPER) return unused_lids, used_lids def run_er_profile( @@ -285,19 +289,19 @@ def run_er_profile( ) -> Tuple[List[Labware], List[Labware]]: """End Repair Profile.""" # Presetting Thermocycler Temps - ctx.comment( + protocol.comment( "****Starting End Repair Profile (65C for 30 minutes with 100C lid)****" ) tc_mod.set_lid_temperature(100) tc_mod.set_block_temperature(65) # Move Plate to TC - ctx.comment("****Moving Plate to Pre-Warmed TC Module Block****") - ctx.move_labware(sample_plate, tc_mod, use_gripper=USE_GRIPPER) + protocol.comment("****Moving Plate to Pre-Warmed TC Module Block****") + protocol.move_labware(sample_plate, tc_mod, use_gripper=USE_GRIPPER) if disposable_lid: lid_on_plate, unused_lids, used_lids = helpers.use_disposable_lid_with_tc( - ctx, unused_lids, used_lids, sample_plate, tc_mod + protocol, unused_lids, used_lids, sample_plate, tc_mod ) else: tc_mod.close_lid() @@ -311,13 +315,13 @@ def run_er_profile( if disposable_lid: # move lid if len(used_lids) <= 1: - ctx.move_labware(lid_on_plate, "C4", use_gripper=True) + protocol.move_labware(lid_on_plate, "C4", use_gripper=True) else: - ctx.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) # #Move Plate to H-S - ctx.comment("****Moving Plate off of TC****") + protocol.comment("****Moving Plate off of TC****") - ctx.move_labware(sample_plate, "D1", use_gripper=USE_GRIPPER) + protocol.move_labware(sample_plate, "D1", use_gripper=USE_GRIPPER) return unused_lids, used_lids def run_ligation_profile( @@ -325,20 +329,20 @@ def run_ligation_profile( ) -> Tuple[List[Labware], List[Labware]]: """Run Ligation Profile.""" # Presetting Thermocycler Temps - ctx.comment( + protocol.comment( "****Starting Ligation Profile (20C for 15 minutes with 100C lid)****" ) tc_mod.set_lid_temperature(100) tc_mod.set_block_temperature(20) # Move Plate to TC - ctx.comment("****Moving Plate to Pre-Warmed TC Module Block****") + protocol.comment("****Moving Plate to Pre-Warmed TC Module Block****") - ctx.move_labware(sample_plate, tc_mod, use_gripper=USE_GRIPPER) + protocol.move_labware(sample_plate, tc_mod, use_gripper=USE_GRIPPER) if disposable_lid: lid_on_plate, unused_lids, used_lids = helpers.use_disposable_lid_with_tc( - ctx, unused_lids, used_lids, sample_plate, tc_mod + protocol, unused_lids, used_lids, sample_plate, tc_mod ) else: tc_mod.close_lid() @@ -353,14 +357,14 @@ def run_ligation_profile( tc_mod.open_lid() if disposable_lid: if len(used_lids) <= 1: - ctx.move_labware(lid_on_plate, "C4", use_gripper=True) + protocol.move_labware(lid_on_plate, "C4", use_gripper=True) else: - ctx.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) # #Move Plate to H-S - ctx.comment("****Moving Plate off of TC****") + protocol.comment("****Moving Plate off of TC****") - ctx.move_labware(sample_plate, "D1", use_gripper=USE_GRIPPER) + protocol.move_labware(sample_plate, "D1", use_gripper=USE_GRIPPER) return unused_lids, used_lids def run_amplification_profile( @@ -368,27 +372,27 @@ def run_amplification_profile( ) -> Tuple[List[Labware], List[Labware]]: """Run Amplification Profile.""" # Presetting Thermocycler Temps - ctx.comment( + protocol.comment( "Amplification Profile (37C for 5 min, 50C for 5 min with 100C lid)" ) tc_mod.set_lid_temperature(100) tc_mod.set_block_temperature(98) # Move Plate to TC - ctx.comment("****Moving Sample Plate onto TC****") - ctx.move_labware(sample_plate_2, tc_mod, use_gripper=USE_GRIPPER) + protocol.comment("****Moving Sample Plate onto TC****") + protocol.move_labware(sample_plate_2, tc_mod, use_gripper=USE_GRIPPER) if not dry_run: tc_mod.set_lid_temperature(105) if disposable_lid: lid_on_plate, unused_lids, used_lids = helpers.use_disposable_lid_with_tc( - ctx, unused_lids, used_lids, sample_plate_2, tc_mod + protocol, unused_lids, used_lids, sample_plate_2, tc_mod ) else: tc_mod.close_lid() if not dry_run: helpers.perform_pcr( - ctx, + protocol, tc_mod, initial_denature_time_sec=45, denaturation_time_sec=15, @@ -401,16 +405,16 @@ def run_amplification_profile( tc_mod.open_lid() if disposable_lid: if len(used_lids) <= 1: - ctx.move_labware(lid_on_plate, "C4", use_gripper=True) + protocol.move_labware(lid_on_plate, "C4", use_gripper=True) else: - ctx.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) # Move Sample Plate to H-S - ctx.comment("****Moving Sample Plate back to H-S****") - ctx.move_labware(sample_plate_2, "D1", use_gripper=USE_GRIPPER) + protocol.comment("****Moving Sample Plate back to H-S****") + protocol.move_labware(sample_plate_2, "D1", use_gripper=USE_GRIPPER) # get FLP plate out of the way - ctx.comment("****Moving FLP Plate back to TC****") - ctx.move_labware(FLP_plate, tc_mod, use_gripper=USE_GRIPPER) + protocol.comment("****Moving FLP Plate back to TC****") + protocol.move_labware(FLP_plate, tc_mod, use_gripper=USE_GRIPPER) return unused_lids, used_lids def mix_beads( @@ -463,39 +467,39 @@ def mix_beads( def remove_supernatant(well: Well, vol: float, waste_: Well, column: int) -> None: """Remove supernatant.""" - ctx.comment("-------Removing " + str(vol) + "ul of Supernatant-------") + protocol.comment("-------Removing " + str(vol) + "ul of Supernatant-------") p200.flow_rate.aspirate = 15 num_trans = math.ceil(vol / 190) vol_per_trans = vol / num_trans tip_track(p200, tip_count) for x in range(num_trans): p200.aspirate(vol_per_trans / 2, well.bottom(0.2)) - ctx.delay(seconds=1) + protocol.delay(seconds=1) p200.aspirate(vol_per_trans / 2, well.bottom(0.2)) p200.air_gap(10) p200.dispense(p200.current_volume, waste_) p200.air_gap(10) if REUSE_REMOVE_TIPS: p200.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") else: if trash_tips: p200.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p200.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") p200.flow_rate.aspirate = 150 def Fragmentation( unused_lids: List[Labware], used_lids: List[Labware] ) -> Tuple[List[Labware], List[Labware]]: """Fragmentation Function.""" - ctx.comment("-------Starting Fragmentation-------") + protocol.comment("-------Starting Fragmentation-------") for i in range(num_cols): - ctx.comment("Mixing and Transfering beads to column " + str(i + 1)) + protocol.comment("Mixing and Transfering beads to column " + str(i + 1)) p50.flow_rate.dispense = 15 tip_track(p50, tip_count) @@ -512,10 +516,10 @@ def Fragmentation( p50.flow_rate.dispense = 150 if trash_tips: p50.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p50.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") unused_lids, used_lids = run_tag_profile( unused_lids, used_lids @@ -526,11 +530,11 @@ def end_repair( unused_lids: List[Labware], used_lids: List[Labware] ) -> Tuple[List[Labware], List[Labware]]: """End Repair Function.""" - ctx.comment("-------Starting end_repair-------") + protocol.comment("-------Starting end_repair-------") for i in range(num_cols): - ctx.comment( + protocol.comment( "**** Mixing and Transfering beads to column " + str(i + 1) + " ****" ) @@ -549,10 +553,10 @@ def end_repair( p50.flow_rate.dispense = 150 if trash_tips: p50.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p50.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") unused_lids, used_lids = run_er_profile( unused_lids, used_lids @@ -565,8 +569,8 @@ def index_ligation( unused_lids: List[Labware], used_lids: List[Labware] ) -> Tuple[List[Labware], List[Labware]]: """Index Ligation.""" - ctx.comment("-------Ligating Indexes-------") - ctx.comment("-------Adding and Mixing ELM-------") + protocol.comment("-------Ligating Indexes-------") + protocol.comment("-------Adding and Mixing ELM-------") for i in samples: tip_track(p50, tip_count) p50.aspirate(ligation_vol, ligation_res) @@ -581,13 +585,13 @@ def index_ligation( p50.flow_rate.dispense = 150 if trash_tips: p50.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p50.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") # Add and mix adapters - ctx.comment("-------Adding and Mixing Adapters-------") + protocol.comment("-------Adding and Mixing Adapters-------") for i_well, x_well in zip(samples, adapters): tip_track(p50, tip_count) p50.aspirate(adapter_vol, x_well) @@ -600,10 +604,10 @@ def index_ligation( p50.dispense(40, i_well.bottom(8)) if trash_tips: p50.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p50.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") p50.flow_rate.aspirate = 150 p50.flow_rate.dispense = 150 @@ -613,13 +617,13 @@ def index_ligation( def lib_cleanup() -> None: """Litigation Clean up.""" - ctx.comment("-------Starting Cleanup-------") - ctx.comment("-------Adding and Mixing Cleanup Beads-------") + protocol.comment("-------Starting Cleanup-------") + protocol.comment("-------Adding and Mixing Cleanup Beads-------") # Move FLP plate off magnetic module if it's there if FLP_plate.parent == magblock: - ctx.comment("****Moving FLP Plate off Magnetic Module****") - ctx.move_labware(FLP_plate, tc_mod, use_gripper=USE_GRIPPER) + protocol.comment("****Moving FLP Plate off Magnetic Module****") + protocol.move_labware(FLP_plate, tc_mod, use_gripper=USE_GRIPPER) for x, i in enumerate(samples): mix_beads(p200, bead_res, bead_vol_1, 7 if x == 0 else 2, x) @@ -636,22 +640,22 @@ def lib_cleanup() -> None: p200.flow_rate.dispense = 150 if trash_tips: p200.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p200.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") - ctx.delay( + protocol.delay( minutes=bead_inc, msg="Please wait " + str(bead_inc) + " minutes while samples incubate at RT.", ) - ctx.comment("****Moving Labware to Magnet for Pelleting****") - ctx.move_labware(sample_plate, magblock, use_gripper=USE_GRIPPER) + protocol.comment("****Moving Labware to Magnet for Pelleting****") + protocol.move_labware(sample_plate, magblock, use_gripper=USE_GRIPPER) - ctx.delay(minutes=4.5, msg="Time for Pelleting") + protocol.delay(minutes=4.5, msg="Time for Pelleting") for col, i in enumerate(samples): remove_supernatant(i, 130, waste1_res, col) @@ -661,7 +665,7 @@ def lib_cleanup() -> None: p200.flow_rate.aspirate = 75 p200.flow_rate.dispense = 75 for y in range(2 if not dry_run else 1): - ctx.comment(f"-------Wash # {y+1} with Ethanol-------") + protocol.comment(f"-------Wash # {y+1} with Ethanol-------") if y == 0: # First wash this_res = etoh1_res this_waste_res = waste1_res @@ -672,42 +676,42 @@ def lib_cleanup() -> None: p200.aspirate(150, this_res) p200.air_gap(10) p200.dispense(p200.current_volume, i.top()) - ctx.delay(seconds=1) + protocol.delay(seconds=1) p200.air_gap(10) if not REUSE_ETOH_TIPS: p200.drop_tip() if trash_tips else p200.return_tip() - ctx.delay(seconds=10) + protocol.delay(seconds=10) # Remove the ethanol wash for x, i in enumerate(samp_list): tip_track(p200, tip_count) p200.aspirate(155, i) p200.air_gap(10) p200.dispense(p200.current_volume, this_waste_res) - ctx.delay(seconds=1) + protocol.delay(seconds=1) p200.air_gap(10) if trash_tips: p200.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p200.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") p200.flow_rate.aspirate = 150 p200.flow_rate.dispense = 150 # Wash complete, move on to drying steps. - ctx.delay(minutes=2, msg="Allow 3 minutes for residual ethanol to dry") + protocol.delay(minutes=2, msg="Allow 3 minutes for residual ethanol to dry") # Return Plate to H-S from Magnet - ctx.comment("****Moving sample plate off of Magnet****") - ctx.move_labware(sample_plate, "D1", use_gripper=USE_GRIPPER) + protocol.comment("****Moving sample plate off of Magnet****") + protocol.move_labware(sample_plate, "D1", use_gripper=USE_GRIPPER) # Adding RSB and Mixing for col, i in enumerate(samp_list): - ctx.comment(f"****Adding RSB to Columns: {col+1}****") + protocol.comment(f"****Adding RSB to Columns: {col+1}****") tip_track(p50, tip_count) p50.aspirate(rsb_vol_1, rsb_res) p50.air_gap(5) @@ -723,23 +727,23 @@ def lib_cleanup() -> None: p50.air_gap(5) if REUSE_RSB_TIPS: p50.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") else: if trash_tips: p50.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p50.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") - ctx.delay( + protocol.delay( minutes=3, msg="Allow 3 minutes for incubation and liquid aggregation." ) - ctx.comment("****Move Samples to Magnet for Pelleting****") - ctx.move_labware(sample_plate, magblock, use_gripper=USE_GRIPPER) + protocol.comment("****Move Samples to Magnet for Pelleting****") + protocol.move_labware(sample_plate, magblock, use_gripper=USE_GRIPPER) - ctx.delay(minutes=2, msg="Please allow 2 minutes for beads to pellet.") + protocol.delay(minutes=2, msg="Please allow 2 minutes for beads to pellet.") p200.flow_rate.aspirate = 10 for i_int, (s, e) in enumerate(zip(samp_list, samples_2)): @@ -750,31 +754,31 @@ def lib_cleanup() -> None: p50.air_gap(5) if trash_tips: p50.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p50.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") # move new sample plate to D1 or heatershaker - ctx.comment("****Moving sample plate off of Magnet****") - ctx.move_labware(sample_plate_2, "D1", use_gripper=USE_GRIPPER) + protocol.comment("****Moving sample plate off of Magnet****") + protocol.move_labware(sample_plate_2, "D1", use_gripper=USE_GRIPPER) # Keep Sample PLate 1 to B2 - ctx.comment("****Moving Sample_plate_1 Plate off magnet to B2****") - ctx.move_labware(sample_plate, "B2", use_gripper=USE_GRIPPER) + protocol.comment("****Moving Sample_plate_1 Plate off magnet to B2****") + protocol.move_labware(sample_plate, "B2", use_gripper=USE_GRIPPER) - ctx.comment("****Moving FLP Plate off TC****") - ctx.move_labware(FLP_plate, magblock, use_gripper=USE_GRIPPER) + protocol.comment("****Moving FLP Plate off TC****") + protocol.move_labware(FLP_plate, magblock, use_gripper=USE_GRIPPER) def lib_amplification( unused_lids: List[Labware], used_lids: List[Labware] ) -> Tuple[List[Labware], List[Labware]]: """Library Amplification.""" - ctx.comment("-------Starting lib_amplification-------") + protocol.comment("-------Starting lib_amplification-------") for i in range(num_cols): - ctx.comment( + protocol.comment( "**** Mixing and Transfering beads to column " + str(i + 1) + " ****" ) mix_beads( @@ -795,10 +799,10 @@ def lib_amplification( p50.flow_rate.dispense = 150 if trash_tips: p50.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p50.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") unused_lids, used_lids = run_amplification_profile( unused_lids, used_lids @@ -807,8 +811,8 @@ def lib_amplification( def lib_cleanup_2() -> None: """Final Library Clean up.""" - ctx.comment("-------Starting Cleanup-------") - ctx.comment("-------Adding and Mixing Cleanup Beads-------") + protocol.comment("-------Starting Cleanup-------") + protocol.comment("-------Adding and Mixing Cleanup Beads-------") for x, i in enumerate(samples_2): mix_beads(p200, bead_res, bead_vol_2, 7 if x == 0 else 2, x) tip_track(p200, tip_count) @@ -826,22 +830,22 @@ def lib_cleanup_2() -> None: p200.flow_rate.dispense = 150 if trash_tips: p200.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p200.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") - ctx.delay( + protocol.delay( minutes=bead_inc, msg="Please wait " + str(bead_inc) + " minutes while samples incubate at RT.", ) - ctx.comment("****Moving Labware to Magnet for Pelleting****") - ctx.move_labware(sample_plate_2, magblock, use_gripper=USE_GRIPPER) + protocol.comment("****Moving Labware to Magnet for Pelleting****") + protocol.move_labware(sample_plate_2, magblock, use_gripper=USE_GRIPPER) - ctx.delay(minutes=4.5, msg="Time for Pelleting") + protocol.delay(minutes=4.5, msg="Time for Pelleting") for col, i in enumerate(samples_2): remove_supernatant(i, 130, waste1_res, col) @@ -851,7 +855,7 @@ def lib_cleanup_2() -> None: p200.flow_rate.aspirate = 75 p200.flow_rate.dispense = 75 for y in range(2 if not dry_run else 1): - ctx.comment(f"-------Wash # {y+1} with Ethanol-------") + protocol.comment(f"-------Wash # {y+1} with Ethanol-------") if y == 0: # First wash this_res = etoh1_res this_waste_res = waste1_res @@ -862,41 +866,41 @@ def lib_cleanup_2() -> None: p200.aspirate(150, this_res) p200.air_gap(10) p200.dispense(p200.current_volume, i.top()) - ctx.delay(seconds=1) + protocol.delay(seconds=1) p200.air_gap(10) if not REUSE_ETOH_TIPS: p200.drop_tip() if trash_tips else p200.return_tip() - ctx.delay(seconds=10) + protocol.delay(seconds=10) # Remove the ethanol wash for x, i in enumerate(samp_list_2): tip_track(p200, tip_count) p200.aspirate(155, i) p200.air_gap(10) p200.dispense(p200.current_volume, this_waste_res) - ctx.delay(seconds=1) + protocol.delay(seconds=1) p200.air_gap(10) if trash_tips: p200.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p200.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") p200.flow_rate.aspirate = 150 p200.flow_rate.dispense = 150 # Washes Complete, Move on to Drying Steps - ctx.delay(minutes=3, msg="Allow 3 minutes for residual ethanol to dry") + protocol.delay(minutes=3, msg="Allow 3 minutes for residual ethanol to dry") - ctx.comment("****Moving sample plate off of Magnet****") - ctx.move_labware(sample_plate_2, "D1", use_gripper=USE_GRIPPER) + protocol.comment("****Moving sample plate off of Magnet****") + protocol.move_labware(sample_plate_2, "D1", use_gripper=USE_GRIPPER) # Adding RSB and Mixing for col, i in enumerate(samp_list_2): - ctx.comment(f"****Adding RSB to Columns: {col+1}****") + protocol.comment(f"****Adding RSB to Columns: {col+1}****") tip_track(p50, tip_count) p50.aspirate(rsb_vol_2, rsb_res) p50.air_gap(5) @@ -912,23 +916,23 @@ def lib_cleanup_2() -> None: p50.air_gap(5) if REUSE_RSB_TIPS: p50.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") else: if trash_tips: p50.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p50.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") - ctx.delay( + protocol.delay( minutes=3, msg="Allow 3 minutes for incubation and liquid aggregation." ) - ctx.comment("****Move Samples to Magnet for Pelleting****") - ctx.move_labware(sample_plate_2, magblock, use_gripper=USE_GRIPPER) + protocol.comment("****Move Samples to Magnet for Pelleting****") + protocol.move_labware(sample_plate_2, magblock, use_gripper=USE_GRIPPER) - ctx.delay(minutes=2, msg="Please allow 2 minutes for beads to pellet.") + protocol.delay(minutes=2, msg="Please allow 2 minutes for beads to pellet.") p200.flow_rate.aspirate = 10 for i_int, (s, e) in enumerate(zip(samp_list_2, samples_flp)): @@ -939,10 +943,10 @@ def lib_cleanup_2() -> None: p50.air_gap(5) if trash_tips: p50.drop_tip() - ctx.comment("****Dropping Tip in Waste shoot****") + protocol.comment("****Dropping Tip in Waste shoot****") else: p50.return_tip() - ctx.comment("****Dropping Tip Back in Tip Box****") + protocol.comment("****Dropping Tip Back in Tip Box****") # Set Block Temp for Final Plate tc_mod.set_block_temperature(4) @@ -963,4 +967,6 @@ def lib_cleanup_2() -> None: waste2_res = waste2[0] end_probed_wells = [waste1_res, waste2_res] - helpers.find_liquid_height_of_all_wells(ctx, p50, end_probed_wells) + helpers.find_liquid_height_of_all_wells(protocol, p50, end_probed_wells) + if deactivate_mods: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/1_Simple Normalize Long Right.py b/abr-testing/abr_testing/protocols/active_protocols/1_Simple Normalize Long Right.py index 525a82c3095..6162e6ab34e 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/1_Simple Normalize Long Right.py +++ b/abr-testing/abr_testing/protocols/active_protocols/1_Simple Normalize Long Right.py @@ -4,10 +4,12 @@ ParameterContext, Labware, SINGLE, + ALL, InstrumentContext, Well, ) from abr_testing.protocols import helpers +from typing import List, Dict metadata = { "protocolName": "Simple Normalize Long with LPD and Single Tip", @@ -15,16 +17,14 @@ "source": "Protocol Library", } -requirements = { - "robotType": "Flex", - "apiLevel": "2.21", -} +requirements = {"robotType": "Flex", "apiLevel": "2.21"} def add_parameters(parameters: ParameterContext) -> None: """Parameters.""" helpers.create_single_pipette_mount_parameter(parameters) - helpers.create_tip_size_parameter(parameters) + helpers.create_csv_parameter(parameters) + helpers.create_dot_bottom_parameter(parameters) def get_next_tip_by_row(tip_rack: Labware, pipette: InstrumentContext) -> Well | None: @@ -79,149 +79,68 @@ def get_next_tip_by_row(tip_rack: Labware, pipette: InstrumentContext) -> Well | def run(protocol: ProtocolContext) -> None: """Protocol.""" - tip_type = protocol.params.tip_size # type: ignore[attr-defined] + dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] mount_pos = protocol.params.pipette_mount # type: ignore[attr-defined] + all_data = protocol.params.parameters_csv.parse_as_csv() # type: ignore[attr-defined] + data = all_data[1:] + helpers.comment_protocol_version(protocol, "01") # DECK SETUP AND LABWARE protocol.comment("THIS IS A NO MODULE RUN") - tiprack_x_1 = protocol.load_labware(tip_type, "D1") - tiprack_x_2 = protocol.load_labware(tip_type, "D2") - tiprack_x_3 = protocol.load_labware(tip_type, "B1") + tiprack_x_1 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "D1") + tiprack_x_2 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "D2") + tiprack_x_3 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "A1") sample_plate_1 = protocol.load_labware( "armadillo_96_wellplate_200ul_pcr_full_skirt", "D3" ) reservoir = protocol.load_labware("nest_12_reservoir_15ml", "B3") + waste_reservoir = protocol.load_labware( + "nest_1_reservoir_195ml", "C1", "Liquid Waste" + ) sample_plate_2 = protocol.load_labware( "armadillo_96_wellplate_200ul_pcr_full_skirt", "C2" ) sample_plate_3 = protocol.load_labware( "armadillo_96_wellplate_200ul_pcr_full_skirt", "B2" ) + sample_plate_4 = protocol.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "A2" + ) protocol.load_trash_bin("A3") - # reagent + # reagentg146 Dye_1 = reservoir["A1"] Dye_2 = reservoir["A2"] Dye_3 = reservoir["A3"] Diluent_1 = reservoir["A4"] Diluent_2 = reservoir["A5"] + Diluent_3 = reservoir["A6"] # pipette p1000 = protocol.load_instrument( "flex_8channel_1000", mount_pos, liquid_presence_detection=True ) + p1000_single = protocol.load_instrument( + "flex_1channel_1000", + "right", + liquid_presence_detection=True, + tip_racks=[tiprack_x_2, tiprack_x_3], + ) # LOAD LIQUIDS liquid_volumes = [675.0, 675.0, 675.0, 675.0, 675.0] - wells = [Dye_1, Dye_2, Dye_3, Diluent_1, Diluent_2] + wells = [Dye_1, Dye_2, Dye_3, Diluent_1, Diluent_2, Diluent_3] helpers.load_wells_with_water(protocol, wells, liquid_volumes) - + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Dye": [{"well": [Dye_1, Dye_2, Dye_3], "volume": 675.0}], + "Diluent": [{"well": [Diluent_1, Diluent_2, Diluent_3], "volume": 675.0}], + } current_rack = tiprack_x_1 # CONFIGURE SINGLE LAYOUT - p1000.configure_nozzle_layout( - style=SINGLE, start="H1", tip_racks=[tiprack_x_1, tiprack_x_2, tiprack_x_3] + p1000.configure_nozzle_layout(style=SINGLE, start="H1", tip_racks=[tiprack_x_1]) + helpers.find_liquid_height_of_loaded_liquids( + protocol, liquid_vols_and_wells, p1000_single ) - helpers.find_liquid_height_of_all_wells(protocol, p1000, wells) - sample_quant_csv = """ - sample_plate_1, Sample_well,DYE,DILUENT - sample_plate_1,A1,0,100 - sample_plate_1,B1,5,95 - sample_plate_1,C1,10,90 - sample_plate_1,D1,20,80 - sample_plate_1,E1,40,60 - sample_plate_1,F1,15,40 - sample_plate_1,G1,40,20 - sample_plate_1,H1,40,0 - sample_plate_1,A2,35,65 - sample_plate_1,B2,38,42 - sample_plate_1,C2,42,58 - sample_plate_1,D2,32,8 - sample_plate_1,E2,38,12 - sample_plate_1,F2,26,74 - sample_plate_1,G2,31,69 - sample_plate_1,H2,46,4 - sample_plate_1,A3,47,13 - sample_plate_1,B3,42,18 - sample_plate_1,C3,46,64 - sample_plate_1,D3,48,22 - sample_plate_1,E3,26,74 - sample_plate_1,F3,34,66 - sample_plate_1,G3,43,37 - sample_plate_1,H3,20,80 - sample_plate_1,A4,44,16 - sample_plate_1,B4,49,41 - sample_plate_1,C4,48,42 - sample_plate_1,D4,44,16 - sample_plate_1,E4,47,53 - sample_plate_1,F4,47,33 - sample_plate_1,G4,42,48 - sample_plate_1,H4,39,21 - sample_plate_1,A5,30,20 - sample_plate_1,B5,36,14 - sample_plate_1,C5,31,59 - sample_plate_1,D5,38,52 - sample_plate_1,E5,36,4 - sample_plate_1,F5,32,28 - sample_plate_1,G5,35,55 - sample_plate_1,H5,39,1 - sample_plate_1,A6,31,59 - sample_plate_1,B6,20,80 - sample_plate_1,C6,38,2 - sample_plate_1,D6,34,46 - sample_plate_1,E6,30,70 - sample_plate_1,F6,32,58 - sample_plate_1,G6,21,79 - sample_plate_1,H6,38,52 - sample_plate_1,A7,33,27 - sample_plate_1,B7,34,16 - sample_plate_1,C7,40,60 - sample_plate_1,D7,34,26 - sample_plate_1,E7,30,20 - sample_plate_1,F7,44,56 - sample_plate_1,G7,26,74 - sample_plate_1,H7,45,55 - sample_plate_1,A8,39,1 - sample_plate_1,B8,38,2 - sample_plate_1,C8,34,66 - sample_plate_1,D8,39,11 - sample_plate_1,E8,46,54 - sample_plate_1,F8,37,63 - sample_plate_1,G8,38,42 - sample_plate_1,H8,34,66 - sample_plate_1,A9,44,56 - sample_plate_1,B9,39,11 - sample_plate_1,C9,30,70 - sample_plate_1,D9,37,33 - sample_plate_1,E9,46,54 - sample_plate_1,F9,39,21 - sample_plate_1,G9,29,41 - sample_plate_1,H9,23,77 - sample_plate_1,A10,26,74 - sample_plate_1,B10,39,1 - sample_plate_1,C10,31,49 - sample_plate_1,D10,38,62 - sample_plate_1,E10,29,1 - sample_plate_1,F10,21,79 - sample_plate_1,G10,29,41 - sample_plate_1,H10,28,42 - sample_plate_1,A11,15,55 - sample_plate_1,B11,28,72 - sample_plate_1,C11,11,49 - sample_plate_1,D11,34,66 - sample_plate_1,E11,27,73 - sample_plate_1,F11,30,40 - sample_plate_1,G11,33,67 - sample_plate_1,H11,31,39 - sample_plate_1,A12,39,31 - sample_plate_1,B12,47,53 - sample_plate_1,C12,46,54 - sample_plate_1,D12,13,7 - sample_plate_1,E12,34,46 - sample_plate_1,F12,45,35 - sample_plate_1,G12,28,42 - sample_plate_1,H12,37,63 - """ - data = [r.split(",") for r in sample_quant_csv.strip().splitlines() if r][1:] for X in range(1): protocol.comment("==============================================") protocol.comment("Adding Dye Sample Plate 1") @@ -232,8 +151,8 @@ def run(protocol: ProtocolContext) -> None: well = get_next_tip_by_row(current_rack, p1000) p1000.pick_up_tip(well) while current < len(data): - CurrentWell = str(data[current][1]) - DyeVol = float(data[current][2]) + CurrentWell = str(data[current][0]) + DyeVol = float(data[current][1]) if DyeVol != 0 and DyeVol < 100: p1000.liquid_presence_detection = False p1000.transfer( @@ -245,7 +164,7 @@ def run(protocol: ProtocolContext) -> None: if DyeVol > 20: wells.append(sample_plate_1.wells_by_name()[CurrentWell]) current += 1 - p1000.blow_out() + p1000.blow_out(location=waste_reservoir["A1"]) p1000.touch_tip() p1000.drop_tip() p1000.liquid_presence_detection = True @@ -256,34 +175,34 @@ def run(protocol: ProtocolContext) -> None: current = 0 while current < len(data): - CurrentWell = str(data[current][1]) + CurrentWell = str(data[current][0]) DilutionVol = float(data[current][2]) if DilutionVol != 0 and DilutionVol < 100: well = get_next_tip_by_row(current_rack, p1000) p1000.pick_up_tip(well) - p1000.aspirate(DilutionVol, Diluent_1.bottom(z=2)) + p1000.aspirate(DilutionVol, Diluent_1.bottom(z=dot_bottom)) p1000.dispense( DilutionVol, sample_plate_1.wells_by_name()[CurrentWell].top(z=0.2) ) if DilutionVol > 20: wells.append(sample_plate_1.wells_by_name()[CurrentWell]) - p1000.blow_out() + p1000.blow_out(location=waste_reservoir["A1"]) p1000.touch_tip() p1000.drop_tip() current += 1 + protocol.comment("Changing pipette configuration to 8ch.") + protocol.comment("==============================================") protocol.comment("Adding Dye Sample Plate 2") protocol.comment("==============================================") - current = 0 - well = get_next_tip_by_row(tiprack_x_2, p1000) - p1000.pick_up_tip(well) + p1000_single.pick_up_tip() while current < len(data): - CurrentWell = str(data[current][1]) - DyeVol = float(data[current][2]) + CurrentWell = str(data[current][0]) + DyeVol = float(data[current][1]) if DyeVol != 0 and DyeVol < 100: - p1000.transfer( + p1000_single.transfer( DyeVol, Dye_2.bottom(z=2), sample_plate_2.wells_by_name()[CurrentWell].top(z=1), @@ -292,9 +211,9 @@ def run(protocol: ProtocolContext) -> None: if DyeVol > 20: wells.append(sample_plate_2.wells_by_name()[CurrentWell]) current += 1 - p1000.blow_out() - p1000.touch_tip() - p1000.drop_tip() + p1000_single.blow_out(location=waste_reservoir["A1"]) + p1000_single.touch_tip() + p1000_single.return_tip() protocol.comment("==============================================") protocol.comment("Adding Diluent Sample Plate 2") @@ -302,20 +221,19 @@ def run(protocol: ProtocolContext) -> None: current = 0 while current < len(data): - CurrentWell = str(data[current][1]) + CurrentWell = str(data[current][0]) DilutionVol = float(data[current][2]) if DilutionVol != 0 and DilutionVol < 100: - well = get_next_tip_by_row(tiprack_x_2, p1000) - p1000.pick_up_tip(well) - p1000.aspirate(DilutionVol, Diluent_2.bottom(z=2)) - p1000.dispense( + p1000_single.pick_up_tip() + p1000_single.aspirate(DilutionVol, Diluent_2.bottom(z=dot_bottom)) + p1000_single.dispense( DilutionVol, sample_plate_2.wells_by_name()[CurrentWell].top(z=0.2) ) if DilutionVol > 20: wells.append(sample_plate_2.wells_by_name()[CurrentWell]) - p1000.blow_out() - p1000.touch_tip() - p1000.drop_tip() + p1000_single.blow_out(location=waste_reservoir["A1"]) + p1000_single.touch_tip() + p1000_single.return_tip() current += 1 protocol.comment("==============================================") @@ -323,14 +241,13 @@ def run(protocol: ProtocolContext) -> None: protocol.comment("==============================================") current = 0 - well = get_next_tip_by_row(tiprack_x_3, p1000) - p1000.pick_up_tip(well) + p1000_single.pick_up_tip() while current < len(data): - CurrentWell = str(data[current][1]) - DyeVol = float(data[current][2]) + CurrentWell = str(data[current][0]) + DyeVol = float(data[current][1]) if DyeVol != 0 and DyeVol < 100: - p1000.liquid_presence_detection = False - p1000.transfer( + p1000_single.liquid_presence_detection = False + p1000_single.transfer( DyeVol, Dye_3.bottom(z=2), sample_plate_3.wells_by_name()[CurrentWell].top(z=1), @@ -341,14 +258,85 @@ def run(protocol: ProtocolContext) -> None: if DyeVol > 20: wells.append(sample_plate_3.wells_by_name()[CurrentWell]) current += 1 - p1000.liquid_presence_detection = True - p1000.blow_out() - p1000.touch_tip() - p1000.drop_tip() + p1000_single.liquid_presence_detection = True + p1000_single.blow_out(location=waste_reservoir["A1"]) + p1000_single.touch_tip() + p1000_single.return_tip() protocol.comment("==============================================") protocol.comment("Adding Diluent Sample Plate 3") protocol.comment("==============================================") + current = 0 + while current < len(data): + CurrentWell = str(data[current][0]) + DilutionVol = float(data[current][2]) + if DilutionVol != 0 and DilutionVol < 100: + p1000_single.pick_up_tip() + p1000_single.aspirate(DilutionVol, Diluent_3.bottom(z=dot_bottom)) + p1000_single.dispense( + DilutionVol, sample_plate_3.wells_by_name()[CurrentWell].top(z=0.2) + ) + if DilutionVol > 20: + wells.append(sample_plate_3.wells_by_name()[CurrentWell]) + p1000_single.blow_out(location=waste_reservoir["A1"]) + p1000_single.touch_tip() + p1000_single.return_tip() + current += 1 + protocol.comment("==============================================") + protocol.comment("Adding Dye Sample Plate 4") + protocol.comment("==============================================") + p1000_single.reset_tipracks() current = 0 - # Probe heights - helpers.find_liquid_height_of_all_wells(protocol, p1000, wells) + p1000_single.pick_up_tip() + while current < len(data): + CurrentWell = str(data[current][0]) + DyeVol = float(data[current][1]) + if DyeVol != 0 and DyeVol < 100: + p1000_single.liquid_presence_detection = False + p1000_single.transfer( + DyeVol, + Dye_3.bottom(z=2), + sample_plate_4.wells_by_name()[CurrentWell].top(z=1), + blow_out=True, + blowout_location="destination well", + new_tip="never", + ) + if DyeVol > 20: + wells.append(sample_plate_4.wells_by_name()[CurrentWell]) + current += 1 + p1000_single.liquid_presence_detection = True + p1000_single.blow_out(location=waste_reservoir["A1"]) + p1000_single.touch_tip() + p1000_single.return_tip() + protocol.comment("==============================================") + protocol.comment("Adding Diluent Sample Plate 4") + protocol.comment("==============================================") + current = 0 + while current < len(data): + CurrentWell = str(data[current][0]) + DilutionVol = float(data[current][2]) + if DilutionVol != 0 and DilutionVol < 100: + p1000_single.pick_up_tip() + p1000_single.aspirate(DilutionVol, Diluent_3.bottom(z=dot_bottom)) + p1000_single.dispense( + DilutionVol, sample_plate_4.wells_by_name()[CurrentWell].top(z=0.2) + ) + if DilutionVol > 20: + wells.append(sample_plate_4.wells_by_name()[CurrentWell]) + p1000_single.blow_out(location=waste_reservoir["A1"]) + p1000_single.touch_tip() + p1000_single.return_tip() + current += 1 + + current = 0 + # Probe heights + p1000.configure_nozzle_layout(style=ALL, tip_racks=[tiprack_x_3]) + helpers.clean_up_plates( + p1000, + [sample_plate_1, sample_plate_2, sample_plate_3, sample_plate_4], + waste_reservoir["A1"], + 200, + ) + helpers.find_liquid_height_of_all_wells( + protocol, p1000_single, [waste_reservoir["A1"]] + ) diff --git a/abr-testing/abr_testing/protocols/active_protocols/2_BMS_PCR_Protocol.py b/abr-testing/abr_testing/protocols/active_protocols/2_BMS_PCR_Protocol.py index 24e7358f6e1..3b11b51b7fe 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/2_BMS_PCR_Protocol.py +++ b/abr-testing/abr_testing/protocols/active_protocols/2_BMS_PCR_Protocol.py @@ -5,7 +5,7 @@ ThermocyclerContext, TemperatureModuleContext, ) -from opentrons.protocol_api import SINGLE, Well +from opentrons.protocol_api import SINGLE, Well, ALL from abr_testing.protocols import helpers from typing import List, Dict @@ -14,7 +14,7 @@ "protocolName": "PCR Protocol with TC Auto Sealing Lid", "author": "Rami Farawi None: @@ -23,81 +23,79 @@ def add_parameters(parameters: ParameterContext) -> None: helpers.create_disposable_lid_parameter(parameters) helpers.create_csv_parameter(parameters) helpers.create_tc_lid_deck_riser_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) -def run(ctx: ProtocolContext) -> None: +def run(protocol: ProtocolContext) -> None: """Protocol.""" - pipette_mount = ctx.params.pipette_mount # type: ignore[attr-defined] - disposable_lid = ctx.params.disposable_lid # type: ignore[attr-defined] - parsed_csv = ctx.params.parameters_csv.parse_as_csv() # type: ignore[attr-defined] - deck_riser = ctx.params.deck_riser # type: ignore[attr-defined] + pipette_mount = protocol.params.pipette_mount # type: ignore[attr-defined] + disposable_lid = protocol.params.disposable_lid # type: ignore[attr-defined] + parsed_csv = protocol.params.parameters_csv.parse_as_csv() # type: ignore[attr-defined] + deck_riser = protocol.params.deck_riser # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") rxn_vol = 50 real_mode = True # DECK SETUP AND LABWARE - tc_mod: ThermocyclerContext = ctx.load_module( + tc_mod: ThermocyclerContext = protocol.load_module( helpers.tc_str ) # type: ignore[assignment] tc_mod.open_lid() tc_mod.set_lid_temperature(105) - temp_mod: TemperatureModuleContext = ctx.load_module( + temp_mod: TemperatureModuleContext = protocol.load_module( helpers.temp_str, location="D3" ) # type: ignore[assignment] reagent_rack = temp_mod.load_labware( - "opentrons_24_aluminumblock_nest_1.5ml_snapcap" - ) # check if 2mL - - dest_plate = tc_mod.load_labware( - "opentrons_96_wellplate_200ul_pcr_full_skirt" - ) # do I change this to tough plate if they run pcr? - - source_plate = ctx.load_labware( - "opentrons_96_wellplate_200ul_pcr_full_skirt", location="D1" - ) # do I change this to their plate? + "opentrons_24_aluminumblock_nest_1.5ml_snapcap", "Reagent Rack" + ) + dest_plate_1 = tc_mod.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", "Destination Plate 1" + ) + source_plate_1 = protocol.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", "D1", "DNA Plate 1" + ) + waste = protocol.load_labware("nest_1_reservoir_195ml", "D2", "Liquid Waste") + liquid_waste = waste["A1"] tiprack_50 = [ - ctx.load_labware("opentrons_flex_96_tiprack_50ul", slot) for slot in [8, 9] + protocol.load_labware("opentrons_flex_96_tiprack_50ul", slot) for slot in [8, 9] ] # Opentrons tough pcr auto sealing lids if disposable_lid: - unused_lids = helpers.load_disposable_lids(ctx, 3, ["C3"], deck_riser) + unused_lids = helpers.load_disposable_lids(protocol, 3, ["C3"], deck_riser) used_lids: List[Labware] = [] # LOAD PIPETTES - p50 = ctx.load_instrument( + p50 = protocol.load_instrument( "flex_8channel_50", pipette_mount, tip_racks=tiprack_50, liquid_presence_detection=True, ) p50.configure_nozzle_layout(style=SINGLE, start="A1", tip_racks=tiprack_50) - ctx.load_trash_bin("A3") + protocol.load_trash_bin("A3") temp_mod.set_temperature(4) # LOAD LIQUIDS water: Well = reagent_rack["B1"] mmx_pic: List[Well] = reagent_rack.rows()[0] - dna_pic: List[Well] = source_plate.wells() + dna_pic: List[Well] = source_plate_1.wells() liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { - "Water": [{"well": water, "volume": 1500.0}], - "Mastermix": [{"well": mmx_pic, "volume": 1500.0}], - "DNA": [{"well": dna_pic, "volume": 50.0}], + "Water": [{"well": water, "volume": 500.0}], + "Mastermix": [{"well": mmx_pic, "volume": 500.0}], + "DNA": [{"well": dna_pic, "volume": 100.0}], } - helpers.load_wells_with_custom_liquids(ctx, liquid_vols_and_wells) - wells_to_probe = [[water], mmx_pic, dna_pic] - wells_to_probe_flattened = [ - well for list_of_wells in wells_to_probe for well in list_of_wells - ] - helpers.find_liquid_height_of_all_wells(ctx, p50, wells_to_probe_flattened) + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, p50) # adding water - ctx.comment("\n\n----------ADDING WATER----------\n") + protocol.comment("\n\n----------ADDING WATER----------\n") p50.pick_up_tip() - # p50.aspirate(40, water) # prewet - # p50.dispense(40, water) + p50.aspirate(40, water) # prewet + p50.dispense(40, water) parsed_csv = parsed_csv[1:] num_of_rows = len(parsed_csv) for row_index in range(num_of_rows): @@ -112,13 +110,13 @@ def run(ctx: ProtocolContext) -> None: p50.configure_for_volume(water_vol) p50.aspirate(water_vol, water) - p50.dispense(water_vol, dest_plate[dest_well], rate=0.5) + p50.dispense(water_vol, dest_plate_1[dest_well], rate=0.5) p50.configure_for_volume(50) - # p50.blow_out() + p50.blow_out() p50.drop_tip() # adding Mastermix - ctx.comment("\n\n----------ADDING MASTERMIX----------\n") + protocol.comment("\n\n----------ADDING MASTERMIX----------\n") for i, row in enumerate(parsed_csv): p50.pick_up_tip() mmx_vol = row[3] @@ -144,8 +142,8 @@ def run(ctx: ProtocolContext) -> None: break p50.configure_for_volume(mmx_vol) p50.aspirate(mmx_vol, reagent_rack[mmx_tube]) - p50.dispense(mmx_vol, dest_plate[dest_well].top()) - ctx.delay(seconds=2) + p50.dispense(mmx_vol, dest_plate_1[dest_well].top()) + protocol.delay(seconds=2) p50.blow_out() p50.touch_tip() p50.configure_for_volume(50) @@ -154,7 +152,7 @@ def run(ctx: ProtocolContext) -> None: p50.drop_tip() # adding DNA - ctx.comment("\n\n----------ADDING DNA----------\n") + protocol.comment("\n\n----------ADDING DNA----------\n") for row in parsed_csv: dna_vol = row[2] if dna_vol.lower() == "x": @@ -168,29 +166,28 @@ def run(ctx: ProtocolContext) -> None: if dna_vol == 0: break p50.configure_for_volume(dna_vol) - p50.aspirate(dna_vol, source_plate[dest_and_source_well]) - p50.dispense(dna_vol, dest_plate[dest_and_source_well], rate=0.5) + p50.aspirate(dna_vol, source_plate_1[dest_and_source_well]) + p50.dispense(dna_vol, dest_plate_1[dest_and_source_well], rate=0.5) p50.mix( 10, 0.7 * rxn_vol if 0.7 * rxn_vol < 30 else 30, - dest_plate[dest_and_source_well], + dest_plate_1[dest_and_source_well], ) p50.drop_tip() p50.configure_for_volume(50) - wells_to_probe_flattened.append(dest_plate[dest_well]) - ctx.comment("\n\n-----------Running PCR------------\n") + protocol.comment("\n\n-----------Running PCR------------\n") if real_mode: if disposable_lid: lid_on_plate, unused_lids, used_lids = helpers.use_disposable_lid_with_tc( - ctx, unused_lids, used_lids, dest_plate, tc_mod + protocol, unused_lids, used_lids, dest_plate_1, tc_mod ) else: tc_mod.close_lid() helpers.perform_pcr( - ctx, + protocol, tc_mod, initial_denature_time_sec=120, denaturation_time_sec=10, @@ -205,9 +202,16 @@ def run(ctx: ProtocolContext) -> None: tc_mod.open_lid() if disposable_lid: if len(used_lids) <= 1: - ctx.move_labware(lid_on_plate, "C2", use_gripper=True) + protocol.move_labware(lid_on_plate, "C2", use_gripper=True) else: - ctx.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) p50.drop_tip() p50.configure_nozzle_layout(style=SINGLE, start="A1", tip_racks=tiprack_50) - helpers.find_liquid_height_of_all_wells(ctx, p50, wells_to_probe_flattened) + mmx_pic.append(water) + # Empty plates into liquid waste + p50.configure_nozzle_layout(style=ALL, tip_racks=tiprack_50) + helpers.clean_up_plates(p50, [source_plate_1, dest_plate_1], liquid_waste, 50) + # Probe liquid waste + helpers.find_liquid_height_of_all_wells(protocol, p50, [liquid_waste]) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py b/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py index 66db85468f4..9916ef7f7fc 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py +++ b/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py @@ -1,5 +1,10 @@ """Tartrazine Protocol.""" -from opentrons.protocol_api import ProtocolContext, ParameterContext, Well +from opentrons.protocol_api import ( + ProtocolContext, + ParameterContext, + Well, + InstrumentContext, +) from abr_testing.protocols import helpers from opentrons.protocol_api.module_contexts import ( AbsorbanceReaderContext, @@ -23,101 +28,172 @@ def add_parameters(parameters: ParameterContext) -> None: parameters.add_int( variable_name="number_of_plates", display_name="Number of Plates", - default=4, + default=1, minimum=1, maximum=4, ) + helpers.create_channel_parameter(parameters) + helpers.create_plate_reader_compatible_labware_parameter(parameters) -def run(ctx: ProtocolContext) -> None: +def run(protocol: ProtocolContext) -> None: """Protocol.""" - number_of_plates = ctx.params.number_of_plates # type: ignore [attr-defined] + # Load parameters + number_of_plates = protocol.params.number_of_plates # type: ignore [attr-defined] + channels = protocol.params.channels # type: ignore [attr-defined] + plate_type = protocol.params.labware_plate_reader_compatible # type: ignore [attr-defined] + + helpers.comment_protocol_version(protocol, "01") # Plate Reader - plate_reader: AbsorbanceReaderContext = ctx.load_module( + plate_reader: AbsorbanceReaderContext = protocol.load_module( helpers.abs_mod_str, "A3" ) # type: ignore[assignment] - hs: HeaterShakerContext = ctx.load_module(helpers.hs_str, "A1") # type: ignore[assignment] - hs_adapter = hs.load_adapter("opentrons_universal_flat_adapter") - tube_rack = ctx.load_labware( - "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", "C2", "Reagent Tube" - ) - tartrazine_tube = tube_rack["A3"] - water_tube_1 = tube_rack["A4"] - water_tube_2 = tube_rack["B3"] - sample_plate_1 = ctx.load_labware( - "corning_96_wellplate_360ul_flat", "D1", "Sample Plate 1" - ) - sample_plate_2 = ctx.load_labware( - "corning_96_wellplate_360ul_flat", "D2", "Sample Plate 2" + hs: HeaterShakerContext = protocol.load_module(helpers.hs_str, "A1") # type: ignore[assignment] + # Load Plates based off of number_of_plates parameter + available_deck_slots = ["D1", "D2", "C1", "B1"] + sample_plate_list = [] + for plate_num, slot in zip(range(number_of_plates), available_deck_slots): + plate = protocol.load_labware(plate_type, slot, f"Sample Plate {plate_num + 1}") + sample_plate_list.append(plate) + available_tip_rack_slots = ["D3", "C3", "B3", "B2"] + # LOAD PIPETTES AND TIP RACKS + # 50 CHANNEL + tip_racks_50 = [] + for plate_num, slot_2 in zip(range(number_of_plates), available_tip_rack_slots): + tiprack_50 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", slot_2) + tip_racks_50.append(tiprack_50) + p50 = protocol.load_instrument( + f"flex_{channels}_50", "left", tip_racks=tip_racks_50 ) - sample_plate_3 = ctx.load_labware( - "corning_96_wellplate_360ul_flat", "C1", "Sample Plate 3" - ) - sample_plate_4 = ctx.load_labware( - "corning_96_wellplate_360ul_flat", "B1", "Sample Plate 4" + # 1000 CHANNEL + tiprack_1000_1 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "A2") + p1000 = protocol.load_instrument( + f"flex_{channels}_1000", "right", tip_racks=[tiprack_1000_1] ) + # DETERMINE RESERVOIR BASED OFF # OF PIPETTE CHANNELS + # 1 CHANNEL = TUBE RACK + if p50.active_channels == 1: + reservoir = protocol.load_labware( + "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", "C2", "Reservoir" + ) + water_max_vol = reservoir["A3"].max_volume - 500 + reservoir_wells = reservoir.wells()[6:] # Skip first 4 bc they are 15ml + else: + # 8 CHANNEL = 12 WELL RESERVOIR + reservoir = protocol.load_labware("nest_12_reservoir_15ml", "C2", "Reservoir") + water_max_vol = reservoir["A1"].max_volume - 500 + reservoir_wells = reservoir.wells()[ + 1: + ] # Skip A1 as it's reserved for tartrazine - sample_plate_list = [sample_plate_1, sample_plate_2, sample_plate_3, sample_plate_4] - tiprack_50_1 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "D3") - tiprack_50_2 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "C3") - tiprack_50_3 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "B3") - tiprack_1000_1 = ctx.load_labware("opentrons_flex_96_tiprack_1000ul", "A2") - tip_racks = [tiprack_50_1, tiprack_50_2, tiprack_50_3] + # LABEL RESERVOIR WELLS AND DETERMINE NEEDED LIQUID + tartrazine_well = reservoir["A1"] + # NEEDED TARTRAZINE + needed_tartrazine: float = ( + float(number_of_plates) * 96.0 + ) * 10.0 + 1000.0 # loading extra as a safety factor + # NEEDED WATER + needed_water: float = ( + float(number_of_plates) * 96.0 * 250 + ) # loading extra as a safety factor + # CALCULATING NEEDED # OF WATER WELLS + needed_wells = round(needed_water / water_max_vol) + water_wells = [] + for i in range(needed_wells + 1): + water_wells.append(reservoir_wells[i]) - # Pipette - p50 = ctx.load_instrument("flex_1channel_50", "left", tip_racks=tip_racks) - p1000 = ctx.load_instrument( - "flex_1channel_1000", "right", tip_racks=[tiprack_1000_1] - ) + def _mix_tartrazine(pipette: InstrumentContext, well_to_probe: Well) -> None: + """Mix Tartrazine.""" + # Mix step is needed to ensure tartrazine does not settle between plates. + pipette.pick_up_tip() + top_of_tartrazine = helpers.find_liquid_height(pipette, well_to_probe) + for i in range(20): + p50.aspirate(1, well_to_probe.bottom(z=1)) + p50.dispense(1, well_to_probe.bottom(z=top_of_tartrazine + 1)) + pipette.return_tip() - # Probe wells + # LOAD LIQUIDS AND PROBE WELLS liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { - "Tartrazine": [{"well": tartrazine_tube, "volume": 45.0}], - "Water": [{"well": [water_tube_1, water_tube_2], "volume": 45.0}], + "Tartrazine": [{"well": tartrazine_well, "volume": needed_tartrazine}], + "Water": [{"well": water_wells, "volume": water_max_vol}], } - helpers.find_liquid_height_of_loaded_liquids(ctx, liquid_vols_and_wells, p50) - + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, p50) + tip_count = 1 * p50.active_channels # number of 50 ul tip uses. + p50.reset_tipracks() i = 0 all_percent_error_dict = {} cv_dict = {} - vol = 0.0 - tip_count = 0 + vol = 0.0 # counter to track available water volume + water_tip_count = 0 * p1000.active_channels # number of 1000 ul tip uses + well_num = 0 # index of well being used for water for sample_plate in sample_plate_list[:number_of_plates]: - deck_locations = ["D1", "D2", "C1", "B1"] - p1000.pick_up_tip() - for well in sample_plate.wells(): - if vol < 45000: - tube_of_choice = water_tube_1 + return_location = sample_plate.parent + # Mix Tartrazine to ensure no settling as occurred + _mix_tartrazine(p50, tartrazine_well) + tip_count += 1 * p50.active_channels + # Determine list of wells to probe + if p50.active_channels == 1: + well_list = sample_plate.wells() + elif p50.active_channels == 8: + well_list = sample_plate.rows()[0] + for well in well_list: + p1000.pick_up_tip() + # Determine which water well to aspirate from. + if vol < water_max_vol - 6000: + well_of_choice = water_wells[well_num] else: - tube_of_choice = water_tube_2 + well_num += 1 + well_of_choice = water_wells[well_num] + vol = 0.0 p50.pick_up_tip() - p1000.aspirate(190, tube_of_choice) - p1000.air_gap(5) - p1000.dispense(5, well.top()) + p1000.aspirate(190, well_of_choice) + p1000.air_gap(10) + p1000.dispense(10, well.top()) p1000.dispense(190, well) - vol += 190 - height = helpers.find_liquid_height(p50, tartrazine_tube) - p50.aspirate(10, tartrazine_tube.bottom(z=height)) + # Two blow outs ensures water is completely removed from pipette + p1000.blow_out(well.top()) + protocol.delay(minutes=0.1) + p1000.blow_out(well.top()) + vol += 190 * p1000.active_channels + # Probe to find liquid height of tartrazine to ensure correct amount is aspirated + height = helpers.find_liquid_height(p50, tartrazine_well) + if height <= 0.0: + # If a negative tartrazine height is found, + # the protocol will pause, prompt a refill, and reprobe. + protocol.pause("Fill tartrazine") + height = helpers.find_liquid_height(p50, tartrazine_well) + p50.aspirate(10, tartrazine_well.bottom(z=height), rate=0.15) p50.air_gap(5) p50.dispense(5, well.top()) - p50.dispense(10, well.bottom(z=0.5)) + p50.dispense(10, well.bottom(z=0.5), rate=0.15) + p50.blow_out() + protocol.delay(minutes=0.1) p50.blow_out() p50.return_tip() - tip_count += 1 - if tip_count >= (96 * 3): + tip_count += p50.active_channels + if tip_count >= (96 * len(tip_racks_50)): p50.reset_tipracks() - p1000.return_tip() - helpers.move_labware_to_hs(ctx, sample_plate, hs, hs_adapter) - helpers.set_hs_speed(ctx, hs, 1500, 2.0, True) + tip_count = 0 + p1000.return_tip() + water_tip_count += p1000.active_channels + if water_tip_count >= 96: + p1000.reset_tipracks() + water_tip_count = 0 + # Move labware to heater shaker to be mixed + helpers.move_labware_to_hs(protocol, sample_plate, hs, hs) + helpers.set_hs_speed(protocol, hs, 1500, 2.0, True) hs.open_labware_latch() + # Initialize plate reader plate_reader.close_lid() plate_reader.initialize("single", [450]) plate_reader.open_lid() - ctx.move_labware(sample_plate, plate_reader, use_gripper=True) + # Move sample plate into plate reader + protocol.move_labware(sample_plate, plate_reader, use_gripper=True) sample_plate_name = "sample plate_" + str(i + 1) csv_string = sample_plate_name + "_" + str(datetime.now()) plate_reader.close_lid() result = plate_reader.read(csv_string) + # Calculate CV and % error of expected value. for wavelength in result: dict_of_wells = result[wavelength] readings_and_wells = dict_of_wells.items() @@ -145,11 +221,15 @@ def run(ctx: ProtocolContext) -> None: "SD": standard_deviation, "Avg Percent Error": avg_percent_error, } + # Move Plate back to original location all_percent_error_dict[sample_plate_name] = percent_error_dict plate_reader.open_lid() - ctx.move_labware(sample_plate, deck_locations[i], use_gripper=True) + protocol.comment( + f"------plate {sample_plate}. {cv_dict[sample_plate_name]}------" + ) + protocol.move_labware(sample_plate, return_location, use_gripper=True) i += 1 # Print percent error dictionary - ctx.comment("Percent Error: " + str(all_percent_error_dict)) + protocol.comment("Percent Error: " + str(all_percent_error_dict)) # Print cv dictionary - ctx.comment("Plate Reader result: " + str(cv_dict)) + protocol.comment("Plate Reader Result: " + str(cv_dict)) diff --git a/abr-testing/abr_testing/protocols/active_protocols/4_Illumina DNA Enrichment.py b/abr-testing/abr_testing/protocols/active_protocols/4_Illumina DNA Enrichment.py new file mode 100644 index 00000000000..ff9a9807c92 --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/4_Illumina DNA Enrichment.py @@ -0,0 +1,1016 @@ +"""DVT1ABR4: Illumina DNA Enrichment.""" +from opentrons.protocol_api import ( + ParameterContext, + ProtocolContext, + Labware, + Well, + InstrumentContext, +) +from opentrons import types +from abr_testing.protocols import helpers +from opentrons.protocol_api.module_contexts import ( + HeaterShakerContext, + MagneticBlockContext, + ThermocyclerContext, + TemperatureModuleContext, +) +from opentrons.hardware_control.modules.types import ThermocyclerStep +from typing import List, Dict + + +metadata = { + "protocolName": "Illumina DNA Enrichment v4 with TC Auto Sealing Lid", + "author": "Opentrons ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + +# SCRIPT SETTINGS +DRYRUN = False # True = skip incubation times, shorten mix, for testing purposes +USE_GRIPPER = True # True = Uses Gripper, False = Manual Move +TIP_TRASH = False # True = Used tips go in Trash, False = Used tips go back into rack +HYBRID_PAUSE = True # True = sets a pause on the Hybridization + +# PROTOCOL SETTINGS +COLUMNS = 4 # 1-4 +HYBRIDDECK = True +HYBRIDTIME = 1.6 # Hours + +# PROTOCOL BLOCKS +STEP_VOLPOOL = 0 +STEP_HYB = 0 +STEP_CAPTURE = 1 +STEP_WASH = 1 +STEP_PCR = 1 +STEP_PCRDECK = 1 +STEP_CLEANUP = 1 + +p200_tips = 0 +p50_tips = 0 +total_waste_volume = 0.0 + + +RUN = 1 + + +def add_parameters(parameters: ParameterContext) -> None: + """Add parameters.""" + helpers.create_hs_speed_parameter(parameters) + helpers.create_dot_bottom_parameter(parameters) + helpers.create_disposable_lid_parameter(parameters) + helpers.create_tc_lid_deck_riser_parameter(parameters) + helpers.create_disposable_lid_trash_location(parameters) + helpers.create_deactivate_modules_parameter(parameters) + + +def run(protocol: ProtocolContext) -> None: + """Protocol.""" + heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] + dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + disposable_lid = protocol.params.disposable_lid # type: ignore[attr-defined] + deck_riser = protocol.params.deck_riser # type: ignore[attr-defined] + trash_lid = protocol.params.trash_lid # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") + + unused_lids: List[Labware] = [] + used_lids: List[Labware] = [] + global p200_tips + global p50_tips + + protocol.comment("THIS IS A DRY RUN") if DRYRUN else protocol.comment( + "THIS IS A REACTION RUN" + ) + protocol.comment("USED TIPS WILL GO IN TRASH") if TIP_TRASH else protocol.comment( + "USED TIPS WILL BE RE-RACKED" + ) + + # DECK SETUP AND LABWARE + # ========== FIRST ROW =========== + heatershaker: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "1" + ) # type: ignore[assignment] + heatershaker.close_labware_latch() + sample_plate_2 = heatershaker.load_labware( + "thermoscientificnunc_96_wellplate_1300ul" + ) + reservoir = protocol.load_labware("nest_96_wellplate_2ml_deep", "2", "Liquid Waste") + temp_block: TemperatureModuleContext = protocol.load_module( + helpers.temp_str, "3" + ) # type: ignore[assignment] + reagent_plate, temp_adapter = helpers.load_temp_adapter_and_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", temp_block, "Reagent Plate" + ) + # ========== SECOND ROW ========== + MAG_PLATE_SLOT: MagneticBlockContext = protocol.load_module( + helpers.mag_str, "C1" + ) # type: ignore[assignment] + tiprack_200_1 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "5") + tiprack_50_1 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "6") + # Opentrons tough pcr auto sealing lids + if disposable_lid: + unused_lids = helpers.load_disposable_lids(protocol, 3, ["C4"], deck_riser) + # ========== THIRD ROW =========== + thermocycler: ThermocyclerContext = protocol.load_module( + helpers.tc_str + ) # type: ignore[assignment] + sample_plate_1 = thermocycler.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt" + ) + thermocycler.open_lid() + tiprack_200_2 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "8") + tiprack_50_2 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "9") + # ========== FOURTH ROW ========== + tiprack_200_3 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "11") + trash_bin = protocol.load_trash_bin("A3") + # reagent + AMPure = reservoir["A1"] + SMB = reservoir["A2"] + + EtOH = reservoir["A4"] + RSB = reservoir["A5"] + Liquid_trash_well_1 = reservoir["A9"] + Liquid_trash_well_2 = reservoir["A10"] + Liquid_trash_well_3 = reservoir["A11"] + Liquid_trash_well_4 = reservoir["A12"] + liquid_trash_list = { + Liquid_trash_well_1: 0.0, + Liquid_trash_well_2: 0.0, + Liquid_trash_well_3: 0.0, + Liquid_trash_well_4: 0.0, + } + + def trash_liquid( + protocol: ProtocolContext, + pipette: InstrumentContext, + vol_to_trash: float, + liquid_trash_list: Dict[Well, float], + ) -> None: + """Determine which wells to use as liquid waste.""" + remaining_volume = vol_to_trash + max_capacity = 1500.0 + # Determine liquid waste location depending on current total volume + # Distribute the liquid volume sequentially + for well, current_volume in liquid_trash_list.items(): + if remaining_volume <= 0.0: + break + available_capacity = max_capacity - current_volume + if available_capacity < remaining_volume: + continue + pipette.dispense(remaining_volume, well.top()) + protocol.delay(minutes=0.1) + pipette.blow_out(well.top()) + liquid_trash_list[well] += remaining_volume + if pipette.current_volume <= 0.0: + break + + # Will Be distributed during the protocol + EEW_1 = sample_plate_2.wells_by_name()["A9"] + EEW_2 = sample_plate_2.wells_by_name()["A10"] + EEW_3 = sample_plate_2.wells_by_name()["A11"] + EEW_4 = sample_plate_2.wells_by_name()["A12"] + + NHB2 = reagent_plate.wells_by_name()["A1"] + Panel = reagent_plate.wells_by_name()["A2"] + EHB2 = reagent_plate.wells_by_name()["A3"] + Elute = reagent_plate.wells_by_name()["A4"] + ET2 = reagent_plate.wells_by_name()["A5"] + PPC = reagent_plate.wells_by_name()["A6"] + EPM = reagent_plate.wells_by_name()["A7"] + + # pipette + p1000 = protocol.load_instrument( + "flex_8channel_1000", + "left", + tip_racks=[tiprack_200_1, tiprack_200_2, tiprack_200_3], + ) + p50 = protocol.load_instrument( + "flex_8channel_50", "right", tip_racks=[tiprack_50_1, tiprack_50_2] + ) + reagent_plate.columns()[3] + # Load liquids and probe + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Reagents": [ + {"well": reagent_plate.columns()[3], "volume": 75.0}, + {"well": reagent_plate.columns()[4], "volume": 15.0}, + {"well": reagent_plate.columns()[5], "volume": 20.0}, + {"well": reagent_plate.columns()[6], "volume": 65.0}, + ], + "AMPure": [{"well": reservoir.columns()[0], "volume": 120.0}], + "SMB": [{"well": reservoir.columns()[1], "volume": 750.0}], + "EtOH": [{"well": reservoir.columns()[3], "volume": 900.0}], + "RSB": [{"well": reservoir.columns()[4], "volume": 96.0}], + "Wash": [ + {"well": sample_plate_2.columns()[8], "volume": 1000.0}, + {"well": sample_plate_2.columns()[9], "volume": 1000.0}, + {"well": sample_plate_2.columns()[10], "volume": 1000.0}, + {"well": sample_plate_2.columns()[11], "volume": 1000.0}, + ], + "Samples": [{"well": sample_plate_1.wells(), "volume": 150.0}], + } + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, p50) + # tip and sample tracking + if COLUMNS == 1: + column_1_list = ["A1"] # Plate 1 + column_2_list = ["A1"] # Plate 2 + column_3_list = ["A4"] # Plate 2 + column_4_list = ["A4"] # Plate 1 + column_5_list = ["A7"] # Plate 2 + column_6_list = ["A7"] # Plate 1 + WASHES = [EEW_1] + if COLUMNS == 2: + column_1_list = ["A1", "A2"] # Plate 1 + column_2_list = ["A1", "A2"] # Plate 2 + column_3_list = ["A4", "A5"] # Plate 2 + column_4_list = ["A4", "A5"] # Plate 1 + column_5_list = ["A7", "A8"] # Plate 2 + column_6_list = ["A7", "A8"] # Plate 1 + WASHES = [EEW_1, EEW_2] + if COLUMNS == 3: + column_1_list = ["A1", "A2", "A3"] # Plate 1 + column_2_list = ["A1", "A2", "A3"] # Plate 2 + column_3_list = ["A4", "A5", "A6"] # Plate 2 + column_4_list = ["A4", "A5", "A6"] # Plate 1 + column_5_list = ["A7", "A8", "A9"] # Plate 2 + column_6_list = ["A7", "A8", "A9"] # Plate 1 + WASHES = [EEW_1, EEW_2, EEW_3] + if COLUMNS == 4: + column_1_list = ["A1", "A2", "A3", "A4"] # Plate 1 + column_2_list = ["A1", "A2", "A3", "A4"] # Plate 2 + column_3_list = ["A5", "A6", "A7", "A8"] # Plate 2 + column_4_list = ["A5", "A6", "A7", "A8"] # Plate 1 + column_5_list = ["A9", "A10", "A11", "A12"] # Plate 2 + column_6_list = ["A9", "A10", "A11", "A12"] # Plate 1 + WASHES = [EEW_1, EEW_2, EEW_3, EEW_4] + + def tipcheck() -> None: + """Tip tracking function.""" + if p200_tips >= 3 * 12: + p1000.reset_tipracks() + p200_tips == 0 + if p50_tips >= 2 * 12: + p50.reset_tipracks() + p50_tips == 0 + + # commands + for loop in range(RUN): + thermocycler.open_lid() + heatershaker.open_labware_latch() + if DRYRUN is False: + if STEP_HYB == 1: + protocol.comment("SETTING THERMO and TEMP BLOCK Temperature") + thermocycler.set_block_temperature(4) + thermocycler.set_lid_temperature(100) + temp_block.set_temperature(4) + else: + protocol.comment("SETTING THERMO and TEMP BLOCK Temperature") + thermocycler.set_block_temperature(58) + thermocycler.set_lid_temperature(58) + heatershaker.set_and_wait_for_temperature(58) + heatershaker.close_labware_latch() + + # Sample Plate contains 30ul of DNA + + if STEP_VOLPOOL == 1: + protocol.comment("==============================================") + protocol.comment("--> Quick Vol Pool") + protocol.comment("==============================================") + + if STEP_HYB == 1: + protocol.comment("==============================================") + protocol.comment("--> HYB") + protocol.comment("==============================================") + + protocol.comment("--> Adding NHB2") + NHB2Vol = 50 + for loop, X in enumerate(column_1_list): + p50.pick_up_tip() + p50.aspirate(NHB2Vol, NHB2.bottom(z=dot_bottom)) # original = () + p50.dispense( + NHB2Vol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> Adding Panel") + PanelVol = 10 + for loop, X in enumerate(column_1_list): + p50.pick_up_tip() + p50.aspirate(PanelVol, Panel.bottom(z=dot_bottom)) # original = () + p50.dispense( + PanelVol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> Adding EHB2") + EHB2Vol = 10 + EHB2MixRep = 10 if DRYRUN is False else 1 + EHB2MixVol = 90 + for loop, X in enumerate(column_1_list): + p1000.pick_up_tip() + p1000.aspirate(EHB2Vol, EHB2.bottom(z=dot_bottom)) # original = () + p1000.dispense( + EHB2Vol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p1000.move_to(sample_plate_1[X].bottom(z=dot_bottom)) # original = () + p1000.mix(EHB2MixRep, EHB2MixVol) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p50_tips += 1 + tipcheck() + + if HYBRIDDECK: + protocol.comment("Hybridize on Deck") + if disposable_lid: + ( + lid_on_plate, + unused_lids, + used_lids, + ) = helpers.use_disposable_lid_with_tc( + protocol, unused_lids, used_lids, sample_plate_1, thermocycler + ) + else: + thermocycler.close_lid() + if DRYRUN is False: + profile_TAGSTOP: List[ThermocyclerStep] = [ + {"temperature": 98, "hold_time_minutes": 5}, + {"temperature": 97, "hold_time_minutes": 1}, + {"temperature": 95, "hold_time_minutes": 1}, + {"temperature": 93, "hold_time_minutes": 1}, + {"temperature": 91, "hold_time_minutes": 1}, + {"temperature": 89, "hold_time_minutes": 1}, + {"temperature": 87, "hold_time_minutes": 1}, + {"temperature": 85, "hold_time_minutes": 1}, + {"temperature": 83, "hold_time_minutes": 1}, + {"temperature": 81, "hold_time_minutes": 1}, + {"temperature": 79, "hold_time_minutes": 1}, + {"temperature": 77, "hold_time_minutes": 1}, + {"temperature": 75, "hold_time_minutes": 1}, + {"temperature": 73, "hold_time_minutes": 1}, + {"temperature": 71, "hold_time_minutes": 1}, + {"temperature": 69, "hold_time_minutes": 1}, + {"temperature": 67, "hold_time_minutes": 1}, + {"temperature": 65, "hold_time_minutes": 1}, + {"temperature": 63, "hold_time_minutes": 1}, + {"temperature": 62, "hold_time_minutes": HYBRIDTIME * 60}, + ] + thermocycler.execute_profile( + steps=profile_TAGSTOP, repetitions=1, block_max_volume=100 + ) + thermocycler.set_block_temperature(62) + if HYBRID_PAUSE: + protocol.comment("HYBRIDIZATION PAUSED") + thermocycler.set_block_temperature(10) + thermocycler.open_lid() + if disposable_lid: + if trash_lid: + protocol.move_labware(lid_on_plate, trash_bin, use_gripper=True) + elif len(used_lids) <= 1: + protocol.move_labware(lid_on_plate, "B4", use_gripper=True) + else: + protocol.move_labware( + lid_on_plate, used_lids[-2], use_gripper=True + ) + else: + protocol.comment("Hybridize off Deck") + + if STEP_CAPTURE == 1: + protocol.comment("==============================================") + protocol.comment("--> Capture") + protocol.comment("==============================================") + # Standard Setup + + if DRYRUN is False: + protocol.comment("SETTING THERMO and TEMP BLOCK Temperature") + thermocycler.set_block_temperature(58) + thermocycler.set_lid_temperature(58) + + if DRYRUN is False: + heatershaker.set_and_wait_for_temperature(58) + + protocol.comment("--> Transfer Hybridization") + TransferSup = 100 + for loop, X in enumerate(column_1_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_1[X].bottom(z=0.5)) + p1000.aspirate(TransferSup + 1, rate=0.25) + p1000.dispense( + TransferSup + 1, sample_plate_2[column_2_list[loop]].bottom(z=1) + ) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + if disposable_lid: + ( + lid_on_plate, + unused_lids, + used_lids, + ) = helpers.use_disposable_lid_with_tc( + protocol, + unused_lids, + used_lids, + sample_plate_1, + thermocycler, + ) + else: + thermocycler.close_lid() + + protocol.comment("--> ADDING SMB") + SMBVol = 250 + SMBMixRPM = heater_shaker_speed + SMBMixRep = 5.0 if DRYRUN is False else 0.1 # minutes + SMBPremix = 3 if DRYRUN is False else 1 + # ============================== + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.mix(SMBPremix, 200, SMB.bottom(z=1)) + p1000.aspirate(SMBVol / 2, SMB.bottom(z=1), rate=0.25) + p1000.dispense(SMBVol / 2, sample_plate_2[X].top(z=-7), rate=0.25) + p1000.aspirate(SMBVol / 2, SMB.bottom(z=1), rate=0.25) + p1000.dispense(SMBVol / 2, sample_plate_2[X].bottom(z=1), rate=0.25) + p1000.default_speed = 5 + p1000.move_to(sample_plate_2[X].bottom(z=5)) + for Mix in range(2): + p1000.aspirate(100, rate=0.5) + p1000.move_to(sample_plate_2[X].bottom(z=1)) + p1000.aspirate(80, rate=0.5) + p1000.dispense(80, rate=0.5) + p1000.move_to(sample_plate_2[X].bottom(z=5)) + p1000.dispense(100, rate=0.5) + Mix += 1 + p1000.blow_out(sample_plate_2[X].top(z=-7)) + p1000.default_speed = 400 + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.move_to(sample_plate_2[X].top(z=0)) + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + # ============================== + helpers.set_hs_speed(protocol, heatershaker, SMBMixRPM, SMBMixRep, True) + + # GRIPPER MOVE sample_plate_2 FROM heatershaker TO MAGPLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, MAG_PLATE_SLOT + ) + + thermocycler.open_lid() + if disposable_lid: + if trash_lid: + protocol.move_labware(lid_on_plate, trash_bin, use_gripper=True) + elif len(used_lids) <= 1: + protocol.move_labware(lid_on_plate, "B4", use_gripper=True) + else: + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + + if DRYRUN is False: + protocol.delay(minutes=2) + + protocol.comment("==============================================") + protocol.comment("--> WASH") + protocol.comment("==============================================") + # Setting Labware to Resume at Cleanup 1 + + protocol.comment("--> Remove SUPERNATANT") + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(4)) + p1000.aspirate(200, rate=0.25) + trash_liquid(protocol, p1000, 200.0, liquid_trash_list) + p1000.move_to(sample_plate_2[X].bottom(0.5)) + p1000.aspirate(200, rate=0.25) + trash_liquid(protocol, p1000, 200.0, liquid_trash_list) + p1000.aspirate(20) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + # GRIPPER MOVE sample_plate_2 FROM MAGPLATE TO heatershaker + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + + protocol.comment("--> Repeating 6 washes") + washreps = 6 + washcount = 0 + for wash in range(washreps): + + protocol.comment("--> Adding EEW") + EEWVol = 200 + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.aspirate( + EEWVol, WASHES[loop].bottom(z=dot_bottom) + ) # original = () + p1000.dispense( + EEWVol, sample_plate_2[X].bottom(z=dot_bottom) + ) # original = () + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + helpers.set_hs_speed( + protocol, heatershaker, int(heater_shaker_speed * 0.9), 4.0, True + ) + heatershaker.open_labware_latch() + + if DRYRUN is False: + protocol.delay(seconds=5 * 60) + + # GRIPPER MOVE sample_plate_2 FROM heatershaker TO MAGPLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, MAG_PLATE_SLOT + ) + + if DRYRUN is False: + protocol.delay(seconds=1 * 60) + + protocol.comment("--> Removing Supernatant") + RemoveSup = 200 + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=3.5)) + p1000.aspirate(RemoveSup - 100, rate=0.25) + protocol.delay(minutes=0.1) + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(100, rate=0.25) + trash_liquid(protocol, p1000, RemoveSup, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + # ============================================================================================ + # GRIPPER MOVE sample_plate_2 FROM MAGPLATE TO heatershaker + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + washcount += 1 + + protocol.comment("--> Adding EEW") + EEWVol = 200 + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.aspirate( + EEWVol, WASHES[loop].bottom(z=dot_bottom) + ) # original = () + p1000.dispense( + EEWVol, sample_plate_2[X].bottom(z=dot_bottom) + ) # original = () + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + helpers.set_hs_speed( + protocol, heatershaker, int(heater_shaker_speed * 0.9), 4.0, True + ) + + if DRYRUN is False: + protocol.delay(seconds=1 * 60) + + protocol.comment("--> Transfer Hybridization") + TransferSup = 200 + for loop, X in enumerate(column_2_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(TransferSup, rate=0.25) + p1000.dispense( + TransferSup, sample_plate_2[column_3_list[loop]].bottom(z=1) + ) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + if DRYRUN is False: + protocol.delay(seconds=5 * 60) + + # GRIPPER MOVE sample_plate_2 FROM heatershaker TO MAGPLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, MAG_PLATE_SLOT + ) + if DRYRUN is False: + protocol.delay(seconds=1 * 60) + + protocol.comment("--> Removing Supernatant") + RemoveSup = 200 + for loop, X in enumerate(column_3_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=3.5)) + p1000.aspirate(RemoveSup - 100, rate=0.25) + protocol.delay(minutes=0.1) + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(100, rate=0.25) + p1000.move_to(sample_plate_2[X].top(z=0.5)) + trash_liquid(protocol, p1000, 100, liquid_trash_list) + p1000.aspirate(20) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + protocol.comment("--> Removing Residual") + for loop, X in enumerate(column_3_list): + p50.pick_up_tip() + p50.move_to(sample_plate_2[X].bottom(z=dot_bottom)) # original = z=0 + p50.aspirate(50, rate=0.25) + p50.default_speed = 200 + trash_liquid(protocol, p50, 50, liquid_trash_list) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("==============================================") + protocol.comment("--> ELUTE") + protocol.comment("==============================================") + + protocol.comment("--> Adding Elute") + EluteVol = 23 + for loop, X in enumerate(column_3_list): + p50.pick_up_tip() + p50.aspirate(EluteVol, Elute.bottom(z=dot_bottom)) # original = () + p50.dispense( + EluteVol, sample_plate_2[X].bottom(z=dot_bottom) + ) # original = () + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + # ============================================================================================ + # GRIPPER MOVE sample_plate_2 FROM MAGPLATE TO heatershaker + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + # ============================================================================================ + helpers.set_hs_speed( + protocol, heatershaker, int(heater_shaker_speed * 0.9), 2.0, True + ) + heatershaker.open_labware_latch() + + if DRYRUN is False: + protocol.delay(minutes=2) + + # ============================================================================================ + # GRIPPER MOVE sample_plate_2 FROM heatershaker TO MAGPLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, MAG_PLATE_SLOT + ) + protocol.comment("--> Transfer Elution") + TransferSup = 21 + for loop, X in enumerate(column_3_list): + p50.pick_up_tip() + p50.move_to(sample_plate_2[X].bottom(z=0.5)) + p50.aspirate(TransferSup + 1, rate=0.25) + p50.dispense( + TransferSup + 1, sample_plate_1[column_4_list[loop]].bottom(z=1) + ) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> Adding ET2") + ET2Vol = 4 + ET2MixRep = 10 if DRYRUN is False else 1 + ET2MixVol = 20 + for loop, X in enumerate(column_4_list): + p50.pick_up_tip() + p50.aspirate(ET2Vol, ET2.bottom(z=dot_bottom)) # original = () + p50.dispense( + ET2Vol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.move_to(sample_plate_1[X].bottom(z=dot_bottom)) # original = () + p50.mix(ET2MixRep, ET2MixVol) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + if STEP_PCR == 1: + protocol.comment("==============================================") + protocol.comment("--> AMPLIFICATION") + protocol.comment("==============================================") + + protocol.comment("--> Adding PPC") + PPCVol = 5 + for loop, X in enumerate(column_4_list): + p50.pick_up_tip() + p50.aspirate(PPCVol, PPC.bottom(z=dot_bottom)) # original = () + p50.dispense( + PPCVol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> Adding EPM") + EPMVol = 20 + EPMMixRep = 10 if DRYRUN is False else 1 + EPMMixVol = 45 + for loop, X in enumerate(column_4_list): + p50.pick_up_tip() + p50.aspirate(EPMVol, EPM.bottom(z=dot_bottom)) # original = () + p50.dispense( + EPMVol, sample_plate_1[X].bottom(z=dot_bottom) + ) # original = () + p50.move_to(sample_plate_1[X].bottom(z=dot_bottom)) # original = () + p50.mix(EPMMixRep, EPMMixVol) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + if DRYRUN is False: + heatershaker.deactivate_heater() + + if STEP_PCRDECK == 1: + if DRYRUN is False: + if DRYRUN is False: + if disposable_lid: + ( + lid_on_plate, + unused_lids, + used_lids, + ) = helpers.use_disposable_lid_with_tc( + protocol, + unused_lids, + used_lids, + sample_plate_1, + thermocycler, + ) + else: + thermocycler.close_lid() + profile_PCR_1: List[ThermocyclerStep] = [ + {"temperature": 98, "hold_time_seconds": 45} + ] + thermocycler.execute_profile( + steps=profile_PCR_1, repetitions=1, block_max_volume=50 + ) + profile_PCR_2: List[ThermocyclerStep] = [ + {"temperature": 98, "hold_time_seconds": 30}, + {"temperature": 60, "hold_time_seconds": 30}, + {"temperature": 72, "hold_time_seconds": 30}, + ] + thermocycler.execute_profile( + steps=profile_PCR_2, repetitions=12, block_max_volume=50 + ) + profile_PCR_3: List[ThermocyclerStep] = [ + {"temperature": 72, "hold_time_minutes": 1} + ] + thermocycler.execute_profile( + steps=profile_PCR_3, repetitions=1, block_max_volume=50 + ) + thermocycler.set_block_temperature(10) + + thermocycler.open_lid() + if disposable_lid: + if trash_lid: + protocol.move_labware(lid_on_plate, trash_bin, use_gripper=True) + elif len(used_lids) <= 1: + protocol.move_labware(lid_on_plate, "B4", use_gripper=True) + else: + protocol.move_labware( + lid_on_plate, used_lids[-2], use_gripper=True + ) + + if STEP_CLEANUP == 1: + protocol.comment("==============================================") + protocol.comment("--> Cleanup") + protocol.comment("==============================================") + + # GRIPPER MOVE sample_plate_2 FROM MAGPLATE TO heatershaker + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + + protocol.comment("--> Transfer Elution") + TransferSup = 45 + for loop, X in enumerate(column_4_list): + p50.pick_up_tip() + p50.move_to(sample_plate_1[X].bottom(z=0.5)) + p50.aspirate(TransferSup + 1, rate=0.25) + p50.dispense( + TransferSup + 1, sample_plate_2[column_5_list[loop]].bottom(z=1) + ) + p50.return_tip() if TIP_TRASH is False else p50.drop_tip() + p50_tips += 1 + tipcheck() + + protocol.comment("--> ADDING AMPure (0.8x)") + AMPureVol = 40.5 + AMPureMixRep = 5.0 if DRYRUN is False else 0.1 + AMPurePremix = 3 if DRYRUN is False else 1 + # ========NEW SINGLE TIP DISPENSE=========== + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.mix(AMPurePremix, AMPureVol + 10, AMPure.bottom(z=1)) + p1000.aspirate(AMPureVol, AMPure.bottom(z=1), rate=0.25) + p1000.dispense(AMPureVol, sample_plate_2[X].bottom(z=1), rate=0.25) + p1000.default_speed = 5 + p1000.move_to(sample_plate_2[X].bottom(z=5)) + for Mix in range(2): + p1000.aspirate(60, rate=0.5) + p1000.move_to(sample_plate_2[X].bottom(z=1)) + p1000.aspirate(60, rate=0.5) + p1000.dispense(60, rate=0.5) + p1000.move_to(sample_plate_2[X].bottom(z=5)) + p1000.dispense(30, rate=0.5) + Mix += 1 + p1000.blow_out(sample_plate_2[X].top(z=2)) + p1000.default_speed = 400 + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.move_to(sample_plate_2[X].top(z=0)) + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + # ========NEW HS MIX========================= + helpers.set_hs_speed( + protocol, + heatershaker, + int(heater_shaker_speed * 0.9), + AMPureMixRep, + True, + ) + + # GRIPPER MOVE PLATE FROM HEATER SHAKER TO MAG PLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, MAG_PLATE_SLOT + ) + + if DRYRUN is False: + protocol.delay(minutes=4) + + protocol.comment("--> Removing Supernatant") + RemoveSup = 200 + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=3.5)) + p1000.aspirate(RemoveSup - 100, rate=0.25) + protocol.delay(minutes=0.1) + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(100, rate=0.25) + p1000.default_speed = 5 + p1000.move_to(sample_plate_2[X].top(z=2)) + p1000.default_speed = 200 + trash_liquid(protocol, p1000, 200, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + for X_times in range(2): + protocol.comment("--> ETOH Wash") + ETOHMaxVol = 150 + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.aspirate(ETOHMaxVol, EtOH.bottom(z=1)) + p1000.move_to(EtOH.top(z=0)) + p1000.move_to(EtOH.top(z=-5)) + p1000.move_to(EtOH.top(z=0)) + p1000.move_to(sample_plate_2[X].top(z=-2)) + p1000.dispense(ETOHMaxVol, rate=1) + protocol.delay(minutes=0.1) + p1000.blow_out() + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.move_to(sample_plate_2[X].top(z=0)) + p1000.move_to(sample_plate_2[X].top(z=5)) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + if DRYRUN is False: + protocol.delay(minutes=0.5) + + protocol.comment("--> Remove ETOH Wash") + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=3.5)) + p1000.aspirate(RemoveSup - 100, rate=0.25) + protocol.delay(minutes=0.1) + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(100, rate=0.25) + p1000.default_speed = 5 + p1000.move_to(sample_plate_2[X].top(z=2)) + p1000.default_speed = 200 + trash_liquid(protocol, p1000, RemoveSup, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + if DRYRUN is False: + protocol.delay(minutes=2) + + protocol.comment("--> Removing Residual ETOH") + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.move_to( + sample_plate_2[X].bottom(z=dot_bottom) + ) # original = (z=0) + p1000.aspirate(50, rate=0.25) + p1000.default_speed = 200 + trash_liquid(protocol, p1000, 50, liquid_trash_list) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + + if DRYRUN is False: + protocol.delay(minutes=1) + + # GRIPPER MOVE PLATE FROM MAG PLATE TO HEATER SHAKER + helpers.move_labware_to_hs( + protocol, sample_plate_2, heatershaker, heatershaker + ) + + protocol.comment("--> Adding RSB") + RSBVol = 32 + RSBMixRep = 1.0 if DRYRUN is False else 0.1 # minutes + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.aspirate(RSBVol, RSB.bottom(z=1)) + + p1000.move_to( + ( + sample_plate_2.wells_by_name()[X] + .center() + .move(types.Point(x=1.3 * 0.8, y=0, z=-4)) + ) + ) + p1000.dispense(RSBVol, rate=1) + p1000.move_to(sample_plate_2.wells_by_name()[X].bottom(z=1)) + p1000.aspirate(RSBVol, rate=1) + p1000.move_to( + ( + sample_plate_2.wells_by_name()[X] + .center() + .move(types.Point(x=0, y=1.3 * 0.8, z=-4)) + ) + ) + p1000.dispense(RSBVol, rate=1) + p1000.move_to(sample_plate_2.wells_by_name()[X].bottom(z=1)) + p1000.aspirate(RSBVol, rate=1) + p1000.move_to( + ( + sample_plate_2.wells_by_name()[X] + .center() + .move(types.Point(x=1.3 * -0.8, y=0, z=-4)) + ) + ) + p1000.dispense(RSBVol, rate=1) + p1000.move_to(sample_plate_2.wells_by_name()[X].bottom(z=1)) + p1000.aspirate(RSBVol, rate=1) + p1000.move_to( + ( + sample_plate_2.wells_by_name()[X] + .center() + .move(types.Point(x=0, y=1.3 * -0.8, z=-4)) + ) + ) + p1000.dispense(RSBVol, rate=1) + p1000.move_to(sample_plate_2.wells_by_name()[X].bottom(z=1)) + p1000.aspirate(RSBVol, rate=1) + p1000.dispense(RSBVol, rate=1) + + p1000.blow_out(sample_plate_2.wells_by_name()[X].center()) + p1000.move_to(sample_plate_2.wells_by_name()[X].top(z=5)) + p1000.move_to(sample_plate_2.wells_by_name()[X].top(z=0)) + p1000.move_to(sample_plate_2.wells_by_name()[X].top(z=5)) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + if DRYRUN is False: + helpers.set_hs_speed( + protocol, + heatershaker, + int(heater_shaker_speed * 0.8), + RSBMixRep, + True, + ) + + # GRIPPER MOVE PLATE FROM HEATER SHAKER TO MAG PLATE + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate_2, heatershaker, MAG_PLATE_SLOT + ) + + if DRYRUN is False: + protocol.delay(minutes=3) + + protocol.comment("--> Transferring Supernatant") + TransferSup = 30 + for loop, X in enumerate(column_5_list): + p1000.pick_up_tip() + p1000.move_to(sample_plate_2[X].bottom(z=0.5)) + p1000.aspirate(TransferSup + 1, rate=0.25) + p1000.dispense( + TransferSup + 1, sample_plate_1[column_6_list[loop]].bottom(z=1) + ) + p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() + p200_tips += 1 + tipcheck() + liquids_to_probe_at_end = [ + Liquid_trash_well_1, + Liquid_trash_well_2, + Liquid_trash_well_3, + Liquid_trash_well_4, + ] + helpers.find_liquid_height_of_all_wells(protocol, p50, liquids_to_probe_at_end) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/5_96ch complex protocol with single tip Pick Up.py b/abr-testing/abr_testing/protocols/active_protocols/5_96ch complex protocol with single tip Pick Up.py index dc40db7f177..ca7506cf6f0 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/5_96ch complex protocol with single tip Pick Up.py +++ b/abr-testing/abr_testing/protocols/active_protocols/5_96ch complex protocol with single tip Pick Up.py @@ -1,14 +1,14 @@ """96 ch Test Single Tip and Gripper Moves.""" from opentrons.protocol_api import ( - ALL, + COLUMN, SINGLE, + ALL, ParameterContext, ProtocolContext, Labware, ) from opentrons.protocol_api.module_contexts import ( HeaterShakerContext, - MagneticBlockContext, ThermocyclerContext, TemperatureModuleContext, ) @@ -21,7 +21,7 @@ } requirements = { - "robotType": "OT-3", + "robotType": "Flex", "apiLevel": "2.21", } @@ -45,25 +45,31 @@ def add_parameters(parameters: ParameterContext) -> None: helpers.create_dot_bottom_parameter(parameters) helpers.create_disposable_lid_parameter(parameters) helpers.create_tc_lid_deck_riser_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) -def run(ctx: ProtocolContext) -> None: +def run(protocol: ProtocolContext) -> None: """Protocol.""" - b = ctx.params.dot_bottom # type: ignore[attr-defined] - TIPRACK_96_NAME = ctx.params.tip_size # type: ignore[attr-defined] - disposable_lid = ctx.params.disposable_lid # type: ignore[attr-defined] - deck_riser = ctx.params.deck_riser # type: ignore[attr-defined] + b = protocol.params.dot_bottom # type: ignore[attr-defined] + TIPRACK_96_NAME = protocol.params.tip_size # type: ignore[attr-defined] + disposable_lid = protocol.params.disposable_lid # type: ignore[attr-defined] + deck_riser = protocol.params.deck_riser # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] - waste_chute = ctx.load_waste_chute() + waste_chute = protocol.load_waste_chute() + helpers.comment_protocol_version(protocol, "01") - thermocycler: ThermocyclerContext = ctx.load_module(helpers.tc_str) # type: ignore[assignment] - mag: MagneticBlockContext = ctx.load_module(helpers.mag_str, "A3") # type: ignore[assignment] - h_s: HeaterShakerContext = ctx.load_module(helpers.hs_str, "D1") # type: ignore[assignment] - temperature_module: TemperatureModuleContext = ctx.load_module( + thermocycler: ThermocyclerContext = protocol.load_module( + helpers.tc_str + ) # type: ignore[assignment] + h_s: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "D1" + ) # type: ignore[assignment] + temperature_module: TemperatureModuleContext = protocol.load_module( helpers.temp_str, "C1" ) # type: ignore[assignment] if disposable_lid: - unused_lids = helpers.load_disposable_lids(ctx, 3, ["A4"], deck_riser) + unused_lids = helpers.load_disposable_lids(protocol, 3, ["A2"], deck_riser) used_lids: List[Labware] = [] thermocycler.open_lid() h_s.open_labware_latch() @@ -75,16 +81,15 @@ def run(ctx: ProtocolContext) -> None: adapters = [temperature_module_adapter, h_s_adapter] - source_reservoir = ctx.load_labware(RESERVOIR_NAME, "D2") - dest_pcr_plate = ctx.load_labware(PCR_PLATE_96_NAME, "C2") + source_reservoir = protocol.load_labware(RESERVOIR_NAME, "D2") + dest_pcr_plate = protocol.load_labware(PCR_PLATE_96_NAME, "C2") + liquid_waste = protocol.load_labware("nest_1_reservoir_195ml", "B2", "Liquid Waste") - tip_rack_1 = ctx.load_labware( - TIPRACK_96_NAME, "A2", adapter=TIPRACK_96_ADAPTER_NAME + tip_rack_1 = protocol.load_labware( + TIPRACK_96_NAME, "A3", adapter="opentrons_flex_96_tiprack_adapter" ) - tip_rack_adapter = tip_rack_1.parent - - tip_rack_2 = ctx.load_labware(TIPRACK_96_NAME, "C3") - tip_rack_3 = ctx.load_labware(TIPRACK_96_NAME, "C4") + tip_rack_2 = protocol.load_labware(TIPRACK_96_NAME, "C3") + tip_rack_3 = protocol.load_labware(TIPRACK_96_NAME, "C4") tip_racks = [ tip_rack_1, @@ -92,14 +97,16 @@ def run(ctx: ProtocolContext) -> None: tip_rack_3, ] - pipette_96_channel = ctx.load_instrument( + pipette_96_channel = protocol.load_instrument( PIPETTE_96_CHANNEL_NAME, mount="left", tip_racks=tip_racks, liquid_presence_detection=True, ) - water = ctx.define_liquid(name="water", description="H₂O", display_color="#42AB2D") + water = protocol.define_liquid( + name="water", description="H₂O", display_color="#42AB2D" + ) source_reservoir.wells_by_name()["A1"].load_liquid(liquid=water, volume=29000) def run_moves( @@ -123,7 +130,7 @@ def move_to_locations( def reset_labware() -> None: """Reset the labware to the reset location.""" - ctx.move_labware( + protocol.move_labware( labware_to_move, reset_location, use_gripper=use_gripper ) @@ -131,7 +138,9 @@ def reset_labware() -> None: return for location in move_locations: - ctx.move_labware(labware_to_move, location, use_gripper=use_gripper) + protocol.move_labware( + labware_to_move, location, use_gripper=use_gripper + ) if reset_after_each_move: reset_labware() @@ -161,14 +170,13 @@ def test_gripper_moves() -> None: def deck_moves(labware: Labware, reset_location: str) -> None: """Function to perform the movement of labware.""" deck_move_sequence = [ - ["B2"], # Deck Moves + ["B3"], # Deck Moves ["C3"], # Staging Area Slot 3 Moves ["C4", "D4"], # Staging Area Slot 4 Moves [ thermocycler, temperature_module_adapter, h_s_adapter, - mag, ], # Module Moves ] @@ -177,14 +185,13 @@ def deck_moves(labware: Labware, reset_location: str) -> None: def staging_area_slot_3_moves(labware: Labware, reset_location: str) -> None: """Function to perform the movement of labware, starting w/ staging area slot 3.""" staging_area_slot_3_move_sequence = [ - ["B2", "C2"], # Deck Moves + ["B3", "C2"], # Deck Moves [], # Don't have Staging Area Slot 3 open ["C4", "D4"], # Staging Area Slot 4 Moves [ thermocycler, temperature_module_adapter, h_s_adapter, - mag, ], # Module Moves ] @@ -198,14 +205,13 @@ def staging_area_slot_3_moves(labware: Labware, reset_location: str) -> None: def staging_area_slot_4_moves(labware: Labware, reset_location: str) -> None: """Function to perform the movement of labware, starting with staging area slot 4.""" staging_area_slot_4_move_sequence = [ - ["C2", "B2"], # Deck Moves + ["C2", "B3"], # Deck Moves ["C3"], # Staging Area Slot 3 Moves ["C4"], # Staging Area Slot 4 Moves [ thermocycler, temperature_module_adapter, h_s_adapter, - mag, ], # Module Moves ] @@ -219,7 +225,7 @@ def staging_area_slot_4_moves(labware: Labware, reset_location: str) -> None: def module_moves(labware: Labware, module_locations: List) -> None: """Function to perform the movement of labware, starting on a module.""" module_move_sequence = [ - ["C2", "B2"], # Deck Moves + ["C2", "B3"], # Deck Moves ["C3"], # Staging Area Slot 3 Moves ["C4", "D4"], # Staging Area Slot 4 Moves ] @@ -229,7 +235,7 @@ def module_moves(labware: Labware, module_locations: List) -> None: labware_move_to_locations.remove(module_starting_location) all_sequences = module_move_sequence.copy() all_sequences.append(labware_move_to_locations) - ctx.move_labware( + protocol.move_labware( labware, module_starting_location, use_gripper=USING_GRIPPER ) run_moves( @@ -242,27 +248,27 @@ def module_moves(labware: Labware, module_locations: List) -> None: deck_moves(dest_pcr_plate, DECK_MOVE_RESET_LOCATION) - ctx.move_labware( + protocol.move_labware( dest_pcr_plate, STAGING_AREA_SLOT_3_RESET_LOCATION, use_gripper=USING_GRIPPER, ) staging_area_slot_3_moves(dest_pcr_plate, STAGING_AREA_SLOT_3_RESET_LOCATION) - ctx.move_labware( + protocol.move_labware( dest_pcr_plate, STAGING_AREA_SLOT_4_RESET_LOCATION, use_gripper=USING_GRIPPER, ) staging_area_slot_4_moves(dest_pcr_plate, STAGING_AREA_SLOT_4_RESET_LOCATION) - module_locations = [thermocycler, mag] + adapters + module_locations = [thermocycler] + adapters module_moves(dest_pcr_plate, module_locations) - ctx.move_labware(dest_pcr_plate, thermocycler, use_gripper=USING_GRIPPER) + protocol.move_labware(dest_pcr_plate, thermocycler, use_gripper=USING_GRIPPER) def test_manual_moves() -> None: """Test manual moves.""" - ctx.move_labware(source_reservoir, "D4", use_gripper=USING_GRIPPER) + protocol.move_labware(source_reservoir, "D4", use_gripper=USING_GRIPPER) def test_pipetting() -> None: """Test pipetting.""" @@ -279,64 +285,59 @@ def test_single_tip_pickup_usage() -> None: well_position = f"{row}{col}" pipette_96_channel.pick_up_tip(tip_rack_2) - pipette_96_channel.aspirate(5, source_reservoir[well_position]) - pipette_96_channel.touch_tip() + pipette_96_channel.aspirate(45, source_reservoir[well_position]) + pipette_96_channel.air_gap(5) pipette_96_channel.dispense( - 5, dest_pcr_plate[well_position].bottom(b) + 25, dest_pcr_plate[well_position].bottom(b) ) + pipette_96_channel.blow_out(location=liquid_waste["A1"]) pipette_96_channel.drop_tip() tip_count += 1 # leave this dropping in waste chute, do not use get_disposal_preference # want to test partial drop - ctx.move_labware(tip_rack_2, waste_chute, use_gripper=USING_GRIPPER) + protocol.move_labware(tip_rack_2, waste_chute, use_gripper=USING_GRIPPER) + + def test_column_tip_rack_usage() -> None: + """Column Tip Pick Up.""" + list_of_columns = list(range(1, 13)) + pipette_96_channel.configure_nozzle_layout( + style=COLUMN, start="A12", tip_racks=[tip_rack_3] + ) + protocol.comment("------------------------------") + protocol.comment(f"channels {pipette_96_channel.active_channels}") + protocol.move_labware(tip_rack_3, "C3", use_gripper=USING_GRIPPER) + for well in list_of_columns: + tiprack_well = "A" + str(well) + well_name = "A" + str(well) + pipette_96_channel.liquid_presence_detection = True + pipette_96_channel.pick_up_tip(tip_rack_3[tiprack_well]) + pipette_96_channel.aspirate(45, source_reservoir[well_name]) + pipette_96_channel.liquid_presence_detection = False + pipette_96_channel.air_gap(5) + pipette_96_channel.dispense(25, dest_pcr_plate[tiprack_well].bottom(b)) + pipette_96_channel.blow_out(location=liquid_waste["A1"]) + pipette_96_channel.drop_tip() + protocol.move_labware(tip_rack_3, waste_chute, use_gripper=USING_GRIPPER) def test_full_tip_rack_usage() -> None: """Full Tip Pick Up.""" - pipette_96_channel.configure_nozzle_layout(style=ALL, start="A1") + pipette_96_channel.configure_nozzle_layout( + style=ALL, tip_racks=[tip_rack_1] + ) + protocol.comment(f"channels {pipette_96_channel.active_channels}") pipette_96_channel.liquid_presence_detection = True - pipette_96_channel.pick_up_tip(tip_rack_1["A1"]) - - pipette_96_channel.aspirate(5, source_reservoir["A1"]) - pipette_96_channel.touch_tip() - + pipette_96_channel.pick_up_tip() + pipette_96_channel.aspirate(45, source_reservoir["A1"]) pipette_96_channel.liquid_presence_detection = False - pipette_96_channel.air_gap(height=30) - pipette_96_channel.blow_out(waste_chute) - - pipette_96_channel.aspirate(5, source_reservoir["A1"]) - pipette_96_channel.touch_tip() - - pipette_96_channel.air_gap(height=30) - pipette_96_channel.blow_out() - - pipette_96_channel.aspirate(10, source_reservoir["A1"]) - pipette_96_channel.touch_tip() - - pipette_96_channel.dispense(10, dest_pcr_plate["A1"].bottom(b)) - pipette_96_channel.mix(repetitions=5, volume=15) + pipette_96_channel.air_gap(5) + pipette_96_channel.dispense(25, dest_pcr_plate["A1"].bottom(b)) + pipette_96_channel.blow_out(location=liquid_waste["A1"]) pipette_96_channel.return_tip() - - ctx.move_labware(tip_rack_1, waste_chute, use_gripper=USING_GRIPPER) - ctx.move_labware(tip_rack_3, tip_rack_adapter, use_gripper=USING_GRIPPER) - - pipette_96_channel.pick_up_tip(tip_rack_3["A1"]) - pipette_96_channel.transfer( - volume=10, - source=source_reservoir["A1"], - dest=dest_pcr_plate["A1"], - new_tip="never", - touch_tip=True, - blow_out=True, - blowout_location="trash", - mix_before=(3, 5), - mix_after=(1, 5), - ) - pipette_96_channel.return_tip() - - ctx.move_labware(tip_rack_3, waste_chute, use_gripper=USING_GRIPPER) + pipette_96_channel.reset_tipracks() test_single_tip_pickup_usage() + test_column_tip_rack_usage() test_full_tip_rack_usage() def test_module_usage(unused_lids: List[Labware], used_lids: List[Labware]) -> None: @@ -351,14 +352,14 @@ def test_thermocycler( unused_lids, used_lids, ) = helpers.use_disposable_lid_with_tc( - ctx, unused_lids, used_lids, dest_pcr_plate, thermocycler + protocol, unused_lids, used_lids, dest_pcr_plate, thermocycler ) thermocycler.set_block_temperature(4) thermocycler.set_lid_temperature(105) # Close lid thermocycler.close_lid() helpers.perform_pcr( - ctx, + protocol, thermocycler, initial_denature_time_sec=45, denaturation_time_sec=30, @@ -374,9 +375,9 @@ def test_thermocycler( thermocycler.open_lid() if disposable_lid: if len(used_lids) <= 1: - ctx.move_labware(lid_on_plate, "B3", use_gripper=True) + protocol.move_labware(lid_on_plate, waste_chute, use_gripper=True) else: - ctx.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) + protocol.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) thermocycler.deactivate() def test_h_s() -> None: @@ -397,16 +398,21 @@ def test_temperature_module() -> None: temperature_module.set_temperature(10) temperature_module.deactivate() - def test_mag() -> None: - """Tests magnetic block.""" - pass - test_thermocycler(unused_lids, used_lids) test_h_s() test_temperature_module() - test_mag() test_pipetting() test_gripper_moves() test_module_usage(unused_lids, used_lids) test_manual_moves() + protocol.move_labware(source_reservoir, "C2", use_gripper=True) + helpers.clean_up_plates( + pipette_96_channel, [dest_pcr_plate, source_reservoir], liquid_waste["A1"], 50 + ) + pipette_96_channel.reset_tipracks() + helpers.find_liquid_height_of_all_wells( + protocol, pipette_96_channel, [liquid_waste["A1"]] + ) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py b/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py index 89f643729fb..cc2103d2555 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py +++ b/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py @@ -29,19 +29,28 @@ def add_parameters(parameters: ParameterContext) -> None: """Parameters.""" helpers.create_dot_bottom_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) + parameters.add_int( + variable_name="number_of_runs", + display_name="Number of Runs", + default=2, + minimum=1, + maximum=10, + ) # Start protocol def run(ctx: ProtocolContext) -> None: """Protocol.""" dot_bottom = ctx.params.dot_bottom # type: ignore[attr-defined] - + deactivate_modules = ctx.params.deactivate_modules # type: ignore[attr-defined] + number_of_runs = ctx.params.number_of_runs # type: ignore[attr-defined] dry_run = False tip_mixing = False wash_vol = 600.0 AL_vol = 230.0 - bind_vol = 320.0 + bind_vol = 300.0 sample_vol = 180.0 elution_vol = 100.0 @@ -211,74 +220,43 @@ def bead_mix(vol: float, plate: Well, reps: int = 5) -> None: pip.flow_rate.aspirate = 150 pip.flow_rate.dispense = 200 - # Start Protocol - temp.set_temperature(inc_temp) - # Transfer and mix lysis - pip.pick_up_tip(tips) - pip.aspirate(AL_total_vol, lysis_res) - pip.dispense(AL_total_vol, samples_m) - resuspend_pellet(400, samples_m, reps=4 if not dry_run else 1) - if not tip_mixing: - pip.return_tip() - - # Mix, then heat - ctx.comment("Lysis Mixing") - helpers.set_hs_speed(ctx, h_s, 1800, 10, False) - if not dry_run: - h_s.set_and_wait_for_temperature(55) - ctx.delay( - minutes=10 if not dry_run else 0.25, - msg="Please allow another 10 minutes of 55C incubation to complete lysis.", - ) - h_s.deactivate_shaker() - - # Transfer and mix bind&beads - pip.pick_up_tip(tips) - bead_mix(binding_buffer_vol, bind_res, reps=4 if not dry_run else 1) - pip.aspirate(binding_buffer_vol, bind_res) - pip.dispense(binding_buffer_vol, samples_m) - bead_mix(binding_buffer_vol + starting_vol, samples_m, reps=4 if not dry_run else 1) - if not tip_mixing: - pip.return_tip() - pip.home() - - # Shake for binding incubation - ctx.comment("Binding incubation") - helpers.set_hs_speed(ctx, h_s, 1800, 10, True) - - # Transfer plate to magnet - helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) - - ctx.delay( - minutes=settling_time, - msg="Please wait " + str(settling_time) + " minute(s) for beads to pellet.", - ) + def protocol() -> None: + # Start Protocol + temp.set_temperature(inc_temp) + # Transfer and mix lysis + pip.pick_up_tip(tips) + pip.aspirate(AL_total_vol, lysis_res) + pip.dispense(AL_total_vol, samples_m) + resuspend_pellet(200, samples_m, reps=4 if not dry_run else 1) + if not tip_mixing: + pip.return_tip() - # Remove Supernatant and move off magnet - pip.pick_up_tip(tips) - pip.aspirate(1000, samples_m.bottom(dot_bottom)) - pip.dispense(1000, waste) - if starting_vol + binding_buffer_vol > 1000: - pip.aspirate(1000, samples_m.bottom(dot_bottom)) - pip.dispense(1000, waste) - pip.return_tip() - - # Transfer plate from magnet to H/S - helpers.move_labware_to_hs(ctx, sample_plate, h_s, h_s_adapter) - - # Washes - for i in range(num_washes if not dry_run else 1): - if i == 0 or i == 1: - wash_res = wash1_res - else: - wash_res = wash2_res + # Mix, then heat + ctx.comment("Lysis Mixing") + helpers.set_hs_speed(ctx, h_s, 1800, 10, False) + if not dry_run: + h_s.set_and_wait_for_temperature(55) + ctx.delay( + minutes=10 if not dry_run else 0.25, + msg="Please allow another 10 minutes of 55C incubation to complete lysis.", + ) + h_s.deactivate_shaker() + # Transfer and mix bind&beads pip.pick_up_tip(tips) - pip.aspirate(wash_vol, wash_res) - pip.dispense(wash_vol, samples_m) + bead_mix(binding_buffer_vol, bind_res, reps=4 if not dry_run else 1) + pip.aspirate(binding_buffer_vol, bind_res) + pip.dispense(binding_buffer_vol, samples_m) + bead_mix( + binding_buffer_vol + starting_vol, samples_m, reps=4 if not dry_run else 1 + ) if not tip_mixing: pip.return_tip() - helpers.set_hs_speed(ctx, h_s, 1800, 5, True) + pip.home() + + # Shake for binding incubation + ctx.comment("Binding incubation") + helpers.set_hs_speed(ctx, h_s, 1800, 10, True) # Transfer plate to magnet helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) @@ -290,62 +268,161 @@ def bead_mix(vol: float, plate: Well, reps: int = 5) -> None: # Remove Supernatant and move off magnet pip.pick_up_tip(tips) - pip.aspirate(1000, samples_m.bottom(dot_bottom)) - pip.dispense(1000, bind_res.top()) - if wash_vol > 1000: - pip.aspirate(1000, samples_m.bottom(dot_bottom)) - pip.dispense(1000, bind_res.top()) + pip.aspirate(550, samples_m.bottom(dot_bottom)) + pip.dispense(550, waste) + if starting_vol + binding_buffer_vol > 1000: + pip.aspirate(550, samples_m.bottom(dot_bottom)) + pip.dispense(550, waste) pip.return_tip() # Transfer plate from magnet to H/S helpers.move_labware_to_hs(ctx, sample_plate, h_s, h_s_adapter) - # Dry beads - if dry_run: - drybeads = 0.5 - else: - drybeads = 10 - # Number of minutes you want to dry for - for beaddry in np.arange(drybeads, 0, -0.5): + # Washes + for i in range(num_washes if not dry_run else 1): + if i == 0 or i == 1: + wash_res = wash1_res + else: + wash_res = wash2_res + + pip.pick_up_tip(tips) + pip.aspirate(wash_vol, wash_res) + pip.dispense(wash_vol, samples_m) + if not tip_mixing: + pip.return_tip() + helpers.set_hs_speed(ctx, h_s, 1800, 5, True) + + # Transfer plate to magnet + helpers.move_labware_from_hs_to_destination( + ctx, sample_plate, h_s, magblock + ) + + ctx.delay( + minutes=settling_time, + msg="Please wait " + + str(settling_time) + + " minute(s) for beads to pellet.", + ) + + # Remove Supernatant and move off magnet + pip.pick_up_tip(tips) + pip.aspirate(473, samples_m.bottom(dot_bottom)) + pip.dispense(473, bind_res.top()) + if wash_vol > 1000: + pip.aspirate(473, samples_m.bottom(dot_bottom)) + pip.dispense(473, bind_res.top()) + pip.return_tip() + + # Transfer plate from magnet to H/S + helpers.move_labware_to_hs(ctx, sample_plate, h_s, h_s_adapter) + + # Dry beads + if dry_run: + drybeads = 0.5 + else: + drybeads = 10 + # Number of minutes you want to dry for + for beaddry in np.arange(drybeads, 0, -0.5): + ctx.delay( + minutes=0.5, + msg="There are " + str(beaddry) + " minutes left in the drying step.", + ) + + # Elution + pip.pick_up_tip(tips1) + pip.aspirate(elution_vol, elution_res) + pip.dispense(elution_vol, samples_m) + resuspend_pellet(elution_vol, samples_m, reps=3 if not dry_run else 1) + if not tip_mixing: + pip.return_tip() + pip.home() + + helpers.set_hs_speed(ctx, h_s, 2000, 5, True) + + # Transfer plate to magnet + helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) + ctx.delay( - minutes=0.5, - msg="There are " + str(beaddry) + " minutes left in the drying step.", + minutes=settling_time, + msg="Please wait " + str(settling_time) + " minute(s) for beads to pellet.", ) - # Elution - pip.pick_up_tip(tips1) - pip.aspirate(elution_vol, elution_res) - pip.dispense(elution_vol, samples_m) - resuspend_pellet(elution_vol, samples_m, reps=3 if not dry_run else 1) - if not tip_mixing: + pip.pick_up_tip(tips1) + pip.aspirate(elution_vol, samples_m) + pip.dispense(elution_vol, elutionplate.wells()[0]) pip.return_tip() - pip.home() - - helpers.set_hs_speed(ctx, h_s, 2000, 5, True) - # Transfer plate to magnet - helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) + pip.home() + pip.reset_tipracks() + + # Empty Plates + pip.pick_up_tip() + pip.aspirate(500, samples_m) + pip.dispense(500, liquid_waste["A1"].top()) + pip.aspirate(500, wash1_res) + pip.dispense(500, liquid_waste["A1"].top()) + pip.aspirate(500, wash2_res) + pip.dispense(500, liquid_waste["A1"].top()) + pip.return_tip() + helpers.find_liquid_height_of_all_wells(ctx, pip, [liquid_waste["A1"]]) + helpers.move_labware_to_hs(ctx, sample_plate, h_s, h_s_adapter) - ctx.delay( - minutes=settling_time, - msg="Please wait " + str(settling_time) + " minute(s) for beads to pellet.", - ) + def setup() -> None: + pip.pick_up_tip() + pip.transfer( + volume=250, + source=liquid_waste["A1"].bottom(z=2), + dest=lysis_reservoir["A1"], + blow_out=True, + blowout_location="source well", + new_tip="never", + trash=False, + ) + pip.transfer( + 1700, + liquid_waste["A1"].bottom(z=2), + wash1_reservoir["A1"], + blow_out=True, + blowout_location="source well", + new_tip="never", + trash=False, + ) + pip.transfer( + 1100, + bind_reservoir["A1"].bottom(z=2), + wash2_reservoir["A1"], + blow_out=True, + blowout_location="source well", + new_tip="never", + trash=False, + ) + pip.transfer( + 100, + liquid_waste["A1"].bottom(z=2), + sample_plate["A1"], + blow_out=True, + blowout_location="source well", + new_tip="never", + trash=False, + ) + pip.return_tip() - pip.pick_up_tip(tips1) - pip.aspirate(elution_vol, samples_m) - pip.dispense(elution_vol, elutionplate.wells()[0]) - pip.return_tip() - - pip.home() - pip.reset_tipracks() - - # Empty Plates - pip.pick_up_tip() - pip.aspirate(1000, samples_m) - pip.dispense(1000, liquid_waste["A1"].top()) - pip.aspirate(1000, wash1_res) - pip.dispense(1000, liquid_waste["A1"].top()) - pip.aspirate(1000, wash2_res) - pip.dispense(1000, liquid_waste["A1"].top()) - pip.return_tip() - helpers.find_liquid_height_of_all_wells(ctx, pip, [liquid_waste["A1"]]) + def clean() -> None: + plates_to_clean = [ + sample_plate, + elutionplate, + wash2_reservoir, + wash1_reservoir, + liquid_waste, + ] + helpers.clean_up_plates(pip, plates_to_clean, liquid_waste["A1"], 1000) + + for i in range(number_of_runs): + protocol() + pip.reset_tipracks() + if i < number_of_runs - 1: + setup() + pip.reset_tipracks() + clean() + if deactivate_modules: + helpers.deactivate_modules(ctx) diff --git a/abr-testing/abr_testing/protocols/active_protocols/7_HDQ_DNA_Bacteria_Flex.py b/abr-testing/abr_testing/protocols/active_protocols/7_HDQ_DNA_Bacteria_Flex.py index aa33079f553..4350888b0d6 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/7_HDQ_DNA_Bacteria_Flex.py +++ b/abr-testing/abr_testing/protocols/active_protocols/7_HDQ_DNA_Bacteria_Flex.py @@ -22,7 +22,7 @@ } requirements = { - "robotType": "OT-3", + "robotType": "Flex", "apiLevel": "2.21", } """ @@ -57,18 +57,22 @@ def add_parameters(parameters: ParameterContext) -> None: helpers.create_single_pipette_mount_parameter(parameters) helpers.create_hs_speed_parameter(parameters) helpers.create_dot_bottom_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) -def run(ctx: ProtocolContext) -> None: +def run(protocol: ProtocolContext) -> None: """Protocol.""" - heater_shaker_speed = ctx.params.heater_shaker_speed # type: ignore[attr-defined] - mount = ctx.params.pipette_mount # type: ignore[attr-defined] - dot_bottom = ctx.params.dot_bottom # type: ignore[attr-defined] + heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] + mount = protocol.params.pipette_mount # type: ignore[attr-defined] + dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") + dry_run = False TIP_TRASH = False res_type = "nest_12_reservoir_22ml" - num_samples = 8 + num_samples = 96 wash1_vol = 600.0 wash2_vol = 600.0 wash3_vol = 600.0 @@ -95,37 +99,48 @@ def run(ctx: ProtocolContext) -> None: starting_vol = AL_vol + sample_vol binding_buffer_vol = bind_vol + bead_vol - ctx.load_trash_bin("A3") - h_s: HeaterShakerContext = ctx.load_module(helpers.hs_str, "D1") # type: ignore[assignment] + protocol.load_trash_bin("A3") + h_s: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "D1" + ) # type: ignore[assignment] sample_plate, h_s_adapter = helpers.load_hs_adapter_and_labware( deepwell_type, h_s, "Sample Plate" ) h_s.close_labware_latch() - temp: TemperatureModuleContext = ctx.load_module( + temp: TemperatureModuleContext = protocol.load_module( helpers.temp_str, "D3" ) # type: ignore[assignment] elutionplate, temp_adapter = helpers.load_temp_adapter_and_labware( "armadillo_96_wellplate_200ul_pcr_full_skirt", temp, "Elution Plate" ) - magnetic_block: MagneticBlockContext = ctx.load_module( + magnetic_block: MagneticBlockContext = protocol.load_module( helpers.mag_str, "C1" ) # type: ignore[assignment] - waste_reservoir = ctx.load_labware("nest_1_reservoir_195ml", "B3", "Liquid Waste") + waste_reservoir = protocol.load_labware( + "nest_1_reservoir_195ml", "B3", "Liquid Waste" + ) waste = waste_reservoir.wells()[0].top() - res1 = ctx.load_labware(res_type, "D2", "Reagent Reservoir 1") + res1 = protocol.load_labware(res_type, "D2", "Reagent Reservoir 1") num_cols = math.ceil(num_samples / 8) - # Load tips and combine all similar boxes - tips1000 = ctx.load_labware("opentrons_flex_96_tiprack_1000ul", "A1", "Tips 1") - tips1001 = ctx.load_labware("opentrons_flex_96_tiprack_1000ul", "A2", "Tips 2") - tips1002 = ctx.load_labware("opentrons_flex_96_tiprack_1000ul", "B1", "Tips 3") - tips = [*tips1000.wells()[num_samples:96], *tips1001.wells(), *tips1002.wells()] + tips1000 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "A1", "Tips 1") + tips1001 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "A2", "Tips 2") + tips1002 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "B1", "Tips 3") + tips1003 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "B2", "Tips 4") + tips1004 = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "C2", "Tips 5") + + tips = [ + *tips1000.wells()[num_samples:96], + *tips1001.wells(), + *tips1002.wells(), + *tips1003.wells(), + ] tips_sn = tips1000.wells()[:num_samples] # load instruments - m1000 = ctx.load_instrument( - "flex_8channel_1000", mount, tip_racks=[tips1000, tips1001, tips1002] + m1000 = protocol.load_instrument( + "flex_8channel_1000", mount, tip_racks=[tips1000, tips1001, tips1002, tips1003] ) """ @@ -158,7 +173,7 @@ def run(ctx: ProtocolContext) -> None: m1000.flow_rate.aspirate = 300 m1000.flow_rate.dispense = 300 m1000.flow_rate.blow_out = 300 - helpers.find_liquid_height_of_loaded_liquids(ctx, liquid_vols_and_wells, m1000) + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, m1000) def tiptrack(tipbox: List[Well]) -> None: """Track Tips.""" @@ -167,14 +182,15 @@ def tiptrack(tipbox: List[Well]) -> None: if tipbox == tips: m1000.pick_up_tip(tipbox[int(tip1k)]) tip1k = tip1k + 8 + if tip1k >= len(tipbox): + tip1k = 0 drop_count = drop_count + 8 if drop_count >= 150: drop_count = 0 - ctx.pause("Empty Waste Bin.") def remove_supernatant(vol: float) -> None: """Remove supernatants.""" - ctx.comment("-----Removing Supernatant-----") + protocol.comment("-----Removing Supernatant-----") m1000.flow_rate.aspirate = 150 num_trans = math.ceil(vol / 980) vol_per_trans = vol / num_trans @@ -192,7 +208,7 @@ def remove_supernatant(vol: float) -> None: m1000.air_gap(20) m1000.drop_tip(tips_sn[8 * i]) if TIP_TRASH else m1000.return_tip() m1000.flow_rate.aspirate = 300 - helpers.move_labware_to_hs(ctx, sample_plate, h_s, h_s_adapter) + helpers.move_labware_to_hs(protocol, sample_plate, h_s, h_s_adapter) def bead_mixing( well: Well, pip: InstrumentContext, mvol: float, reps: int = 8 @@ -284,7 +300,7 @@ def mixing(well: Well, pip: InstrumentContext, mvol: float, reps: int = 8) -> No def A_lysis(vol: float, source: Well) -> None: """A Lysis.""" - ctx.comment("-----Mixing then transferring AL buffer-----") + protocol.comment("-----Mixing then transferring AL buffer-----") num_transfers = math.ceil(vol / 980) tiptrack(tips) for i in range(num_cols): @@ -303,7 +319,6 @@ def A_lysis(vol: float, source: Well) -> None: m1000.require_liquid_presence(src) m1000.aspirate(tvol, src.bottom(1)) m1000.dispense(tvol, src.bottom(4)) - m1000.require_liquid_presence(src) m1000.aspirate(tvol, src.bottom(height)) m1000.air_gap(10) m1000.dispense(m1000.current_volume, samples_m[i].top()) @@ -318,12 +333,12 @@ def A_lysis(vol: float, source: Well) -> None: m1000.air_gap(20) m1000.drop_tip() if TIP_TRASH else m1000.return_tip() - ctx.comment("-----Mixing then Heating AL and Sample-----") + protocol.comment("-----Mixing then Heating AL and Sample-----") - helpers.set_hs_speed(ctx, h_s, heater_shaker_speed, A_lysis_time_1, False) + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, A_lysis_time_1, False) if not dry_run: h_s.set_and_wait_for_temperature(55) - ctx.delay( + protocol.delay( minutes=A_lysis_time_2, msg="Incubating at 55C " + str(heater_shaker_speed) @@ -347,17 +362,17 @@ def bind(vol: float) -> None: supernatant to the final clean elutions PCR plate. """ - ctx.comment("-----Beginning Bind Steps-----") + protocol.comment("-----Beginning Bind Steps-----") tiptrack(tips) for i, well in enumerate(samples_m): num_trans = math.ceil(vol / 980) vol_per_trans = vol / num_trans - source = binding_buffer[i // 3] + source = binding_buffer[i // 7] if i == 0: reps = 6 if not dry_run else 1 else: reps = 1 - ctx.comment("-----Mixing Beads in Reservoir-----") + protocol.comment("-----Mixing Beads in Reservoir-----") bead_mixing(source, m1000, vol_per_trans, reps=reps if not dry_run else 1) # Transfer beads and binding from source to H-S plate for t in range(num_trans): @@ -370,7 +385,7 @@ def bind(vol: float) -> None: if t < num_trans - 1: m1000.air_gap(20) - ctx.comment("-----Mixing Beads in Plate-----") + protocol.comment("-----Mixing Beads in Plate-----") for i in range(num_cols): if i != 0: tiptrack(tips) @@ -379,19 +394,19 @@ def bind(vol: float) -> None: ) m1000.drop_tip() if TIP_TRASH else m1000.return_tip() - ctx.comment("-----Incubating Beads and Bind on H-S-----") + protocol.comment("-----Incubating Beads and Bind on H-S-----") speed_val = heater_shaker_speed * 0.9 - helpers.set_hs_speed(ctx, h_s, speed_val, bind_time, True) + helpers.set_hs_speed(protocol, h_s, speed_val, bind_time, True) # Transfer from H-S plate to Magdeck plate helpers.move_labware_from_hs_to_destination( - ctx, sample_plate, h_s, magnetic_block + protocol, sample_plate, h_s, magnetic_block ) for bindi in np.arange( settling_time + 1, 0, -0.5 ): # Settling time delay with countdown timer - ctx.delay( + protocol.delay( minutes=0.5, msg="There are " + str(bindi) + " minutes left in the incubation.", ) @@ -410,29 +425,29 @@ def wash(vol: float, source: List[Well]) -> None: if source == wash3: whichwash = 3 - ctx.comment("-----Beginning Wash #" + str(whichwash) + "-----") + protocol.comment("-----Beginning Wash #" + str(whichwash) + "-----") num_trans = math.ceil(vol / 980) vol_per_trans = vol / num_trans tiptrack(tips) for i, m in enumerate(samples_m): - src = source[i // 2] + src = source[i // 4] for n in range(num_trans): if m1000.current_volume > 0: m1000.dispense(m1000.current_volume, src.top()) m1000.transfer(vol_per_trans, src, m.top(), air_gap=20, new_tip="never") m1000.drop_tip() if TIP_TRASH else m1000.return_tip() - helpers.set_hs_speed(ctx, h_s, heater_shaker_speed, elute_wash_time, True) + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, elute_wash_time, True) helpers.move_labware_from_hs_to_destination( - ctx, sample_plate, h_s, magnetic_block + protocol, sample_plate, h_s, magnetic_block ) for washi in np.arange( settling_time, 0, -0.5 ): # settling time timer for washes - ctx.delay( + protocol.delay( minutes=0.5, msg="There are " + str(washi) @@ -445,7 +460,7 @@ def wash(vol: float, source: List[Well]) -> None: def elute(vol: float) -> None: """Elution Function.""" - ctx.comment("-----Beginning Elution Steps-----") + protocol.comment("-----Beginning Elution Steps-----") tiptrack(tips) for i, (m, e) in enumerate(zip(samples_m, elution_samples_m)): m1000.flow_rate.aspirate = 25 @@ -457,15 +472,15 @@ def elute(vol: float) -> None: h_s.set_and_wait_for_shake_speed(heater_shaker_speed * 1.1) speed_val = heater_shaker_speed * 1.1 - helpers.set_hs_speed(ctx, h_s, speed_val, elute_wash_time, True) + helpers.set_hs_speed(protocol, h_s, speed_val, elute_wash_time, True) # Transfer back to magnet helpers.move_labware_from_hs_to_destination( - ctx, sample_plate, h_s, magnetic_block + protocol, sample_plate, h_s, magnetic_block ) for elutei in np.arange(settling_time, 0, -0.5): - ctx.delay( + protocol.delay( minutes=0.5, msg="Incubating on MagDeck for " + str(elutei) + " more minutes.", ) @@ -495,7 +510,7 @@ def elute(vol: float) -> None: else: drybeads = 0.5 for beaddry in np.arange(drybeads, 0, -0.5): - ctx.delay( + protocol.delay( minutes=0.5, msg="There are " + str(beaddry) + " minutes left in the drying step.", ) @@ -504,7 +519,9 @@ def elute(vol: float) -> None: # Probe wells end_wells_with_liquid = [ waste_reservoir.wells()[0], - res1.wells()[0], - elutionplate.wells()[0], ] - helpers.find_liquid_height_of_all_wells(ctx, m1000, end_wells_with_liquid) + m1000.tip_racks = [tips1004] + helpers.clean_up_plates(m1000, [res1, elutionplate], waste_reservoir["A1"], 1000) + helpers.find_liquid_height_of_all_wells(protocol, m1000, end_wells_with_liquid) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py b/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py index 4894cae41d4..3d8c664956c 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py +++ b/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py @@ -1,5 +1,11 @@ """Illumina DNA Prep and Plate Reader Test.""" -from opentrons.protocol_api import ParameterContext, ProtocolContext, Labware +from opentrons.protocol_api import ( + ParameterContext, + ProtocolContext, + Labware, + Well, + InstrumentContext, +) from abr_testing.protocols import helpers from opentrons.protocol_api.module_contexts import ( AbsorbanceReaderContext, @@ -10,9 +16,10 @@ ) from datetime import datetime from opentrons.hardware_control.modules.types import ThermocyclerStep -from typing import List +from typing import List, Dict from opentrons import types + metadata = { "protocolName": "Illumina DNA Prep and Plate Reader Test", "author": "Platform Expansion", @@ -31,7 +38,7 @@ HYBRID_PAUSE = True # True = sets a pause on the Hybridization # PROTOCOL SETTINGS -COLUMNS = 3 # 1-3 +COLUMNS = 4 # 1-4 HYBRIDDECK = True HYBRIDTIME = 1.6 # Hours @@ -55,6 +62,8 @@ def add_parameters(parameters: ParameterContext) -> None: """Add Parameters.""" helpers.create_hs_speed_parameter(parameters) helpers.create_dot_bottom_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) + helpers.create_plate_reader_compatible_labware_parameter(parameters) parameters.add_bool( variable_name="plate_orientation", display_name="Hellma Plate Orientation", @@ -103,6 +112,10 @@ def run(protocol: ProtocolContext) -> None: heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] plate_orientation = protocol.params.plate_orientation # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + plate_type = protocol.params.labware_plate_reader_compatible # type: ignore [attr-defined] + helpers.comment_protocol_version(protocol, "01") + plate_name_str = "hellma_plate_" + str(plate_orientation) global p200_tips global p50_tips @@ -115,7 +128,9 @@ def run(protocol: ProtocolContext) -> None: tiprack_50_2 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "A3") # MODULES + LABWARE # Reservoir - reservoir = protocol.load_labware("nest_96_wellplate_2ml_deep", "D2") + reservoir = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "D2", "Liquid Waste" + ) # Heatershaker heatershaker: HeaterShakerContext = protocol.load_module( helpers.hs_str, "D1" @@ -123,6 +138,7 @@ def run(protocol: ProtocolContext) -> None: sample_plate_2 = heatershaker.load_labware( "thermoscientificnunc_96_wellplate_1300ul" ) + heatershaker.close_labware_latch() # Magnetic Block mag_block: MagneticBlockContext = protocol.load_module( helpers.mag_str, "C1" @@ -133,6 +149,7 @@ def run(protocol: ProtocolContext) -> None: sample_plate_1 = thermocycler.load_labware( "armadillo_96_wellplate_200ul_pcr_full_skirt" ) + thermocycler.open_lid() # Temperature Module temp_block: TemperatureModuleContext = protocol.load_module( helpers.temp_str, "B3" @@ -144,7 +161,7 @@ def run(protocol: ProtocolContext) -> None: plate_reader: AbsorbanceReaderContext = protocol.load_module( helpers.abs_mod_str, PLATE_READER_SLOT ) # type: ignore[assignment] - hellma_plate = protocol.load_labware("hellma_reference_plate", HELLMA_PLATE_SLOT) + hellma_plate = protocol.load_labware(plate_type, HELLMA_PLATE_SLOT) # PIPETTES p1000 = protocol.load_instrument( "flex_8channel_1000", @@ -154,6 +171,28 @@ def run(protocol: ProtocolContext) -> None: p50 = protocol.load_instrument( "flex_8channel_50", "right", tip_racks=[tiprack_50_1, tiprack_50_2] ) + + # Load liquids and probe + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Reagents": [ + {"well": reagent_plate.columns()[3], "volume": 75.0}, + {"well": reagent_plate.columns()[4], "volume": 15.0}, + {"well": reagent_plate.columns()[5], "volume": 20.0}, + {"well": reagent_plate.columns()[6], "volume": 65.0}, + ], + "AMPure": [{"well": reservoir.columns()[0], "volume": 120.0}], + "SMB": [{"well": reservoir.columns()[1], "volume": 750.0}], + "EtOH": [{"well": reservoir.columns()[3], "volume": 900.0}], + "RSB": [{"well": reservoir.columns()[4], "volume": 96.0}], + "Wash": [ + {"well": sample_plate_2.columns()[9], "volume": 1000.0}, + {"well": sample_plate_2.columns()[10], "volume": 1000.0}, + {"well": sample_plate_2.columns()[11], "volume": 1000.0}, + ], + "Samples": [{"well": sample_plate_1.wells(), "volume": 150.0}], + } + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, p50) + # reagent AMPure = reservoir["A1"] SMB = reservoir["A2"] @@ -165,11 +204,42 @@ def run(protocol: ProtocolContext) -> None: Liquid_trash_well_2 = reservoir["A10"] Liquid_trash_well_3 = reservoir["A11"] Liquid_trash_well_4 = reservoir["A12"] + liquid_trash_list = { + Liquid_trash_well_1: 0.0, + Liquid_trash_well_2: 0.0, + Liquid_trash_well_3: 0.0, + Liquid_trash_well_4: 0.0, + } + + def trash_liquid( + protocol: ProtocolContext, + pipette: InstrumentContext, + vol_to_trash: float, + liquid_trash_list: Dict[Well, float], + ) -> None: + """Determine which wells to use as liquid waste.""" + remaining_volume = vol_to_trash + max_capacity = 1500.0 + # Determine liquid waste location depending on current total volume + # Distribute the liquid volume sequentially + for well, current_volume in liquid_trash_list.items(): + if remaining_volume <= 0.0: + break + available_capacity = max_capacity - current_volume + if available_capacity < remaining_volume: + continue + pipette.dispense(remaining_volume, well.top()) + protocol.delay(minutes=0.1) + pipette.blow_out(well.top()) + liquid_trash_list[well] += remaining_volume + if pipette.current_volume <= 0.0: + break # Will Be distributed during the protocol - EEW_1 = sample_plate_2.wells_by_name()["A10"] - EEW_2 = sample_plate_2.wells_by_name()["A11"] - EEW_3 = sample_plate_2.wells_by_name()["A12"] + EEW_1 = sample_plate_2.wells_by_name()["A9"] + EEW_2 = sample_plate_2.wells_by_name()["A10"] + EEW_3 = sample_plate_2.wells_by_name()["A11"] + EEW_4 = sample_plate_2.wells_by_name()["A12"] NHB2 = reagent_plate.wells_by_name()["A1"] Panel = reagent_plate.wells_by_name()["A2"] @@ -206,6 +276,14 @@ def run(protocol: ProtocolContext) -> None: column_5_list = ["A7", "A8", "A9"] # Plate 2 column_6_list = ["A7", "A8", "A9"] # Plate 1 WASHES = [EEW_1, EEW_2, EEW_3] + if COLUMNS == 4: + column_1_list = ["A1", "A2", "A3", "A4"] # Plate 1 + column_2_list = ["A1", "A2", "A3", "A4"] # Plate 2 + column_3_list = ["A5", "A6", "A7", "A8"] # Plate 2 + column_4_list = ["A5", "A6", "A7", "A8"] # Plate 1 + column_5_list = ["A9", "A10", "A11", "A12"] # Plate 2 + column_6_list = ["A9", "A10", "A11", "A12"] # Plate 1 + WASHES = [EEW_1, EEW_2, EEW_3, EEW_4] def tipcheck() -> None: """Check tips.""" @@ -231,9 +309,7 @@ def tipcheck() -> None: thermocycler.set_block_temperature(58) thermocycler.set_lid_temperature(58) heatershaker.set_and_wait_for_temperature(58) - protocol.pause("Ready") heatershaker.close_labware_latch() - Liquid_trash = Liquid_trash_well_1 # Sample Plate contains 30ul of DNA @@ -409,20 +485,14 @@ def tipcheck() -> None: p1000.pick_up_tip() p1000.move_to(sample_plate_2[X].bottom(4)) p1000.aspirate(200, rate=0.25) - p1000.dispense(200, Liquid_trash.top(z=-7)) + trash_liquid(protocol, p1000, 200, liquid_trash_list) p1000.move_to(sample_plate_2[X].bottom(0.5)) p1000.aspirate(200, rate=0.25) - p1000.dispense(200, Liquid_trash.top(z=-7)) - p1000.move_to(Liquid_trash.top(z=-7)) - protocol.delay(minutes=0.1) - p1000.blow_out(Liquid_trash.top(z=-7)) - p1000.aspirate(20) + trash_liquid(protocol, p1000, 200, liquid_trash_list) p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() p200_tips += 1 tipcheck() - Liquid_trash = Liquid_trash_well_2 - # ============================================================================================ # GRIPPER MOVE sample_plate_2 FROM MAGPLATE TO heatershaker helpers.move_labware_to_hs( @@ -469,9 +539,6 @@ def tipcheck() -> None: if DRYRUN is False: protocol.delay(seconds=1 * 60) - if washcount > 2: - Liquid_trash = Liquid_trash_well_3 - protocol.comment("--> Removing Supernatant") RemoveSup = 200 for loop, X in enumerate(column_2_list): @@ -482,10 +549,7 @@ def tipcheck() -> None: p1000.move_to(sample_plate_2[X].bottom(z=0.5)) p1000.aspirate(100, rate=0.25) p1000.move_to(sample_plate_2[X].top(z=0.5)) - p1000.dispense(200, Liquid_trash.top(z=-7)) - protocol.delay(minutes=0.1) - p1000.blow_out(Liquid_trash.top(z=-7)) - p1000.aspirate(20) + trash_liquid(protocol, p1000, 200, liquid_trash_list) p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() p200_tips += 1 tipcheck() @@ -554,10 +618,7 @@ def tipcheck() -> None: p1000.move_to(sample_plate_2[X].bottom(z=0.5)) p1000.aspirate(100, rate=0.25) p1000.move_to(sample_plate_2[X].top(z=0.5)) - p1000.dispense(200, Liquid_trash.top(z=-7)) - protocol.delay(minutes=0.1) - p1000.blow_out(Liquid_trash.top(z=-7)) - p1000.aspirate(20) + trash_liquid(protocol, p1000, 200, liquid_trash_list) p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() p200_tips += 1 tipcheck() @@ -568,12 +629,7 @@ def tipcheck() -> None: p50.move_to(sample_plate_2[X].bottom(z=dot_bottom)) # original = z=0 p50.aspirate(50, rate=0.25) p50.default_speed = 200 - p50.dispense(50, Liquid_trash.top(z=-7)) - protocol.delay(minutes=0.1) - p50.blow_out() - p50.default_speed = 400 - p50.move_to(Liquid_trash.top(z=-7)) - p50.move_to(Liquid_trash.top(z=0)) + trash_liquid(protocol, p50, 50, liquid_trash_list) p50.return_tip() if TIP_TRASH is False else p50.drop_tip() p50_tips += 1 tipcheck() @@ -722,8 +778,6 @@ def tipcheck() -> None: p50_tips += 1 tipcheck() - Liquid_trash = Liquid_trash_well_4 - protocol.comment("--> ADDING AMPure (0.8x)") AMPureVol = 40.5 AMPureMixRep = 5 * 60 if DRYRUN is False else 0.1 * 60 @@ -777,12 +831,7 @@ def tipcheck() -> None: p1000.default_speed = 5 p1000.move_to(sample_plate_2[X].top(z=2)) p1000.default_speed = 200 - p1000.dispense(200, Liquid_trash.top(z=-7)) - protocol.delay(minutes=0.1) - p1000.blow_out() - p1000.default_speed = 400 - p1000.move_to(Liquid_trash.top(z=-7)) - p1000.move_to(Liquid_trash.top(z=0)) + trash_liquid(protocol, p1000, 200, liquid_trash_list) p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() p200_tips += 1 tipcheck() @@ -821,12 +870,7 @@ def tipcheck() -> None: p1000.default_speed = 5 p1000.move_to(sample_plate_2[X].top(z=2)) p1000.default_speed = 200 - p1000.dispense(200, Liquid_trash.top(z=-7)) - protocol.delay(minutes=0.1) - p1000.blow_out() - p1000.default_speed = 400 - p1000.move_to(Liquid_trash.top(z=-7)) - p1000.move_to(Liquid_trash.top(z=0)) + trash_liquid(protocol, p1000, 200, liquid_trash_list) p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() p200_tips += 1 tipcheck() @@ -842,12 +886,7 @@ def tipcheck() -> None: ) # original = (z=0) p1000.aspirate(50, rate=0.25) p1000.default_speed = 200 - p1000.dispense(50, Liquid_trash.top(z=-7)) - protocol.delay(minutes=0.1) - p1000.blow_out() - p1000.default_speed = 400 - p1000.move_to(Liquid_trash.top(z=-7)) - p1000.move_to(Liquid_trash.top(z=0)) + trash_liquid(protocol, p1000, 50, liquid_trash_list) p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() p200_tips += 1 tipcheck() @@ -945,4 +984,13 @@ def tipcheck() -> None: p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() p200_tips += 1 tipcheck() + liquids_to_probe_at_end = [ + Liquid_trash_well_1, + Liquid_trash_well_2, + Liquid_trash_well_3, + Liquid_trash_well_4, + ] + helpers.find_liquid_height_of_all_wells(protocol, p50, liquids_to_probe_at_end) plate_reader_actions(protocol, plate_reader, hellma_plate, plate_name_str) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/active_protocols/9_Magmax_RNA_Cells_Flex.py b/abr-testing/abr_testing/protocols/active_protocols/9_Magmax_RNA_Cells_Flex.py index 09201e58314..c44e8111490 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/9_Magmax_RNA_Cells_Flex.py +++ b/abr-testing/abr_testing/protocols/active_protocols/9_Magmax_RNA_Cells_Flex.py @@ -24,7 +24,7 @@ } requirements = { - "robotType": "OT-3", + "robotType": "Flex", "apiLevel": "2.21", } """ @@ -44,15 +44,17 @@ Reservoir 1: Well 1 - 8120 ul -Well 2 - 6400 ul -Well 3-7 - 8550 ul +Well 2 - 8120 ul +Well 3 - 12800 ul +Well 4-12 - 9500 ul + """ -whichwash = 1 -sample_max = 48 -tip = 0 +whichwash = 0 +tip_pick_up = 0 drop_count = 0 waste_vol = 0 +wash_volume_tracker = 0.0 # Start protocol @@ -61,22 +63,25 @@ def add_parameters(parameters: ParameterContext) -> None: helpers.create_dot_bottom_parameter(parameters) helpers.create_single_pipette_mount_parameter(parameters) helpers.create_hs_speed_parameter(parameters) + helpers.create_deactivate_modules_parameter(parameters) -def run(ctx: ProtocolContext) -> None: +def run(protocol: ProtocolContext) -> None: """Protocol.""" dry_run = False inc_lysis = True res_type = "nest_12_reservoir_15ml" TIP_TRASH = False - num_samples = 48 + num_samples = 96 wash_vol = 150.0 lysis_vol = 140.0 stop_vol = 100.0 - elution_vol = dnase_vol = 50.0 - heater_shaker_speed = ctx.params.heater_shaker_speed # type: ignore[attr-defined] - dot_bottom = ctx.params.dot_bottom # type: ignore[attr-defined] - pipette_mount = ctx.params.pipette_mount # type: ignore[attr-defined] + elution_vol = dnase_vol = 55.0 + heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] + dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + pipette_mount = protocol.params.pipette_mount # type: ignore[attr-defined] + deactivate_modules_bool = protocol.params.deactivate_modules # type: ignore[attr-defined] + helpers.comment_protocol_version(protocol, "01") # Protocol Parameters deepwell_type = "nest_96_wellplate_2ml_deep" @@ -93,63 +98,67 @@ def run(ctx: ProtocolContext) -> None: drybeads = elute_time = 0.25 bind_time = wash_time = dnase_time = stop_time = 0.25 bead_vol = 20.0 - ctx.load_trash_bin("A3") - h_s: HeaterShakerContext = ctx.load_module(helpers.hs_str, "D1") # type: ignore[assignment] + protocol.load_trash_bin("A3") + h_s: HeaterShakerContext = protocol.load_module( + helpers.hs_str, "D1" + ) # type: ignore[assignment] sample_plate, h_s_adapter = helpers.load_hs_adapter_and_labware( deepwell_type, h_s, "Sample Plate" ) h_s.close_labware_latch() - temp: TemperatureModuleContext = ctx.load_module( + temp: TemperatureModuleContext = protocol.load_module( helpers.temp_str, "D3" ) # type: ignore[assignment] elutionplate, temp_adapter = helpers.load_temp_adapter_and_labware( "armadillo_96_wellplate_200ul_pcr_full_skirt", temp, "Elution Plate" ) temp.set_temperature(4) - magblock: MagneticBlockContext = ctx.load_module( + magblock: MagneticBlockContext = protocol.load_module( helpers.mag_str, "C1" ) # type: ignore[assignment] - waste_reservoir = ctx.load_labware("nest_1_reservoir_195ml", "B3", "Liquid Waste") + waste_reservoir = protocol.load_labware( + "nest_1_reservoir_195ml", "B3", "Liquid Waste" + ) waste = waste_reservoir.wells()[0].top() - res1 = ctx.load_labware(res_type, "D2", "reagent reservoir 1") + res1 = protocol.load_labware(res_type, "D2", "reagent reservoir 1") num_cols = math.ceil(num_samples / 8) # Load tips and combine all similar boxes - tips200 = ctx.load_labware("opentrons_flex_96_tiprack_200ul", "A1", "Tips 1") - tips201 = ctx.load_labware("opentrons_flex_96_tiprack_200ul", "A2", "Tips 2") - tips202 = ctx.load_labware("opentrons_flex_96_tiprack_200ul", "B1", "Tips 3") - tips203 = ctx.load_labware("opentrons_flex_96_tiprack_200ul", "B2", "Tips 4") - tips = [ - *tips200.wells()[num_samples:96], - *tips201.wells(), - *tips202.wells(), - *tips203.wells(), - ] + tips200 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "A1", "Tips 1") + tips201 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "A2", "Tips 2") + tips202 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "B1", "Tips 3") + tips203 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "B2", "Tips 4") + tips204 = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "C2", "Tips 5") + tips_sn = tips200.wells()[:num_samples] # load P1000M pipette - m1000 = ctx.load_instrument( + m1000 = protocol.load_instrument( "flex_8channel_1000", pipette_mount, - tip_racks=[tips200, tips201, tips202, tips203], + tip_racks=[tips200, tips201, tips202, tips203, tips204], ) # Load Liquid Locations in Reservoir elution_solution = elutionplate.rows()[0][:num_cols] - dnase1 = elutionplate.rows()[0][num_cols : 2 * num_cols] - lysis_ = res1.wells()[0] - stopreaction = res1.wells()[1] - wash1 = res1.wells()[2] - wash2 = res1.wells()[3] - wash3 = res1.wells()[4] - wash4 = res1.wells()[5] - wash5 = res1.wells()[6] - + dnase1 = elutionplate.rows()[0][:num_cols] + lysis_ = res1.wells()[0:2] + stopreaction = res1.wells()[2] + wash1 = res1.wells()[3] + wash2 = res1.wells()[4] + wash3 = res1.wells()[5] + wash4 = res1.wells()[6] + wash5 = res1.wells()[7] + wash6 = res1.wells()[8] + wash7 = res1.wells()[9] + wash8 = res1.wells()[10] + wash9 = res1.wells()[11] + all_washes = res1.wells()[3:12] """ Here is where you can define the locations of your reagents. """ samples_m = sample_plate.rows()[0][:num_cols] # 20ul beads each well - cells_m = sample_plate.rows()[0][num_cols : 2 * num_cols] + cells_m = sample_plate.rows()[0][:num_cols] elution_samples_m = elutionplate.rows()[0][:num_cols] # Do the same for color mapping beads_ = sample_plate.wells()[: (8 * num_cols)] @@ -163,36 +172,42 @@ def run(ctx: ProtocolContext) -> None: "Sample": [{"well": cells_, "volume": 0.0}], "DNAse": [{"well": dnase1_, "volume": dnase_vol}], "Elution Buffer": [{"well": elution_samps, "volume": elution_vol}], - "Lysis": [{"well": lysis_, "volume": lysis_vol}], - "Wash 1": [{"well": wash1, "volume": wash_vol}], - "Wash 2": [{"well": wash2, "volume": wash_vol}], - "Wash 3": [{"well": wash3, "volume": wash_vol}], - "Wash 4": [{"well": wash4, "volume": wash_vol}], - "Wash 5": [{"well": wash5, "volume": wash_vol}], - "Stop": [{"well": stopreaction, "volume": stop_vol}], + "Lysis": [{"well": lysis_, "volume": 8120.0}], + "Stop": [{"well": stopreaction, "volume": 6400.0}], + "Wash 1": [{"well": wash1, "volume": 9500.0}], + "Wash 2": [{"well": wash2, "volume": 9500.0}], + "Wash 3": [{"well": wash3, "volume": 9500.0}], + "Wash 4": [{"well": wash4, "volume": 9500.0}], + "Wash 5": [{"well": wash5, "volume": 9500.0}], + "Wash 6": [{"well": wash6, "volume": 9500.0}], + "Wash 7": [{"well": wash7, "volume": 9500.0}], + "Wash 8": [{"well": wash8, "volume": 9500.0}], + "Wash 9": [{"well": wash9, "volume": 9500.0}], } - helpers.find_liquid_height_of_loaded_liquids(ctx, liquid_vols_and_wells, m1000) + helpers.find_liquid_height_of_loaded_liquids(protocol, liquid_vols_and_wells, m1000) m1000.flow_rate.aspirate = 50 m1000.flow_rate.dispense = 150 m1000.flow_rate.blow_out = 300 - def tiptrack(pip: InstrumentContext, tipbox: List[Well]) -> None: + def tiptrack(pip: InstrumentContext) -> None: """Tip Track.""" - global tip + global tip_pick_up global drop_count - pip.pick_up_tip(tipbox[int(tip)]) - tip = tip + 8 + pip.pick_up_tip() + tip_pick_up += 1 drop_count = drop_count + 8 if drop_count >= 250: drop_count = 0 if TIP_TRASH: - ctx.pause("Empty Trash bin.") + protocol.pause("Empty Trash bin.") + if tip_pick_up >= 59: + pip.reset_tipracks() def remove_supernatant(vol: float) -> None: """Remove Supernatant.""" - ctx.comment("-----Removing Supernatant-----") + protocol.comment("-----Removing Supernatant-----") m1000.flow_rate.aspirate = 30 num_trans = math.ceil(vol / 180) vol_per_trans = vol / num_trans @@ -211,7 +226,7 @@ def remove_supernatant(vol: float) -> None: m1000.drop_tip(tips_sn[8 * i]) if TIP_TRASH else m1000.return_tip() m1000.flow_rate.aspirate = 300 # Move Plate From Magnet to H-S - helpers.move_labware_to_hs(ctx, sample_plate, h_s, h_s_adapter) + helpers.move_labware_to_hs(protocol, sample_plate, h_s, h_s_adapter) def bead_mixing( well: Well, pip: InstrumentContext, mvol: float, reps: int = 8 @@ -301,32 +316,39 @@ def mixing(well: Well, pip: InstrumentContext, mvol: float, reps: int = 8) -> No pip.flow_rate.aspirate = 300 pip.flow_rate.dispense = 300 - def lysis(vol: float, source: Well) -> None: + def lysis(vol: float, source: List[Well]) -> None: """Lysis Steps.""" - ctx.comment("-----Beginning lysis steps-----") + tvol_total = 0.0 + protocol.comment("-----Beginning lysis steps-----") num_transfers = math.ceil(vol / 180) - tiptrack(m1000, tips) + tiptrack(m1000) + src = source[0] for i in range(num_cols): - src = source tvol = vol / num_transfers for t in range(num_transfers): m1000.require_liquid_presence(src) m1000.aspirate(tvol, src.bottom(1)) m1000.dispense(m1000.current_volume, cells_m[i].top(-3)) + tvol_total += tvol * 8 + if tvol_total > 8000.0: + protocol.comment("-----Changing to second lysis well.------") + src = source[1] + protocol.comment(f"new source {src}") + tvol_total = 0.0 # mix after adding all reagent to wells with cells for i in range(num_cols): if i != 0: - tiptrack(m1000, tips) + tiptrack(m1000) for x in range(8 if not dry_run else 1): m1000.aspirate(tvol * 0.75, cells_m[i].bottom(dot_bottom)) m1000.dispense(tvol * 0.75, cells_m[i].bottom(8)) if x == 3: - ctx.delay(minutes=0.0167) + protocol.delay(minutes=0.0167) m1000.blow_out(cells_m[i].bottom(1)) m1000.drop_tip() if TIP_TRASH else m1000.return_tip() - helpers.set_hs_speed(ctx, h_s, heater_shaker_speed, lysis_time, True) + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, lysis_time, True) def bind() -> None: """Bind. @@ -344,10 +366,10 @@ def bind() -> None: supernatant to the final clean elutions PCR plate. """ - ctx.comment("-----Beginning bind steps-----") + protocol.comment("-----Beginning bind steps-----") for i, well in enumerate(samples_m): # Transfer cells+lysis/bind to wells with beads - tiptrack(m1000, tips) + tiptrack(m1000) m1000.aspirate(185, cells_m[i].bottom(dot_bottom)) m1000.air_gap(10) m1000.dispense(m1000.current_volume, well.bottom(8)) @@ -355,15 +377,17 @@ def bind() -> None: bead_mixing(well, m1000, 130, reps=5 if not dry_run else 1) m1000.air_gap(10) m1000.drop_tip() if TIP_TRASH else m1000.return_tip() - helpers.set_hs_speed(ctx, h_s, heater_shaker_speed, bind_time, True) + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, bind_time, True) # Transfer from H-S plate to Magdeck plate - helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) for bindi in np.arange( settling_time, 0, -0.5 ): # Settling time delay with countdown timer - ctx.delay( + protocol.delay( minutes=0.5, msg="There are " + str(bindi) + " minutes left in the incubation.", ) @@ -371,45 +395,43 @@ def bind() -> None: # remove initial supernatant remove_supernatant(180) - def wash(vol: float, source: Well) -> None: + def wash(vol: float, source: List[Well]) -> None: """Wash Function.""" global whichwash # Defines which wash the protocol is on to log on the app - - if source == wash1: - whichwash = 1 - if source == wash2: - whichwash = 2 - if source == wash3: - whichwash = 3 - if source == wash4: - whichwash = 4 - - ctx.comment("-----Now starting Wash #" + str(whichwash) + "-----") - - tiptrack(m1000, tips) + protocol.comment("-----Now starting Wash #" + str(whichwash) + "-----") + global wash_volume_tracker + tiptrack(m1000) num_trans = math.ceil(vol / 180) vol_per_trans = vol / num_trans for i, m in enumerate(samples_m): - src = source + src = source[whichwash] for n in range(num_trans): m1000.aspirate(vol_per_trans, src) m1000.air_gap(10) m1000.dispense(m1000.current_volume, m.top(-2)) - ctx.delay(seconds=2) + protocol.delay(seconds=2) m1000.blow_out(m.top(-2)) + wash_volume_tracker += vol_per_trans * 8 + if wash_volume_tracker > 9600: + whichwash += 1 + src = source[whichwash] + protocol.comment(f"new wash source {whichwash}") + wash_volume_tracker = 0.0 m1000.air_gap(10) m1000.drop_tip() if TIP_TRASH else m1000.return_tip() # Shake for 5 minutes to mix wash with beads - helpers.set_hs_speed(ctx, h_s, heater_shaker_speed, wash_time, True) + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, wash_time, True) # Transfer from H-S plate to Magdeck plate - helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) for washi in np.arange( settling_time, 0, -0.5 ): # settling time timer for washes - ctx.delay( + protocol.delay( minutes=0.5, msg="There are " + str(washi) @@ -419,13 +441,14 @@ def wash(vol: float, source: Well) -> None: ) remove_supernatant(vol) + protocol.comment(f"final wash source {whichwash}") def dnase(vol: float, source: List[Well]) -> None: """Steps for DNAseI.""" - ctx.comment("-----DNAseI Steps Beginning-----") + protocol.comment("-----DNAseI Steps Beginning-----") num_trans = math.ceil(vol / 180) vol_per_trans = vol / num_trans - tiptrack(m1000, tips) + tiptrack(m1000) for i, m in enumerate(samples_m): src = source[i] m1000.flow_rate.aspirate = 10 @@ -442,17 +465,17 @@ def dnase(vol: float, source: List[Well]) -> None: # Is this mixing needed? \/\/\/ for i in range(num_cols): if i != 0: - tiptrack(m1000, tips) + tiptrack(m1000) mixing(samples_m[i], m1000, 45, reps=5 if not dry_run else 1) m1000.drop_tip() if TIP_TRASH else m1000.return_tip() # Shake for 10 minutes to mix DNAseI - helpers.set_hs_speed(ctx, h_s, heater_shaker_speed, dnase_time, True) + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, dnase_time, True) def stop_reaction(vol: float, source: Well) -> None: """Adding stop solution.""" - ctx.comment("-----Adding Stop Solution-----") - tiptrack(m1000, tips) + protocol.comment("-----Adding Stop Solution-----") + tiptrack(m1000) num_trans = math.ceil(vol / 180) vol_per_trans = vol / num_trans for i, m in enumerate(samples_m): @@ -467,13 +490,15 @@ def stop_reaction(vol: float, source: Well) -> None: m1000.drop_tip() if TIP_TRASH else m1000.return_tip() # Shake for 3 minutes to mix wash with beads - helpers.set_hs_speed(ctx, h_s, heater_shaker_speed, stop_time, True) + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, stop_time, True) # Transfer from H-S plate to Magdeck plate - helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) for stop in np.arange(settling_time, 0, -0.5): - ctx.delay( + protocol.delay( minutes=0.5, msg="There are " + str(stop) + " minutes left in this incubation.", ) @@ -482,8 +507,8 @@ def stop_reaction(vol: float, source: Well) -> None: def elute(vol: float) -> None: """Elution.""" - ctx.comment("-----Elution Beginning-----") - tiptrack(m1000, tips) + protocol.comment("-----Elution Beginning-----") + tiptrack(m1000) m1000.flow_rate.aspirate = 10 for i, m in enumerate(samples_m): loc = m.top(-2) @@ -498,7 +523,7 @@ def elute(vol: float) -> None: # Is this mixing needed? \/\/\/ for i in range(num_cols): if i != 0: - tiptrack(m1000, tips) + tiptrack(m1000) for mixes in range(10): m1000.aspirate(elution_vol - 10, samples_m[i]) m1000.dispense(elution_vol - 10, samples_m[i].bottom(10)) @@ -510,20 +535,22 @@ def elute(vol: float) -> None: m1000.drop_tip() if TIP_TRASH else m1000.return_tip() # Shake for 3 minutes to mix wash with beads - helpers.set_hs_speed(ctx, h_s, heater_shaker_speed, elute_time, True) + helpers.set_hs_speed(protocol, h_s, heater_shaker_speed, elute_time, True) # Transfer from H-S plate to Magdeck plate - helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) + helpers.move_labware_from_hs_to_destination( + protocol, sample_plate, h_s, magblock + ) for elutei in np.arange(settling_time, 0, -0.5): - ctx.delay( + protocol.delay( minutes=0.5, msg="Incubating on MagDeck for " + str(elutei) + " more minutes.", ) - ctx.comment("-----Trasnferring Sample to Elution Plate-----") + protocol.comment("-----Trasnferring Sample to Elution Plate-----") for i, (m, e) in enumerate(zip(samples_m, elution_samples_m)): - tiptrack(m1000, tips) + tiptrack(m1000) loc = m.bottom(dot_bottom) m1000.transfer(vol, loc, e.bottom(5), air_gap=20, new_tip="never") m1000.blow_out(e.top(-2)) @@ -537,23 +564,27 @@ def elute(vol: float) -> None: if inc_lysis: lysis(lysis_vol, lysis_) bind() - wash(wash_vol, wash1) - wash(wash_vol, wash2) + wash(wash_vol, all_washes) + wash(wash_vol, all_washes) + wash(wash_vol, all_washes) # dnase1 treatment dnase(dnase_vol, dnase1) stop_reaction(stop_vol, stopreaction) # Resume washes - wash(wash_vol, wash3) - wash(wash_vol, wash4) - wash(wash_vol, wash5) + wash(wash_vol, all_washes) + wash(wash_vol, all_washes) + wash(wash_vol, all_washes) for beaddry in np.arange(drybeads, 0, -0.5): - ctx.delay( + protocol.delay( minutes=0.5, msg="There are " + str(beaddry) + " minutes left in the drying step.", ) elute(elution_vol) - - end_list_of_wells_to_probe = [waste_reservoir["A1"], res1["A1"]] - end_list_of_wells_to_probe.extend(elution_samples_m) - helpers.find_liquid_height_of_all_wells(ctx, m1000, end_list_of_wells_to_probe) + end_list_of_wells_to_probe = [waste_reservoir["A1"]] + helpers.clean_up_plates( + m1000, [elutionplate, sample_plate], waste_reservoir["A1"], 200 + ) + helpers.find_liquid_height_of_all_wells(protocol, m1000, end_list_of_wells_to_probe) + if deactivate_modules_bool: + helpers.deactivate_modules(protocol) diff --git a/abr-testing/abr_testing/protocols/csv_parameters/1_samplevols.csv b/abr-testing/abr_testing/protocols/csv_parameters/1_samplevols.csv new file mode 100644 index 00000000000..132b4dc70fb --- /dev/null +++ b/abr-testing/abr_testing/protocols/csv_parameters/1_samplevols.csv @@ -0,0 +1,97 @@ +Well,Dye,Diluent +A1,0,100 +B1,5,95 +C1,10,90 +D1,20,80 +E1,40,60 +F1,15,40 +G1,40,20 +H1,40,0 +A2,35,65 +B2,38,42 +C2,42,58 +D2,32,8 +E2,38,12 +F2,26,74 +G2,31,69 +H2,46,4 +A3,47,13 +B3,42,18 +C3,46,64 +D3,48,22 +E3,26,74 +F3,34,66 +G3,43,37 +H3,20,80 +A4,44,16 +B4,49,41 +C4,48,42 +D4,44,16 +E4,47,53 +F4,47,33 +G4,42,48 +H4,39,21 +A5,30,20 +B5,36,14 +C5,31,59 +D5,38,52 +E5,36,4 +F5,32,28 +G5,35,55 +H5,39,1 +A6,31,59 +B6,20,80 +C6,38,2 +D6,34,46 +E6,30,70 +F6,32,58 +G6,21,79 +H6,38,52 +A7,33,27 +B7,34,16 +C7,40,60 +D7,34,26 +E7,30,20 +F7,44,56 +G7,26,74 +H7,45,55 +A8,39,1 +B8,38,2 +C8,34,66 +D8,39,11 +E8,46,54 +F8,37,63 +G8,38,42 +H8,34,66 +A9,44,56 +B9,39,11 +C9,30,70 +D9,37,33 +E9,46,54 +F9,39,21 +G9,29,41 +H9,23,77 +A10,26,74 +B10,39,1 +C10,31,49 +D10,38,62 +E10,29,1 +F10,21,79 +G10,29,41 +H10,28,42 +A11,15,55 +B11,28,72 +C11,11,49 +D11,34,66 +E11,27,73 +F11,30,40 +G11,33,67 +H11,31,39 +A12,39,31 +B12,47,53 +C12,46,54 +D12,13,7 +E12,34,46 +F12,45,35 +G12,28,42 +H12,37,63 \ No newline at end of file diff --git a/abr-testing/abr_testing/protocols/csv_parameters/2_samplevols.csv b/abr-testing/abr_testing/protocols/csv_parameters/2_samplevols.csv index fa50562e68b..424aae072c3 100644 --- a/abr-testing/abr_testing/protocols/csv_parameters/2_samplevols.csv +++ b/abr-testing/abr_testing/protocols/csv_parameters/2_samplevols.csv @@ -6,20 +6,92 @@ D1,3,7,40,A1 E1,2,8,40,A2 F1,1,9,40,A2 G1,5,5,40,A2 -H1,3,7,40,A3 +H1,3,7,40,A2 A2,3,7,40,A3 B2,3,7,40,A3 C2,3,7,40,A3 D2,3,7,40,A3 -E2,3,7,40,A3 -F2,3,7,40,A3 -G2,3,7,40,A3 -H2,3,7,40,A3 -A3,3,7,40,A3 -B3,3,7,40,A3 -C3,3,7,45,A3 -D3,3,7,45,A3 -E3,3,5,45,A3 -F3,3,5,45,A3 +E2,3,7,40,A4 +F2,3,7,40,A4 +G2,3,7,40,A4 +H2,3,7,40,A4 +A3,3,7,40,A5 +B3,3,7,40,A5 +C3,3,7,45,A5 +D3,3,7,45,A5 +E3,3,5,45,A6 +F3,3,5,45,A6 G3,3,5,45,A6 -H3,3,4,45,A5 \ No newline at end of file +H3,3,4,45,A6 +A4,3,7,40,A1 +B4,0,10,40,A1 +C4,10,0,40,A1 +D4,3,7,40,A1 +E4,2,8,40,A2 +F4,1,9,40,A2 +G4,5,5,40,A2 +H4,3,7,40,A2 +A5,3,7,40,A3 +B5,3,7,40,A3 +C5,3,7,40,A3 +D5,3,7,40,A3 +E5,3,7,40,A4 +F5,3,7,40,A4 +G5,3,7,40,A4 +H5,3,7,40,A4 +A6,3,7,40,A5 +B6,3,7,40,A5 +C6,3,7,45,A5 +D6,3,7,45,A5 +E6,3,5,45,A6 +F6,3,5,45,A6 +G6,3,5,45,A6 +H6,3,4,45,A6 +A7,3,7,40,A1 +B7,0,10,40,A1 +C7,10,0,40,A1 +D7,3,7,40,A1 +E7,2,8,40,A2 +F7,1,9,40,A2 +G7,5,5,40,A2 +H7,3,7,40,A2 +A8,3,7,40,A3 +B8,3,7,40,A3 +C8,3,7,40,A3 +D8,3,7,40,A3 +E8,3,7,40,A4 +F8,3,7,40,A4 +G8,3,7,40,A4 +H8,3,7,40,A4 +A9,3,7,40,A5 +B9,3,7,40,A5 +C9,3,7,45,A5 +D9,3,7,45,A5 +E9,3,5,45,A6 +F9,3,5,45,A6 +G9,3,5,45,A6 +H9,3,4,45,A6 +A10,3,7,40,A1 +B10,0,10,40,A1 +C10,10,0,40,A1 +D10,3,7,40,A1 +E10,2,8,40,A2 +F10,1,9,40,A2 +G10,5,5,40,A2 +H10,3,7,40,A2 +A11,3,7,40,A3 +B11,3,7,40,A3 +C11,3,7,40,A3 +D11,3,7,40,A3 +E11,3,7,40,A4 +F11,3,7,40,A4 +G11,3,7,40,A4 +H11,3,7,40,A4 +A12,3,7,40,A5 +B12,3,7,40,A5 +C12,3,7,45,A5 +D12,3,7,45,A5 +E12,3,5,45,A6 +F12,3,5,45,A6 +G12,3,5,45,A6 +H12,3,4,45,A6 diff --git a/abr-testing/abr_testing/protocols/helpers.py b/abr-testing/abr_testing/protocols/helpers.py index 12abbfa9b3f..31a1d1a9244 100644 --- a/abr-testing/abr_testing/protocols/helpers.py +++ b/abr-testing/abr_testing/protocols/helpers.py @@ -7,14 +7,15 @@ ParameterContext, Well, ) -from typing import Tuple from opentrons.protocol_api.module_contexts import ( HeaterShakerContext, MagneticBlockContext, ThermocyclerContext, TemperatureModuleContext, + MagneticModuleContext, + AbsorbanceReaderContext, ) -from typing import List, Union, Dict +from typing import List, Union, Dict, Tuple from opentrons.hardware_control.modules.types import ThermocyclerStep from opentrons_shared_data.errors.exceptions import PipetteLiquidNotFoundError @@ -106,7 +107,62 @@ def load_temp_adapter_and_labware( return labware_on_temp_mod, temp_adapter +# FUNCTIONS FOR COMMON COMMENTS + + +def comment_protocol_version(protocol: ProtocolContext, version: str) -> None: + """Comment version number of protocol.""" + protocol.comment(f"Protocol Version: {version}") + + # FUNCTIONS FOR LOADING COMMON PARAMETERS +def create_channel_parameter(parameters: ParameterContext) -> None: + """Create pipette channel parameter.""" + parameters.add_str( + variable_name="channels", + display_name="Number of Pipette Channels", + choices=[ + {"display_name": "1 Channel", "value": "1channel"}, + {"display_name": "8 Channel", "value": "8channel"}, + ], + default="8channel", + ) + + +def create_pipette_parameters(parameters: ParameterContext) -> None: + """Create parameter for pipettes.""" + # NOTE: Place function inside def add_parameters(parameters) in protocol. + # NOTE: Copy ctx.params.left mount, ctx.params.right_mount # type: ignore[attr-defined] + # to get result + # Left Mount + parameters.add_str( + variable_name="left_mount", + display_name="Left Mount", + description="Pipette Type on Left Mount.", + choices=[ + {"display_name": "8ch 50ul", "value": "flex_8channel_50"}, + {"display_name": "8ch 1000ul", "value": "flex_8channel_1000"}, + {"display_name": "1ch 50ul", "value": "flex_1channel_50"}, + {"display_name": "1ch 1000ul", "value": "flex_1channel_1000"}, + {"display_name": "96ch 1000ul", "value": "flex_96channel_1000"}, + {"display_name": "None", "value": "none"}, + ], + default="flex_8channel_1000", + ) + # Right Mount + parameters.add_str( + variable_name="right_mount", + display_name="Right Mount", + description="Pipette Type on Right Mount.", + choices=[ + {"display_name": "8ch 50ul", "value": "flex_8channel_50"}, + {"display_name": "8ch 1000ul", "value": "flex_8channel_1000"}, + {"display_name": "1ch 50ul", "value": "flex_1channel_50"}, + {"display_name": "1ch 1000ul", "value": "flex_1channel_1000"}, + {"display_name": "None", "value": "none"}, + ], + default="none", + ) def create_single_pipette_mount_parameter(parameters: ParameterContext) -> None: @@ -163,6 +219,16 @@ def create_disposable_lid_parameter(parameters: ParameterContext) -> None: ) +def create_disposable_lid_trash_location(parameters: ParameterContext) -> None: + """Create a parameter for lid placement after use.""" + parameters.add_bool( + variable_name="trash_lid", + display_name="Trash Disposable Lid", + description="True means trash lid, false means keep on deck.", + default=True, + ) + + def create_tc_lid_deck_riser_parameter(parameters: ParameterContext) -> None: """Create parameter for tc lid deck riser.""" parameters.add_bool( @@ -184,7 +250,7 @@ def create_tip_size_parameter(parameters: ParameterContext) -> None: {"display_name": "200 µL", "value": "opentrons_flex_96_tiprack_200ul"}, {"display_name": "1000 µL", "value": "opentrons_flex_96_tiprack_1000ul"}, ], - default="opentrons_flex_96_tiprack_1000ul", + default="opentrons_flex_96_tiprack_50ul", ) @@ -224,6 +290,25 @@ def create_hs_speed_parameter(parameters: ParameterContext) -> None: ) +def create_plate_reader_compatible_labware_parameter( + parameters: ParameterContext, +) -> None: + """Create parameter for flat bottom plates compatible with plate reader.""" + parameters.add_str( + variable_name="labware_plate_reader_compatible", + display_name="Plate Reader Labware", + default="nest_96_wellplate_200ul_flat", + choices=[ + { + "display_name": "Corning_96well", + "value": "corning_96_wellplate_360ul_flat", + }, + {"display_name": "Hellma Plate", "value": "hellma_reference_plate"}, + {"display_name": "Nest_96well", "value": "nest_96_wellplate_200ul_flat"}, + ], + ) + + def create_tc_compatible_labware_parameter(parameters: ParameterContext) -> None: """Create parameter for labware type compatible with thermocycler.""" parameters.add_str( @@ -249,7 +334,33 @@ def create_tc_compatible_labware_parameter(parameters: ParameterContext) -> None ) +def create_deactivate_modules_parameter(parameters: ParameterContext) -> None: + """Create parameter for deactivating modules at the end fof run.""" + parameters.add_bool( + variable_name="deactivate_modules", + display_name="Deactivate Modules", + description="deactivate all modules at end of run", + default=True, + ) + + # FUNCTIONS FOR COMMON MODULE SEQUENCES +def deactivate_modules(protocol: ProtocolContext) -> None: + """Deactivate all loaded modules.""" + print("Deactivating Modules") + modules = protocol.loaded_modules + + if modules: + for module in modules.values(): + if isinstance(module, HeaterShakerContext): + module.deactivate_shaker() + module.deactivate_heater() + elif isinstance(module, TemperatureModuleContext): + module.deactivate() + elif isinstance(module, MagneticModuleContext): + module.disengage() + elif isinstance(module, ThermocyclerContext): + module.deactivate() def move_labware_from_hs_to_destination( @@ -314,6 +425,39 @@ def use_disposable_lid_with_tc( # FUNCTIONS FOR COMMON PIPETTE COMMAND SEQUENCES +def clean_up_plates( + pipette: InstrumentContext, + list_of_labware: List[Labware], + liquid_waste: Well, + tip_size: int, +) -> None: + """Aspirate liquid from labware and dispense into liquid waste.""" + pipette.pick_up_tip() + pipette.liquid_presence_detection = False + num_of_active_channels = pipette.active_channels + for labware in list_of_labware: + if num_of_active_channels == 8: + list_of_wells = labware.rows()[0] + elif num_of_active_channels == 1: + list_of_wells = labware.wells() + elif num_of_active_channels == 96: + list_of_wells = [labware.wells()[0]] + for well in list_of_wells: + vol_removed = 0.0 + while well.max_volume > vol_removed: + pipette.aspirate(tip_size, well) + pipette.dispense( + tip_size, + liquid_waste.top(), + ) + pipette.blow_out(liquid_waste.top()) + vol_removed += pipette.max_volume + if pipette.channels != num_of_active_channels: + pipette.drop_tip() + else: + pipette.return_tip() + + def find_liquid_height(pipette: InstrumentContext, well_to_probe: Well) -> float: """Find liquid height of well.""" try: @@ -372,6 +516,18 @@ def load_wells_with_custom_liquids( well.load_liquid(liquid, volume) +def comment_height_of_specific_labware( + protocol: ProtocolContext, labware_name: str, dict_of_labware_heights: Dict +) -> None: + """Comment height found of specific labware.""" + total_height = 0.0 + for key in dict_of_labware_heights.keys(): + if key[0] == labware_name: + height = dict_of_labware_heights[key] + total_height += height + protocol.comment(f"Liquid Waste Total Height: {total_height}") + + def find_liquid_height_of_all_wells( protocol: ProtocolContext, pipette: InstrumentContext, @@ -382,7 +538,7 @@ def find_liquid_height_of_all_wells( pipette.pick_up_tip() pip_channels = pipette.active_channels for well in wells: - labware_name = well.parent.load_name + labware_name = well.parent.name total_number_of_wells_in_plate = len(well.parent.wells()) # if pip_channels is > 1 and total_wells > 12 - only probe 1st row. if ( @@ -402,6 +558,9 @@ def find_liquid_height_of_all_wells( pipette.reset_tipracks() msg = f"result: {dict_of_labware_heights}" protocol.comment(msg=msg) + comment_height_of_specific_labware( + protocol, "Liquid Waste", dict_of_labware_heights + ) return dict_of_labware_heights @@ -423,6 +582,8 @@ def find_liquid_height_of_loaded_liquids( entry["well"] if isinstance(entry["well"], list) else [entry["well"]] ) ] + if pipette.active_channels == 96: + wells = [well for well in wells if well.display_name.split(" ")[0] == "A1"] find_liquid_height_of_all_wells(ctx, pipette, wells) return wells @@ -463,6 +624,14 @@ def load_wells_with_water( "#C0C0C0", ] +# Modules with deactivate +ModuleTypes = Union[ + TemperatureModuleContext, + ThermocyclerContext, + HeaterShakerContext, + MagneticModuleContext, + AbsorbanceReaderContext, +] # THERMOCYCLER PROFILES @@ -501,6 +670,3 @@ def perform_pcr( thermocycler.execute_profile( steps=final_extension_profile, repetitions=1, block_max_volume=50 ) - - -# TODO: Create dictionary of labware, module, and adapter. diff --git a/abr-testing/abr_testing/protocols/liquid_setups/10_ZymoBIOMICS Magbead Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/10_ZymoBIOMICS Magbead Liquid Setup.py index 422102e4321..76b72b97164 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/10_ZymoBIOMICS Magbead Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/10_ZymoBIOMICS Magbead Liquid Setup.py @@ -13,7 +13,7 @@ requirements = { "robotType": "Flex", - "apiLevel": "2.20", + "apiLevel": "2.21", } @@ -26,14 +26,16 @@ def run(protocol: protocol_api.ProtocolContext) -> None: p1000, ) = load_common_liquid_setup_labware_and_instruments(protocol) - res1 = protocol.load_labware("nest_12_reservoir_15ml", "C3", "R1") - res2 = protocol.load_labware("nest_12_reservoir_15ml", "B3", "R2") + res1 = protocol.load_labware("nest_12_reservoir_15ml", "D3", "Reagent Reservoir 1") + res2 = protocol.load_labware("nest_12_reservoir_15ml", "C3", "Reagent Reservoir 2") + res3 = protocol.load_labware("nest_12_reservoir_15ml", "B3", "Reagent Reservoir 3") lysis_and_pk = 12320 / 8 beads_and_binding = 11875 / 8 binding2 = 13500 / 8 - wash2 = 9000 / 8 + wash2 = 9800 / 8 wash2_list = [wash2] * 12 + final_elution = 1200 / 8 # Fill up Plates # Res1 p1000.transfer( @@ -42,7 +44,10 @@ def run(protocol: protocol_api.ProtocolContext) -> None: beads_and_binding, beads_and_binding, beads_and_binding, - binding2, + beads_and_binding, + beads_and_binding, + beads_and_binding, + beads_and_binding, binding2, binding2, binding2, @@ -58,6 +63,9 @@ def run(protocol: protocol_api.ProtocolContext) -> None: res1["A6"].top(), res1["A7"].top(), res1["A8"].top(), + res1["A9"].top(), + res1["A10"].top(), + res1["A11"].top(), res1["A12"].top(), ], blow_out=True, @@ -66,10 +74,19 @@ def run(protocol: protocol_api.ProtocolContext) -> None: ) # Res2 p1000.transfer( - volume=wash2_list, - source=source_reservoir["A1"], + volume=[final_elution] + wash2_list[:11], + source=[source_reservoir["A1"]] * 12, dest=res2.wells(), blow_out=True, blowout_location="source well", trash=False, ) + # Res 3 + p1000.transfer( + volume=[wash2, wash2], + source=[source_reservoir["A1"], source_reservoir["A1"]], + dest=[res3["A1"], res3["A2"]], + blow_out=True, + blowout_location="source well", + trash=False, + ) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/11_Dynabeads RIT Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/11_Dynabeads RIT Liquid Setup.py index 112aec315b5..2d722d410e5 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/11_Dynabeads RIT Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/11_Dynabeads RIT Liquid Setup.py @@ -13,7 +13,7 @@ requirements = { "robotType": "Flex", - "apiLevel": "2.20", + "apiLevel": "2.21", } @@ -27,8 +27,11 @@ def run(protocol: protocol_api.ProtocolContext) -> None: ) = load_common_liquid_setup_labware_and_instruments(protocol) reservoir_wash = protocol.load_labware("nest_12_reservoir_15ml", "D2", "Reservoir") - sample_plate = protocol.load_labware( - "nest_96_wellplate_2ml_deep", "C3", "Sample Plate" + sample_plate1 = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "C3", "Sample Plate 1" + ) + sample_plate2 = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "B3", "Sample Plate 2" ) columns = [ @@ -52,10 +55,24 @@ def run(protocol: protocol_api.ProtocolContext) -> None: p1000.dispense(750, reservoir_wash[i].top()) p1000.blow_out(location=source_reservoir["A1"].top()) p1000.return_tip() + # 1 column 6000 uL + p1000.pick_up_tip() + for i in columns: + p1000.aspirate(750, source_reservoir["A1"].bottom(z=0.5)) + p1000.dispense(750, reservoir_wash[i].top()) + p1000.blow_out(location=source_reservoir["A1"].top()) + p1000.return_tip() + # Nest 96 Deep Well Plate 2 mL: 250 uL per well + p1000.pick_up_tip() + for n in columns: + p1000.aspirate(250, source_reservoir["A1"].bottom(z=0.5)) + p1000.dispense(250, sample_plate1[n].bottom(z=1)) + p1000.blow_out(location=source_reservoir["A1"].top()) + p1000.return_tip() # Nest 96 Deep Well Plate 2 mL: 250 uL per well p1000.pick_up_tip() for n in columns: p1000.aspirate(250, source_reservoir["A1"].bottom(z=0.5)) - p1000.dispense(250, sample_plate[n].bottom(z=1)) + p1000.dispense(250, sample_plate2[n].bottom(z=1)) p1000.blow_out(location=source_reservoir["A1"].top()) p1000.return_tip() diff --git a/abr-testing/abr_testing/protocols/liquid_setups/12_KAPA HyperPlus Library Prep Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/12_KAPA HyperPlus Library Prep Liquid Setup.py index 2575caecf6e..688533ffd55 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/12_KAPA HyperPlus Library Prep Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/12_KAPA HyperPlus Library Prep Liquid Setup.py @@ -13,7 +13,7 @@ requirements = { "robotType": "Flex", - "apiLevel": "2.20", + "apiLevel": "2.21", } diff --git a/abr-testing/abr_testing/protocols/liquid_setups/1_Simple normalize long Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/1_Simple normalize long Liquid Setup.py index 2d995fede39..a99bb0568cf 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/1_Simple normalize long Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/1_Simple normalize long Liquid Setup.py @@ -12,7 +12,7 @@ requirements = { "robotType": "Flex", - "apiLevel": "2.20", + "apiLevel": "2.21", } @@ -26,14 +26,22 @@ def run(protocol: protocol_api.ProtocolContext) -> None: ) = load_common_liquid_setup_labware_and_instruments(protocol) reservoir = protocol.load_labware("nest_12_reservoir_15ml", "D2", "Reservoir") # Transfer Liquid - vol = 5400 / 8 + vol = 6175 / 8 columns = ["A1", "A2", "A3", "A4", "A5"] for i in columns: p1000.transfer( vol, - source=source_reservoir["A1"].bottom(z=0.5), + source=source_reservoir["A1"].bottom(z=2), dest=reservoir[i].top(), blowout=True, blowout_location="source well", trash=False, ) + p1000.transfer( + 8500 / 8, + source=source_reservoir["A1"].bottom(z=2), + dest=reservoir["A6"], + blowout=True, + blowout_location="source well", + trash=False, + ) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/2_BMS_PCR_protocol Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/2_BMS_PCR_protocol Liquid Setup.py index a6c71b563d4..1ffefc48f19 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/2_BMS_PCR_protocol Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/2_BMS_PCR_protocol Liquid Setup.py @@ -7,13 +7,13 @@ metadata = { "protocolName": "DVT1ABR2 Liquids: BMS PCR Protocol", - "author": "Rhyann clarke ", + "author": "Rhyann Clarke ", "source": "Protocol Library", } requirements = { "robotType": "Flex", - "apiLevel": "2.20", + "apiLevel": "2.21", } @@ -33,9 +33,9 @@ def run(protocol: protocol_api.ProtocolContext) -> None: ) # Steps # Dispense into plate 1 - p1000.transfer(50, source_reservoir["A1"], pcr_plate_1.wells(), trash=False) + p1000.transfer(100, source_reservoir["A1"], pcr_plate_1.wells(), trash=False) # Dispense p1000.configure_nozzle_layout(protocol_api.SINGLE, start="H1", tip_racks=[tip_rack]) - p1000.transfer(1500, source_reservoir["A1"], snap_caps["B1"]) - p1000.transfer(1500, source_reservoir["A1"], snap_caps.rows()[0]) + p1000.transfer(1000, source_reservoir["A1"], snap_caps["B1"]) + p1000.transfer(1000, source_reservoir["A1"], snap_caps.rows()[0]) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/3_Tartrazine Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/3_Tartrazine Liquid Setup.py index 9e0b29a03ed..f0941bb398b 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/3_Tartrazine Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/3_Tartrazine Liquid Setup.py @@ -1,8 +1,6 @@ """Plate Filler Protocol for Tartrazine Protocol.""" from opentrons import protocol_api -from abr_testing.protocols.helpers import ( - load_common_liquid_setup_labware_and_instruments, -) +from abr_testing.protocols import helpers metadata = { "protocolName": "DVT1ABR3 Liquids: Tartrazine Protocol", @@ -12,36 +10,77 @@ requirements = { "robotType": "Flex", - "apiLevel": "2.20", + "apiLevel": "2.21", } +def add_parameters(parameters: protocol_api.ParameterContext) -> None: + """Add parameters.""" + parameters.add_int( + variable_name="number_of_plates", + display_name="Number of Plates", + default=4, + minimum=1, + maximum=4, + ) + helpers.create_channel_parameter(parameters) + + def run(protocol: protocol_api.ProtocolContext) -> None: """Protocol.""" + number_of_plates = protocol.params.number_of_plates # type: ignore [attr-defined] + channels = protocol.params.channels # type: ignore [attr-defined] # Initiate Labware ( source_reservoir, tip_rack, p1000, - ) = load_common_liquid_setup_labware_and_instruments(protocol) - reagent_tube = protocol.load_labware( - "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", "D3", "Reagent Tube" - ) - p1000.configure_nozzle_layout( - style=protocol_api.SINGLE, start="H1", tip_racks=[tip_rack] - ) - # Transfer Liquid - p1000.transfer( - 45000, - source_reservoir["A1"], - reagent_tube["B3"].top(), - blowout=True, - blowout_location="source well", - ) - p1000.transfer( - 45000, - source_reservoir["A1"], - reagent_tube["A4"].top(), - blowout=True, - blowout_location="source well", - ) + ) = helpers.load_common_liquid_setup_labware_and_instruments(protocol) + if channels == "1channel": + reagent_tube = protocol.load_labware( + "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", "D3", "Reagent Tube" + ) + p1000.configure_nozzle_layout( + style=protocol_api.SINGLE, start="H1", tip_racks=[tip_rack] + ) + # Transfer Liquid + p1000.transfer( + 45000, + source_reservoir["A1"], + reagent_tube["B3"].top(), + blowout=True, + blowout_location="source well", + ) + p1000.transfer( + 45000, + source_reservoir["A1"], + reagent_tube["A4"].top(), + blowout=True, + blowout_location="source well", + ) + elif channels == "8channel": + reservoir = protocol.load_labware("nest_12_reservoir_15ml", "D3", "Reservoir") + water_max_vol = reservoir["A1"].max_volume - 500 + reservoir_wells = reservoir.wells()[ + 1: + ] # Skip A1 as it's reserved for tartrazine + # NEEDED WATER + needed_water: float = ( + float(number_of_plates) * 96.0 * 250.0 + ) # loading extra as a safety factor + # CALCULATING NEEDED # OF WATER WELLS + needed_wells = round(needed_water / water_max_vol) + water_wells = [] + for i in range(needed_wells + 1): + water_wells.append(reservoir_wells[i]) + # Create lists of volumes and source that matches wells to fill + water_max_vol_list = [water_max_vol] * len(water_wells) + source_list = [source_reservoir["A1"]] * len(water_wells) + p1000.transfer( + water_max_vol_list, + source_list, + water_wells, + blowout=True, + blowout_locaiton="source", + trash=False, + ) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/4_Illumina DNA Enrichment Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/4_Illumina DNA Enrichment Liquid Setup.py index 18aee383ace..937c9f4dafd 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/4_Illumina DNA Enrichment Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/4_Illumina DNA Enrichment Liquid Setup.py @@ -12,7 +12,7 @@ requirements = { "robotType": "Flex", - "apiLevel": "2.20", + "apiLevel": "2.21", } @@ -27,7 +27,7 @@ def run(protocol: protocol_api.ProtocolContext) -> None: reservoir_1 = protocol.load_labware( "nest_96_wellplate_2ml_deep", "D2", "Reservoir 1" ) # Reservoir - reservoir_2 = protocol.load_labware( + sample_plate_2 = protocol.load_labware( "thermoscientificnunc_96_wellplate_1300ul", "D3", "Sample Plate 2" ) # Reservoir sample_plate_1 = protocol.load_labware( @@ -57,10 +57,10 @@ def run(protocol: protocol_api.ProtocolContext) -> None: volume=[120, 750, 900, 96], source=source_reservoir["A1"], dest=[ - reservoir_1["A1"].top(), - reservoir_1["A2"].top(), - reservoir_1["A4"].top(), - reservoir_1["A5"].top(), + reservoir_1["A1"].top(), # AMPure + reservoir_1["A2"].top(), # SMB + reservoir_1["A4"].top(), # EtOH + reservoir_1["A5"].top(), # RSB ], blow_out=True, blowout_location="source well", @@ -68,10 +68,10 @@ def run(protocol: protocol_api.ProtocolContext) -> None: ) # Reservoir 2 Plate Prep: dispense liquid into columns 1-9 total 3690 ul - reservoir_2_wells = reservoir_2.wells() + reservoir_2_wells = sample_plate_1.wells() list_of_locations = [well_location.top() for well_location in reservoir_2_wells] p1000.transfer( - volume=[50, 50, 50, 50, 50, 50, 330, 330, 330, 800, 800, 800], + volume=[150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150], source=source_reservoir["A1"], dest=list_of_locations, blow_out=True, @@ -80,9 +80,14 @@ def run(protocol: protocol_api.ProtocolContext) -> None: ) # Sample Plate Prep: total 303 - dest_list = [sample_plate_1["A1"], sample_plate_1["A2"], sample_plate_1["A3"]] + dest_list = [ + sample_plate_2["A9"], + sample_plate_2["A10"], + sample_plate_2["A11"], + sample_plate_2["A12"], + ] p1000.transfer( - volume=[101, 101, 101], + volume=[1000, 1000, 1000, 1000], source=source_reservoir["A1"], dest=dest_list, blow_out=True, diff --git a/abr-testing/abr_testing/protocols/liquid_setups/5_96ch Complex Protocol Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/5_96ch Complex Protocol Liquid Setup.py index cd263318442..e7a726b6b46 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/5_96ch Complex Protocol Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/5_96ch Complex Protocol Liquid Setup.py @@ -5,14 +5,14 @@ ) metadata = { - "protocolName": "DVT2ABR5 and 6 Liquids: 96ch Complex Protocol", + "protocolName": "DVT2ABR5 Liquids: 96ch Complex Protocol", "author": "Rhyann clarke ", "source": "Protocol Library", } requirements = { "robotType": "Flex", - "apiLevel": "2.16", + "apiLevel": "2.21", } @@ -29,7 +29,7 @@ def run(protocol: protocol_api.ProtocolContext) -> None: "nest_96_wellplate_2ml_deep", "D2", "Reservoir" ) # Reservoir - vol = 500 + vol = 1000 column_list = [ "A1", @@ -45,9 +45,9 @@ def run(protocol: protocol_api.ProtocolContext) -> None: "A11", "A12", ] + p1000.pick_up_tip() for i in column_list: - p1000.pick_up_tip() p1000.aspirate(vol, source_reservoir["A1"].bottom(z=0.5)) p1000.dispense(vol, reservoir[i].top()) p1000.blow_out(location=source_reservoir["A1"].top()) - p1000.return_tip() + p1000.return_tip() diff --git a/abr-testing/abr_testing/protocols/liquid_setups/7_HDQ DNA Bacteria Extraction Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/7_HDQ DNA Bacteria Extraction Liquid Setup.py index 4addbd5c7e8..309f9916d03 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/7_HDQ DNA Bacteria Extraction Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/7_HDQ DNA Bacteria Extraction Liquid Setup.py @@ -13,7 +13,7 @@ requirements = { "robotType": "Flex", - "apiLevel": "2.16", + "apiLevel": "2.21", } @@ -55,9 +55,22 @@ def run(protocol: protocol_api.ProtocolContext) -> None: # Sample Plate p1000.transfer( - volume=180, + volume=200, source=source_reservoir["A1"].bottom(z=0.5), - dest=sample_plate["A1"].top(), + dest=[ + sample_plate["A1"].top(), + sample_plate["A2"].top(), + sample_plate["A3"].top(), + sample_plate["A4"].top(), + sample_plate["A5"].top(), + sample_plate["A6"].top(), + sample_plate["A7"].top(), + sample_plate["A8"].top(), + sample_plate["A9"].top(), + sample_plate["A10"].top(), + sample_plate["A11"].top(), + sample_plate["A12"].top(), + ], blowout=True, blowout_location="source well", trash=False, @@ -66,7 +79,20 @@ def run(protocol: protocol_api.ProtocolContext) -> None: p1000.transfer( volume=100, source=source_reservoir["A1"].bottom(z=0.5), - dest=elution_plate["A1"].top(), + dest=[ + elution_plate["A1"].top(), + elution_plate["A2"].top(), + elution_plate["A3"].top(), + elution_plate["A4"].top(), + elution_plate["A5"].top(), + elution_plate["A6"].top(), + elution_plate["A7"].top(), + elution_plate["A8"].top(), + elution_plate["A9"].top(), + elution_plate["A10"].top(), + elution_plate["A11"].top(), + elution_plate["A12"].top(), + ], blowout=True, blowout_location="source well", trash=False, diff --git a/abr-testing/abr_testing/protocols/liquid_setups/9_Thermo MagMax RNA Extraction Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/9_Thermo MagMax RNA Extraction Liquid Setup.py index c6ded28719d..e935063ed16 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/9_Thermo MagMax RNA Extraction Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/9_Thermo MagMax RNA Extraction Liquid Setup.py @@ -13,7 +13,7 @@ requirements = { "robotType": "Flex", - "apiLevel": "2.16", + "apiLevel": "2.21", } @@ -34,15 +34,28 @@ def run(protocol: protocol_api.ProtocolContext) -> None: ) # Volumes + lysis = 8120 / 8 + stop_reaction_vol = 6400 / 8 elution_vol = 55 - well1 = 8120 / 8 - well2 = 6400 / 8 - well3_7 = 8550 / 8 + well4_12 = 9500 / 8 sample_vol = 100 # Reservoir p1000.transfer( - volume=[well1, well2, well3_7, well3_7, well3_7, well3_7, well3_7], + volume=[ + lysis, + lysis, + stop_reaction_vol, + well4_12, + well4_12, + well4_12, + well4_12, + well4_12, + well4_12, + well4_12, + well4_12, + well4_12, + ], source=source_reservoir["A1"].bottom(z=0.2), dest=[ res1["A1"].top(), @@ -52,6 +65,11 @@ def run(protocol: protocol_api.ProtocolContext) -> None: res1["A5"].top(), res1["A6"].top(), res1["A7"].top(), + res1["A8"].top(), + res1["A9"].top(), + res1["A10"].top(), + res1["A11"].top(), + res1["A12"].top(), ], blow_out=True, blowout_location="source well", @@ -94,7 +112,20 @@ def run(protocol: protocol_api.ProtocolContext) -> None: ) # Sample Plate p1000.transfer( - volume=[sample_vol, sample_vol, sample_vol, sample_vol, sample_vol, sample_vol], + volume=[ + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + sample_vol, + ], source=source_reservoir["A1"].bottom(z=0.2), dest=[ sample_plate["A1"].top(), @@ -103,6 +134,12 @@ def run(protocol: protocol_api.ProtocolContext) -> None: sample_plate["A4"].top(), sample_plate["A5"].top(), sample_plate["A6"].top(), + sample_plate["A7"].top(), + sample_plate["A8"].top(), + sample_plate["A9"].top(), + sample_plate["A10"].top(), + sample_plate["A11"].top(), + sample_plate["A12"].top(), ], blow_out=True, blowout_location="source well", diff --git a/abr-testing/abr_testing/protocols/test_protocols/tc_lid_x_offset_test.py b/abr-testing/abr_testing/protocols/test_protocols/tc_lid_x_offset_test.py index df85453ff28..254a1b05514 100644 --- a/abr-testing/abr_testing/protocols/test_protocols/tc_lid_x_offset_test.py +++ b/abr-testing/abr_testing/protocols/test_protocols/tc_lid_x_offset_test.py @@ -23,8 +23,8 @@ def add_parameters(parameters: ParameterContext) -> None: default=5, ) parameters.add_float( - variable_name="x_offset", - display_name="X Offset", + variable_name="offset", + display_name="Z Offset", choices=[ {"display_name": "0.0", "value": 0.0}, {"display_name": "0.1", "value": 0.1}, @@ -62,10 +62,10 @@ def run(protocol: ProtocolContext) -> None: """Runs protocol that moves lids and stacks them.""" # Load Parameters lids_in_stack = protocol.params.lids_in_a_stack # type: ignore[attr-defined] - x_offset = protocol.params.x_offset # type: ignore[attr-defined] + offset = protocol.params.offset # type: ignore[attr-defined] negative = protocol.params.negative # type: ignore[attr-defined] if negative: - x_offset = x_offset * -1 + offset = offset * -1 # Thermocycler thermocycler: ThermocyclerContext = protocol.load_module( "thermocyclerModuleV2" @@ -81,16 +81,16 @@ def run(protocol: ProtocolContext) -> None: lid_stack_4 = helpers.load_disposable_lids(protocol, lids_in_stack, ["C3"]) lid_stack_5 = helpers.load_disposable_lids(protocol, lids_in_stack, ["B3"]) - drop_offset = {"x": x_offset, "y": 0, "z": 0} + pickup_offset = {"x": 0, "y": 0, "z": offset} slot = 0 lids = [lid_stack_1, lid_stack_2, lid_stack_3, lid_stack_4, lid_stack_5] for lid_list in lids: lid_to_move = lid_list[0] lid_to_move_back_to = lid_list[1] - protocol.comment(f"Offset {x_offset}, Lid # {slot+1}") + protocol.comment(f"Offset {offset}, Lid # {slot+1}") # move lid to plate in thermocycler protocol.move_labware( - lid_to_move, plate_in_cycler, use_gripper=True, drop_offset=drop_offset + lid_to_move, plate_in_cycler, use_gripper=True, pick_up_offset=pickup_offset ) protocol.move_labware(lid_to_move, lid_to_move_back_to, use_gripper=True) diff --git a/abr-testing/abr_testing/tools/abr_scale.py b/abr-testing/abr_testing/tools/abr_scale.py index a35fee93fbf..e91abf114b2 100644 --- a/abr-testing/abr_testing/tools/abr_scale.py +++ b/abr-testing/abr_testing/tools/abr_scale.py @@ -149,14 +149,11 @@ def get_most_recent_run_and_record( headers, runs_and_lpc, headers_lpc, - list_of_heights, ) = abr_google_drive.create_data_dictionary( most_recent_run_id, storage_directory, "", - labware, - accuracy, - hellma_plate_standards=hellma_file_values, + hellma_file_values, ) google_sheet_abr_data = google_sheets_tool.google_sheet( credentials_path, "ABR-run-data", tab_number=0 @@ -164,15 +161,6 @@ def get_most_recent_run_and_record( start_row = google_sheet_abr_data.get_index_row() + 1 google_sheet_abr_data.batch_update_cells(runs_and_robots, "A", start_row, "0") print("Wrote run to ABR-run-data") - # Add liquid height detection to abr sheet - google_sheet_ldf = google_sheets_tool.google_sheet( - credentials_path, "ABR-run-data", 4 - ) - start_row_lhd = google_sheet_ldf.get_index_row() + 1 - google_sheet_ldf.batch_update_cells( - list_of_heights, "A", start_row_lhd, "1795535088" - ) - print("Wrote found liquid heights to ABR-run-data") # Add LPC to google sheet google_sheet_lpc = google_sheets_tool.google_sheet( credentials_path, "ABR-LPC", tab_number=0 diff --git a/abr-testing/abr_testing/tools/abr_setup.py b/abr-testing/abr_testing/tools/abr_setup.py index 224ba5bf120..a084c54c4f9 100644 --- a/abr-testing/abr_testing/tools/abr_setup.py +++ b/abr-testing/abr_testing/tools/abr_setup.py @@ -42,7 +42,9 @@ def clean_sheet(sheet_name: str, credentials: str) -> Any: # Check if the date is older than the cutoff if formatted_date < cutoff_date: rem_rows.append(row_id) - if len(rem_rows) > 2000: + + # Limit rem_rows to 1500 at a time + if len(rem_rows) >= 1500: break if len(rem_rows) == 0: # No more rows to remove @@ -53,9 +55,7 @@ def clean_sheet(sheet_name: str, credentials: str) -> Any: sheet.batch_delete_rows(rem_rows) print("deleted rows") except Exception: - print("could not delete rows") - traceback.print_exc() - sys.exit(1) + return clean_sheet(sheet_name, credentials) From 77a6ab529e273233611e81d0c94887f531c7ca9c Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 18 Dec 2024 11:38:49 -0500 Subject: [PATCH 24/25] fix(protocol-designer): unify navigation bar of pd (#17128) * fix(protocol-designer): unify navigation bar of pd --- protocol-designer/src/NavigationBar.tsx | 95 ------------------- protocol-designer/src/ProtocolRoutes.tsx | 22 +++-- protocol-designer/src/atoms/constants.ts | 20 ++++ .../__tests__/LiquidButton.test.tsx | 2 +- .../LiquidButton/index.tsx} | 0 protocol-designer/src/molecules/index.ts | 2 +- .../organisms/AssignLiquidsModal/index.tsx | 4 +- .../__tests__/DesignerNavigation.test.tsx} | 19 ++-- .../index.tsx | 36 ++++--- .../Navigation/__tests__/Navigation.test.tsx} | 24 ++--- .../src/organisms/Navigation/index.tsx | 91 ++++++++++++++++++ .../__tests__/SettingsIcon.test.tsx | 1 + .../SettingsIcon/index.tsx | 52 ++++++++-- protocol-designer/src/organisms/index.ts | 4 +- .../Designer/DeckSetup/DeckSetupTools.tsx | 11 +-- .../StepForm/StepFormToolbox.tsx | 10 +- .../Timeline/TimelineToolbox.tsx | 5 +- .../src/pages/Designer/index.tsx | 4 +- .../pages/Liquids/__tests__/Liquids.test.tsx | 8 +- protocol-designer/src/pages/Liquids/index.tsx | 7 +- protocol-designer/src/pages/index.ts | 6 ++ 21 files changed, 245 insertions(+), 178 deletions(-) delete mode 100644 protocol-designer/src/NavigationBar.tsx rename protocol-designer/src/{organisms/ProtocolNavBar => molecules/LiquidButton}/__tests__/LiquidButton.test.tsx (94%) rename protocol-designer/src/{organisms/ProtocolNavBar/LiquidButton.tsx => molecules/LiquidButton/index.tsx} (100%) rename protocol-designer/src/organisms/{ProtocolNavBar/__tests__/ProtocolNavBar.test.tsx => DesignerNavigation/__tests__/DesignerNavigation.test.tsx} (84%) rename protocol-designer/src/organisms/{ProtocolNavBar => DesignerNavigation}/index.tsx (82%) rename protocol-designer/src/{__tests__/NavigationBar.test.tsx => organisms/Navigation/__tests__/Navigation.test.tsx} (66%) create mode 100644 protocol-designer/src/organisms/Navigation/index.tsx rename protocol-designer/src/{molecules => organisms}/SettingsIcon/__tests__/SettingsIcon.test.tsx (99%) rename protocol-designer/src/{molecules => organisms}/SettingsIcon/index.tsx (54%) create mode 100644 protocol-designer/src/pages/index.ts diff --git a/protocol-designer/src/NavigationBar.tsx b/protocol-designer/src/NavigationBar.tsx deleted file mode 100644 index 7ac392f2b4b..00000000000 --- a/protocol-designer/src/NavigationBar.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import type * as React from 'react' -import { useLocation, useNavigate } from 'react-router-dom' -import styled from 'styled-components' -import { useDispatch, useSelector } from 'react-redux' -import { useTranslation } from 'react-i18next' - -import { - ALIGN_CENTER, - Btn, - COLORS, - CURSOR_POINTER, - DIRECTION_COLUMN, - Flex, - JUSTIFY_SPACE_BETWEEN, - SPACING, - StyledText, -} from '@opentrons/components' -import { toggleNewProtocolModal } from './navigation/actions' -import { actions as loadFileActions } from './load-file' -import { BUTTON_LINK_STYLE } from './atoms' -import { getHasUnsavedChanges } from './load-file/selectors' -import { SettingsIcon } from './molecules' -import type { ThunkDispatch } from './types' - -export function NavigationBar(): JSX.Element | null { - const { t } = useTranslation(['shared', 'alert']) - const location = useLocation() - const navigate = useNavigate() - const dispatch: ThunkDispatch = useDispatch() - const loadFile = ( - fileChangeEvent: React.ChangeEvent - ): void => { - dispatch(loadFileActions.loadProtocolFile(fileChangeEvent)) - dispatch(toggleNewProtocolModal(false)) - } - const hasUnsavedChanges = useSelector(getHasUnsavedChanges) - - const handleCreateNew = (): void => { - if ( - !hasUnsavedChanges || - window.confirm(t('alert:confirm_create_new') as string) - ) { - dispatch(toggleNewProtocolModal(true)) - navigate('/createNew') - } - } - - return location.pathname === '/designer' || - location.pathname === '/liquids' ? null : ( - - - - - {t('opentrons')} - - - {t('protocol_designer')} - - - {t('version', { version: process.env.OT_PD_VERSION })} - - - - {location.pathname === '/createNew' ? null : ( - - - {t('create_new')} - - - )} - - - - {t('import')} - - - - - {location.pathname === '/createNew' ? null : } - - - - ) -} - -const StyledLabel = styled.label` - height: 20px; - cursor: ${CURSOR_POINTER}; - input[type='file'] { - display: none; - } -` diff --git a/protocol-designer/src/ProtocolRoutes.tsx b/protocol-designer/src/ProtocolRoutes.tsx index 7350aa0a8da..4d47caca2c4 100644 --- a/protocol-designer/src/ProtocolRoutes.tsx +++ b/protocol-designer/src/ProtocolRoutes.tsx @@ -1,18 +1,20 @@ import { Route, Navigate, Routes, useNavigate } from 'react-router-dom' import { ErrorBoundary } from 'react-error-boundary' import { Box } from '@opentrons/components' -import { Landing } from './pages/Landing' -import { ProtocolOverview } from './pages/ProtocolOverview' -import { Liquids } from './pages/Liquids' -import { Designer } from './pages/Designer' -import { CreateNewProtocolWizard } from './pages/CreateNewProtocolWizard' -import { NavigationBar } from './NavigationBar' -import { Settings } from './pages/Settings' import { - Kitchen, + CreateNewProtocolWizard, + Designer, + Landing, + Liquids, + ProtocolOverview, + Settings, +} from './pages' +import { FileUploadMessagesModal, - LabwareUploadModal, GateModal, + Kitchen, + LabwareUploadModal, + Navigation, } from './organisms' import { ProtocolDesignerAppFallback } from './resources/ProtocolDesignerAppFallback' @@ -72,7 +74,7 @@ export function ProtocolRoutes(): JSX.Element { FallbackComponent={ProtocolDesignerAppFallback} onReset={handleReset} > - + {showGateModal ? : null} diff --git a/protocol-designer/src/atoms/constants.ts b/protocol-designer/src/atoms/constants.ts index 620a3d10dbc..67393baafd9 100644 --- a/protocol-designer/src/atoms/constants.ts +++ b/protocol-designer/src/atoms/constants.ts @@ -13,6 +13,24 @@ export const BUTTON_LINK_STYLE = css` } ` +export const LINK_BUTTON_STYLE = css` + color: ${COLORS.black90}; + + &:hover { + color: ${COLORS.blue50}; + } + + &:focus-visible { + color: ${COLORS.blue50}; + outline: 2px solid ${COLORS.blue50}; + outline-offset: 0.25rem; + } + + &:disabled { + color: ${COLORS.grey40}; + } +` + export const LINE_CLAMP_TEXT_STYLE = ( lineClamp: number ): FlattenSimpleInterpolation => css` @@ -32,3 +50,5 @@ export const COLUMN_STYLE = css` min-width: calc((${MIN_OVERVIEW_WIDTH} - ${COLUMN_GRID_GAP}) * 0.5); flex: 1; ` + +export const NAV_BAR_HEIGHT_REM = 4 diff --git a/protocol-designer/src/organisms/ProtocolNavBar/__tests__/LiquidButton.test.tsx b/protocol-designer/src/molecules/LiquidButton/__tests__/LiquidButton.test.tsx similarity index 94% rename from protocol-designer/src/organisms/ProtocolNavBar/__tests__/LiquidButton.test.tsx rename to protocol-designer/src/molecules/LiquidButton/__tests__/LiquidButton.test.tsx index 179d512f262..08bd1962583 100644 --- a/protocol-designer/src/organisms/ProtocolNavBar/__tests__/LiquidButton.test.tsx +++ b/protocol-designer/src/molecules/LiquidButton/__tests__/LiquidButton.test.tsx @@ -3,7 +3,7 @@ import { fireEvent, screen } from '@testing-library/react' import { i18n } from '../../../assets/localization' import { renderWithProviders } from '../../../__testing-utils__' -import { LiquidButton } from '../LiquidButton' +import { LiquidButton } from '../../../molecules/LiquidButton' import type { ComponentProps } from 'react' diff --git a/protocol-designer/src/organisms/ProtocolNavBar/LiquidButton.tsx b/protocol-designer/src/molecules/LiquidButton/index.tsx similarity index 100% rename from protocol-designer/src/organisms/ProtocolNavBar/LiquidButton.tsx rename to protocol-designer/src/molecules/LiquidButton/index.tsx diff --git a/protocol-designer/src/molecules/index.ts b/protocol-designer/src/molecules/index.ts index 1c0f2bbb67f..8a65e96180b 100644 --- a/protocol-designer/src/molecules/index.ts +++ b/protocol-designer/src/molecules/index.ts @@ -2,6 +2,6 @@ export * from './CheckboxExpandStepFormField' export * from './CheckboxStepFormField' export * from './DropdownStepFormField' export * from './InputStepFormField' -export * from './SettingsIcon' +export * from './LiquidButton' export * from './ToggleExpandStepFormField' export * from './ToggleStepFormField' diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/index.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/index.tsx index a7c891e7c3c..8372c5886fc 100644 --- a/protocol-designer/src/organisms/AssignLiquidsModal/index.tsx +++ b/protocol-designer/src/organisms/AssignLiquidsModal/index.tsx @@ -21,7 +21,7 @@ import { getSelectedWells } from '../../well-selection/selectors' import { SelectableLabware } from '../Labware/SelectableLabware' import { wellFillFromWellContents } from '../LabwareOnDeck/utils' import { deselectWells, selectWells } from '../../well-selection/actions' -import { PROTOCOL_NAV_BAR_HEIGHT_REM } from '../ProtocolNavBar' +import { NAV_BAR_HEIGHT_REM } from '../../atoms' import { LiquidToolbox } from './LiquidToolbox' import type { WellGroup } from '@opentrons/components' @@ -52,7 +52,7 @@ export function AssignLiquidsModal(): JSX.Element | null { return ( { @@ -21,17 +21,22 @@ vi.mock('react-router-dom', async importOriginal => { return { ...reactRouterDom, useNavigate: () => mockNavigate, + useLocation: () => ({ + location: { + pathname: '/designer', + }, + }), } }) -const render = (props: ComponentProps) => { - return renderWithProviders(, { +const render = (props: ComponentProps) => { + return renderWithProviders(, { i18nInstance: i18n, }) } -describe('ProtocolNavBar', () => { - let props: ComponentProps +describe('DesignerNavigation', () => { + let props: ComponentProps beforeEach(() => { props = { hasZoomInSlot: false, diff --git a/protocol-designer/src/organisms/ProtocolNavBar/index.tsx b/protocol-designer/src/organisms/DesignerNavigation/index.tsx similarity index 82% rename from protocol-designer/src/organisms/ProtocolNavBar/index.tsx rename to protocol-designer/src/organisms/DesignerNavigation/index.tsx index fc9d5bff942..ce0e7a9b3ff 100644 --- a/protocol-designer/src/organisms/ProtocolNavBar/index.tsx +++ b/protocol-designer/src/organisms/DesignerNavigation/index.tsx @@ -1,6 +1,6 @@ import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' -import { useNavigate } from 'react-router-dom' +import { useLocation, useNavigate } from 'react-router-dom' import styled from 'styled-components' import { @@ -18,41 +18,39 @@ import { } from '@opentrons/components' import { getFileMetadata } from '../../file-data/selectors' import { selectTerminalItem } from '../../ui/steps/actions/actions' -import { LINE_CLAMP_TEXT_STYLE } from '../../atoms' +import { LINE_CLAMP_TEXT_STYLE, NAV_BAR_HEIGHT_REM } from '../../atoms' import { useKitchen } from '../Kitchen/hooks' -import { LiquidButton } from './LiquidButton' +import { LiquidButton } from '../../molecules/LiquidButton' import type { StyleProps, TabProps } from '@opentrons/components' -export const PROTOCOL_NAV_BAR_HEIGHT_REM = 4 - -interface ProtocolNavBarProps { +interface DesignerNavigationProps { hasZoomInSlot?: boolean tabs?: TabProps[] hasTrashEntity?: boolean showLiquidOverflowMenu?: (liquidOverflowMenu: boolean) => void - liquidPage?: boolean } - -export function ProtocolNavBar({ +// Note: this navigation is used in design page and liquids page +export function DesignerNavigation({ hasZoomInSlot, tabs = [], hasTrashEntity, showLiquidOverflowMenu, - liquidPage = false, -}: ProtocolNavBarProps): JSX.Element { +}: DesignerNavigationProps): JSX.Element { const { t } = useTranslation('starting_deck_state') + const location = useLocation() const metadata = useSelector(getFileMetadata) const { makeSnackbar } = useKitchen() const navigate = useNavigate() const dispatch = useDispatch() + const isLiquidsPage = location.pathname === '/liquids' - const showProtocolEditButtons = !(hasZoomInSlot || liquidPage) + const showProtocolEditButtons = !(hasZoomInSlot === true || isLiquidsPage) let metadataText = t('edit_protocol') - if (liquidPage) { + if (isLiquidsPage) { metadataText = t('add_liquid') - } else if (hasZoomInSlot) { + } else if (hasZoomInSlot === true) { metadataText = t('add_hardware_labware') } return ( @@ -78,10 +76,10 @@ export function ProtocolNavBar({ ) : null} - {liquidPage ? null : ( + {isLiquidsPage ? null : ( { - if (hasTrashEntity) { + if (hasTrashEntity === true) { navigate('/overview') dispatch(selectTerminalItem('__initial_setup__')) } else { @@ -100,12 +98,12 @@ export function ProtocolNavBar({ const NavContainer = styled(Flex)<{ showShadow: boolean }>` z-index: ${props => (props.showShadow === true ? 11 : 0)}; padding: ${SPACING.spacing12}; - height: ${PROTOCOL_NAV_BAR_HEIGHT_REM}rem; + height: ${NAV_BAR_HEIGHT_REM}rem; width: 100%; justify-content: ${JUSTIFY_SPACE_BETWEEN}; align-items: ${ALIGN_CENTER}; box-shadow: ${props => - props.showShadow + props.showShadow === true ? `0px 1px 3px 0px ${COLORS.black90}${COLORS.opacity20HexCode}` : 'none'}; ` @@ -119,7 +117,7 @@ const MetadataContainer = styled.div.withConfig({ display: flex; flex-direction: ${DIRECTION_COLUMN}; text-align: ${props => - props.showProtocolEditButtons === true + props.showProtocolEditButtons ? TYPOGRAPHY.textAlignCenter : TYPOGRAPHY.textAlignLeft}; diff --git a/protocol-designer/src/__tests__/NavigationBar.test.tsx b/protocol-designer/src/organisms/Navigation/__tests__/Navigation.test.tsx similarity index 66% rename from protocol-designer/src/__tests__/NavigationBar.test.tsx rename to protocol-designer/src/organisms/Navigation/__tests__/Navigation.test.tsx index deac271d6d5..90d0fe4b917 100644 --- a/protocol-designer/src/__tests__/NavigationBar.test.tsx +++ b/protocol-designer/src/organisms/Navigation/__tests__/Navigation.test.tsx @@ -2,27 +2,27 @@ import { describe, it, vi, beforeEach, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' -import { i18n } from '../assets/localization' -import { renderWithProviders } from '../__testing-utils__' -import { NavigationBar } from '../NavigationBar' -import { getHasUnsavedChanges } from '../load-file/selectors' -import { toggleNewProtocolModal } from '../navigation/actions' -import { SettingsIcon } from '../molecules' +import { i18n } from '../../../assets/localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { getHasUnsavedChanges } from '../../../load-file/selectors' +import { toggleNewProtocolModal } from '../../../navigation/actions' +import { SettingsIcon } from '../../SettingsIcon' +import { Navigation } from '..' -vi.mock('../molecules') -vi.mock('../navigation/actions') -vi.mock('../file-data/selectors') -vi.mock('../load-file/selectors') +vi.mock('../../SettingsIcon') +vi.mock('../../../navigation/actions') +vi.mock('../../../file-data/selectors') +vi.mock('../../../load-file/selectors') const render = () => { return renderWithProviders( - + , { i18nInstance: i18n } ) } -describe('NavigationBar', () => { +describe('Navigation', () => { beforeEach(() => { vi.mocked(getHasUnsavedChanges).mockReturnValue(false) vi.mocked(SettingsIcon).mockReturnValue(
mock SettingsIcon
) diff --git a/protocol-designer/src/organisms/Navigation/index.tsx b/protocol-designer/src/organisms/Navigation/index.tsx new file mode 100644 index 00000000000..0f5b423ddc6 --- /dev/null +++ b/protocol-designer/src/organisms/Navigation/index.tsx @@ -0,0 +1,91 @@ +import { useLocation, useNavigate } from 'react-router-dom' +import styled from 'styled-components' +import { useDispatch, useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' + +import { + ALIGN_CENTER, + Btn, + COLORS, + CURSOR_POINTER, + Flex, + JUSTIFY_SPACE_BETWEEN, + SPACING, + StyledText, +} from '@opentrons/components' +import { toggleNewProtocolModal } from '../../navigation/actions' +import { actions as loadFileActions } from '../../load-file' +import { LINK_BUTTON_STYLE } from '../../atoms' +import { getHasUnsavedChanges } from '../../load-file/selectors' +import { SettingsIcon } from '../SettingsIcon' +import type { ThunkDispatch } from '../../types' + +export function Navigation(): JSX.Element | null { + const { t } = useTranslation(['shared', 'alert']) + const location = useLocation() + const navigate = useNavigate() + const dispatch: ThunkDispatch = useDispatch() + const loadFile = ( + fileChangeEvent: React.ChangeEvent + ): void => { + dispatch(loadFileActions.loadProtocolFile(fileChangeEvent)) + dispatch(toggleNewProtocolModal(false)) + } + const hasUnsavedChanges = useSelector(getHasUnsavedChanges) + + const handleCreateNew = (): void => { + if ( + !hasUnsavedChanges || + window.confirm(t('alert:confirm_create_new') as string) + ) { + dispatch(toggleNewProtocolModal(true)) + navigate('/createNew') + } + } + + return location.pathname === '/designer' || + location.pathname === '/liquids' ? null : ( + + + + {t('opentrons')} + + + {t('protocol_designer')} + + + {t('version', { version: process.env.OT_PD_VERSION })} + + + + {location.pathname === '/createNew' ? null : ( + + + {t('create_new')} + + + )} + + + + {t('import')} + + + + + {location.pathname === '/createNew' ? null : } + + + ) +} + +const StyledLabel = styled.label` + height: 1.25rem; + cursor: ${CURSOR_POINTER}; + input[type='file'] { + display: none; + } +` diff --git a/protocol-designer/src/molecules/SettingsIcon/__tests__/SettingsIcon.test.tsx b/protocol-designer/src/organisms/SettingsIcon/__tests__/SettingsIcon.test.tsx similarity index 99% rename from protocol-designer/src/molecules/SettingsIcon/__tests__/SettingsIcon.test.tsx rename to protocol-designer/src/organisms/SettingsIcon/__tests__/SettingsIcon.test.tsx index ee2258b0a7c..8c4a9e1ec2b 100644 --- a/protocol-designer/src/molecules/SettingsIcon/__tests__/SettingsIcon.test.tsx +++ b/protocol-designer/src/organisms/SettingsIcon/__tests__/SettingsIcon.test.tsx @@ -3,6 +3,7 @@ import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../__testing-utils__' import { getFileMetadata } from '../../../file-data/selectors' import { SettingsIcon } from '..' + import type { NavigateFunction } from 'react-router-dom' const mockNavigate = vi.fn() diff --git a/protocol-designer/src/molecules/SettingsIcon/index.tsx b/protocol-designer/src/organisms/SettingsIcon/index.tsx similarity index 54% rename from protocol-designer/src/molecules/SettingsIcon/index.tsx rename to protocol-designer/src/organisms/SettingsIcon/index.tsx index 0953ce992cb..934d6371b2c 100644 --- a/protocol-designer/src/molecules/SettingsIcon/index.tsx +++ b/protocol-designer/src/organisms/SettingsIcon/index.tsx @@ -1,17 +1,18 @@ import { useSelector } from 'react-redux' import { useLocation, useNavigate } from 'react-router-dom' +import { css } from 'styled-components' + import { BORDERS, Btn, COLORS, + CURSOR_POINTER, Flex, Icon, JUSTIFY_CENTER, } from '@opentrons/components' import { getFileMetadata } from '../../file-data/selectors' -import { BUTTON_LINK_STYLE } from '../../atoms/constants' -// TODO(ja): this icon needs to be updated to match css states and correct svg export const SettingsIcon = (): JSX.Element => { const location = useLocation() const navigate = useNavigate() @@ -32,22 +33,57 @@ export const SettingsIcon = (): JSX.Element => { data-testid="SettingsIcon" borderRadius={BORDERS.borderRadiusFull} backgroundColor={ - location.pathname === '/settings' ? COLORS.grey30 : COLORS.transparent + location.pathname === '/settings' ? COLORS.grey35 : COLORS.transparent } - cursor="pointer" - width="2rem" - height="2rem" + cursor={CURSOR_POINTER} justifyContent={JUSTIFY_CENTER} > - +
) } + +const GEAR_ICON_STYLE = css` + width: 2rem; + height: 2rem; + border-radius: 50%; + color: ${COLORS.grey60}; + + &:hover { + background-color: ${COLORS.grey30}; + } + + &:active { + color: ${COLORS.grey60}; + background-color: ${COLORS.grey35}; + } + + &:focus-visible { + position: relative; + outline: none; + + /* blue ring */ + &::after { + content: ''; + position: absolute; + top: -0.5rem; + left: -0.5rem; + right: -0.5rem; + bottom: -0.5rem; + + border: 3px solid ${COLORS.blue50}; + border-radius: 50%; + pointer-events: none; + box-sizing: content-box; + } + background-color: ${COLORS.grey35}; + } +` diff --git a/protocol-designer/src/organisms/index.ts b/protocol-designer/src/organisms/index.ts index 0bf328fed4e..cbab9d62f8e 100644 --- a/protocol-designer/src/organisms/index.ts +++ b/protocol-designer/src/organisms/index.ts @@ -6,6 +6,7 @@ export * from './BlockingHintModal' export * from './ConfirmDeleteModal' export * from './ConfirmDeleteStagingAreaModal' export * from './DefineLiquidsModal' +export * from './DesignerNavigation' export * from './DisabledScreen' export * from './EditInstrumentsModal' export * from './EditNickNameModal' @@ -17,10 +18,11 @@ export * from './Kitchen' export * from './KnowledgeLink' export * from './LabwareOnDeck' export * from './LabwareUploadModal' +export * from './Navigation' export * from './PipetteInfoItem' export * from './Portal' -export * from './ProtocolNavBar' export * from './SelectWellsModal' +export * from './SettingsIcon' export * from './SlotDetailsContainer' export * from './SlotInformation' export * from './TipPositionModal' diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index ac0f56b377e..e93ec99d887 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -52,11 +52,8 @@ import { selectors } from '../../../labware-ingred/selectors' import { useKitchen } from '../../../organisms/Kitchen/hooks' import { getDismissedHints } from '../../../tutorial/selectors' import { createContainerAboveModule } from '../../../step-forms/actions/thunks' -import { - ConfirmDeleteStagingAreaModal, - PROTOCOL_NAV_BAR_HEIGHT_REM, -} from '../../../organisms' -import { BUTTON_LINK_STYLE } from '../../../atoms' +import { BUTTON_LINK_STYLE, NAV_BAR_HEIGHT_REM } from '../../../atoms' +import { ConfirmDeleteStagingAreaModal } from '../../../organisms' import { getSlotInformation } from '../utils' import { ALL_ORDERED_CATEGORIES, FIXTURES, MOAM_MODELS } from './constants' import { LabwareTools } from './LabwareTools' @@ -371,7 +368,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { position === POSITION_FIXED ? { right: SPACING.spacing12, - top: `calc(${PROTOCOL_NAV_BAR_HEIGHT_REM}rem + ${SPACING.spacing12})`, + top: `calc(${NAV_BAR_HEIGHT_REM}rem + ${SPACING.spacing12})`, } : {} return ( @@ -398,7 +395,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { ) : null} {changeModuleWarning} { diff --git a/protocol-designer/src/pages/Designer/index.tsx b/protocol-designer/src/pages/Designer/index.tsx index f9f343735d4..410faa44739 100644 --- a/protocol-designer/src/pages/Designer/index.tsx +++ b/protocol-designer/src/pages/Designer/index.tsx @@ -17,7 +17,7 @@ import { selectTerminalItem } from '../../ui/steps/actions/actions' import { useKitchen } from '../../organisms/Kitchen/hooks' import { getDeckSetupForActiveItem } from '../../top-selectors/labware-locations' import { generateNewProtocol } from '../../labware-ingred/actions' -import { DefineLiquidsModal, ProtocolNavBar } from '../../organisms' +import { DefineLiquidsModal, DesignerNavigation } from '../../organisms' import { selectDesignerTab } from '../../file-data/actions' import { getDesignerTab, getFileMetadata } from '../../file-data/selectors' import { DeckSetupContainer } from './DeckSetup' @@ -151,7 +151,7 @@ export function Designer(): JSX.Element { /> ) : null} - { vi.mocked(AssignLiquidsModal).mockReturnValue(
mock AssignLiquidsModal
) - vi.mocked(ProtocolNavBar).mockReturnValue(
mock ProtocolNavBar
) + vi.mocked(DesignerNavigation).mockReturnValue( +
mock DesignerNavigation
+ ) vi.mocked(LiquidsOverflowMenu).mockReturnValue(
mock LiquidsOverflowMenu
) @@ -55,7 +57,7 @@ describe('Liquids', () => { it('renders nav and assign liquids modal', () => { render() - screen.getByText('mock ProtocolNavBar') + screen.getByText('mock DesignerNavigation') screen.getByText('mock AssignLiquidsModal') }) }) diff --git a/protocol-designer/src/pages/Liquids/index.tsx b/protocol-designer/src/pages/Liquids/index.tsx index c8382498863..3604f9e3d24 100644 --- a/protocol-designer/src/pages/Liquids/index.tsx +++ b/protocol-designer/src/pages/Liquids/index.tsx @@ -9,7 +9,7 @@ import { import { AssignLiquidsModal, DefineLiquidsModal, - ProtocolNavBar, + DesignerNavigation, } from '../../organisms' import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' import { LiquidsOverflowMenu } from '../Designer/LiquidsOverflowMenu' @@ -59,10 +59,7 @@ export function Liquids(): JSX.Element { ) : null} - + diff --git a/protocol-designer/src/pages/index.ts b/protocol-designer/src/pages/index.ts new file mode 100644 index 00000000000..c80a9b799ce --- /dev/null +++ b/protocol-designer/src/pages/index.ts @@ -0,0 +1,6 @@ +export * from './CreateNewProtocolWizard' +export * from './Designer' +export * from './Landing' +export * from './Liquids' +export * from './ProtocolOverview/' +export * from './Settings' From 9f0bc7d3efd5cc6ac03807a3ef9b637213a0a16d Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:17:11 -0500 Subject: [PATCH 25/25] fix(protocol-designer): remove console log (#17139) remove console log --- .../src/pages/ProtocolOverview/LiquidDefinitions.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/protocol-designer/src/pages/ProtocolOverview/LiquidDefinitions.tsx b/protocol-designer/src/pages/ProtocolOverview/LiquidDefinitions.tsx index e9da36c4bae..b55b615eb11 100644 --- a/protocol-designer/src/pages/ProtocolOverview/LiquidDefinitions.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/LiquidDefinitions.tsx @@ -65,7 +65,6 @@ export function LiquidDefinitions({ {Object.keys(allIngredientGroupFields).length > 0 ? ( Object.values(allIngredientGroupFields).map((liquid, index) => { - console.log(getLiquidDescription(liquid, enableLiquidClasses)) return (