From 9d16384320715389a1418014694d2a7da0c59a60 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 8 Oct 2024 09:31:54 -0400 Subject: [PATCH] fix(app,api): Display thermocycler profile cycles (#16414) We identified a simple bug with the app: it displays wonky numbers for thermocycler profile cycles: ![image (1)](https://github.com/user-attachments/assets/8005c0b6-c55e-4b48-bf50-7d90f91fa63b) This wouldn't repeat anything at all, what's going on? Well, it turns out that when Protocol Designer added thermocycler support, they wanted something a bit more in depth than the Python protocol API's "list of steps, number of times to repeat the cycle" format. They wanted users to be able to add single steps and cycles, in arbitrary order. To make the two styles work with the engine, we made the engine's `thermocycler/runProfile` command take a flat list of steps - any of the structure of repeated commands was removed. In the app's `CommandText` display, we then had to fix up the way we displayed profiles to remove references to a `repetitions` value that now didn't exist... and we just didn't, instead setting the `repetitions` element to just be the number of steps in the profile. Fixing this ended up being a bit involved. ## summary - ad172608c64c8d00844a7748ae282a1c8284b745 Make a new interface for the hardware controller `execute_profile` function of the thermocycler, since it's important that thermocycler cycles execute inside an asyncio worker so that they won't be interrupted by e.g. pausing. This new interface takes the PD style of protocol. It relies under the hood on the same function that actually sends stuff to the thermocycler, so there shouldn't be any functional execution changes. - 335e25ad61ec36927060993a21342c20ec325c52 Make a new `thermocycler/runExtendedProfile` command in the protocol engine, that takes as its parameters a structured list of either steps or cycles, adhering to PD's expectations since the PAPI's expectations are a strict subset of PD. This implements its command by using the new hardware controller. - e842a2a47da30a62437c093516292b48a676369b Use the new structured data to implement a command text for the new command that can render the equivalent of what PD makes (and, again, what the PAPI does, which is a strict subset) - e842a2a47da30a62437c093516292b48a676369b Also make the old command text not use a "repetitions" keyword that doesn't exist - 0043ab7cf281b4dff4277c1350831671232559ea Switch the PAPI to emitting the new command in api 2.21 ## new UI These UI changes will be on https://s3-us-west-2.amazonaws.com/opentrons-components/rqa-2771-thermocycler-extended-profiles/index.html?path=/docs/app-molecules-command-commandtext--docs for the desktop and general text and https://s3-us-west-2.amazonaws.com/opentrons-components/rqa-2771-thermocycler-extended-profiles/index.html?path=/docs/app-molecules-command-command--docs for the ODD colored-background elements whenever they upload. Here's a screenshot of what they were like when the PR was opened: `CommandText` of `thermocycler/runProfile` : ![runProfileCommandText](https://github.com/user-attachments/assets/dcad4ce8-2f0e-418d-8618-066a084dc832) Note the change to the first line. This needs wordsmithing. `CommandText` of `thermocycler/runExtendedProfile`: ![runProfileExtended](https://github.com/user-attachments/assets/6b1a1977-9200-4d78-84dc-2961d76cfff9) Note the two-level rendering. The common case will probably be that there's only cycles ## todo - [x] wordsmith - ~[ ] add to PD?~ no, but structured in a way that it will be easy eventually - [x] Test. Oh boy Closes RQA-2771 --------- Co-authored-by: Ed Cormany --- .../hardware_control/modules/thermocycler.py | 61 +- .../hardware_control/modules/types.py | 5 + .../protocol_api/core/engine/module_core.py | 56 +- .../opentrons/protocol_api/module_contexts.py | 10 +- api/src/opentrons/protocol_api/validation.py | 3 +- .../commands/command_unions.py | 5 + .../commands/thermocycler/__init__.py | 19 + .../thermocycler/run_extended_profile.py | 166 + .../opentrons/protocols/api_support/types.py | 15 +- .../modules/test_hc_thermocycler.py | 61 + .../core/engine/test_thermocycler_core.py | 81 +- .../thermocycler/test_run_extended_profile.py | 115 + .../en/protocol_command_text.json | 6 +- app/src/molecules/Command/Command.stories.tsx | 22 +- app/src/molecules/Command/CommandText.tsx | 163 +- .../Command/__fixtures__/doItAllV10.json | 4863 +++++++++++++++++ .../molecules/Command/__fixtures__/index.ts | 2 +- .../Command/__tests__/CommandText.test.tsx | 130 +- app/src/molecules/Command/hooks/index.ts | 2 + .../hooks/useCommandTextString/index.tsx | 58 +- .../getTCRunExtendedProfileCommandText.ts | 67 + .../utils/getTCRunProfileCommandText.ts | 15 +- .../hooks/useCommandTextString/utils/index.ts | 1 + .../CategorizedStepContent.stories.tsx | 14 +- .../__tests__/useRecoveryToasts.test.tsx | 34 +- .../hooks/useRecoveryToasts.ts | 24 +- .../__tests__/formatDuration.test.tsx | 55 +- .../transformations/formatDuration.ts | 39 +- shared-data/command/schemas/10.json | 106 + shared-data/command/types/module.ts | 23 + 30 files changed, 6132 insertions(+), 89 deletions(-) create mode 100644 api/src/opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py create mode 100644 api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_extended_profile.py create mode 100644 app/src/molecules/Command/__fixtures__/doItAllV10.json create mode 100644 app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunExtendedProfileCommandText.ts diff --git a/api/src/opentrons/hardware_control/modules/thermocycler.py b/api/src/opentrons/hardware_control/modules/thermocycler.py index bcaac8650d9..4a1b2fe038b 100644 --- a/api/src/opentrons/hardware_control/modules/thermocycler.py +++ b/api/src/opentrons/hardware_control/modules/thermocycler.py @@ -2,7 +2,7 @@ import asyncio import logging -from typing import Callable, Optional, List, Dict, Mapping +from typing import Callable, Optional, List, Dict, Mapping, Union, cast from opentrons.drivers.rpi_drivers.types import USBPort from opentrons.drivers.types import ThermocyclerLidStatus, Temperature, PlateTemperature from opentrons.hardware_control.modules.lid_temp_status import LidTemperatureStatus @@ -363,6 +363,39 @@ async def cycle_temperatures( self.make_cancellable(task) await task + async def execute_profile( + self, + profile: List[Union[types.ThermocyclerCycle, types.ThermocyclerStep]], + volume: Optional[float] = None, + ) -> None: + """Begin a set temperature profile, with both repeating and non-repeating steps. + + Args: + profile: The temperature profile to follow. + volume: Optional volume + + Returns: None + """ + await self.wait_for_is_running() + self._total_cycle_count = 0 + self._total_step_count = 0 + self._current_cycle_index = 0 + self._current_step_index = 0 + for step_or_cycle in profile: + if "steps" in step_or_cycle: + # basically https://github.com/python/mypy/issues/14766 + this_cycle = cast(types.ThermocyclerCycle, step_or_cycle) + self._total_cycle_count += this_cycle["repetitions"] + self._total_step_count += ( + len(this_cycle["steps"]) * this_cycle["repetitions"] + ) + else: + self._total_step_count += 1 + self._total_cycle_count += 1 + task = self._loop.create_task(self._execute_profile(profile, volume)) + self.make_cancellable(task) + await task + async def set_lid_temperature(self, temperature: float) -> None: """Set the lid temperature in degrees Celsius""" await self.wait_for_is_running() @@ -574,7 +607,7 @@ async def _execute_cycles( self, steps: List[types.ThermocyclerStep], repetitions: int, - volume: Optional[float] = None, + volume: Optional[float], ) -> None: """ Execute cycles. @@ -592,6 +625,30 @@ async def _execute_cycles( self._current_step_index = step_idx + 1 # science starts at 1 await self._execute_cycle_step(step, volume) + async def _execute_profile( + self, + profile: List[Union[types.ThermocyclerCycle, types.ThermocyclerStep]], + volume: Optional[float], + ) -> None: + """ + Execute profiles. + + Profiles command a thermocycler pattern that can contain multiple cycles and out-of-cycle steps. + """ + self._current_cycle_index = 0 + self._current_step_index = 0 + for step_or_cycle in profile: + self._current_cycle_index += 1 + if "repetitions" in step_or_cycle: + # basically https://github.com/python/mypy/issues/14766 + this_cycle = cast(types.ThermocyclerCycle, step_or_cycle) + for rep in range(this_cycle["repetitions"]): + for step in this_cycle["steps"]: + self._current_step_index += 1 + await self._execute_cycle_step(step, volume) + else: + await self._execute_cycle_step(step_or_cycle, volume) + # TODO(mc, 2022-10-13): why does this exist? # Do the driver and poller really need to be disconnected? # Could we accomplish the same thing by latching the error state diff --git a/api/src/opentrons/hardware_control/modules/types.py b/api/src/opentrons/hardware_control/modules/types.py index 6c9f6a3e915..9b7c33058d4 100644 --- a/api/src/opentrons/hardware_control/modules/types.py +++ b/api/src/opentrons/hardware_control/modules/types.py @@ -39,6 +39,11 @@ class ThermocyclerStep(ThermocyclerStepBase, total=False): hold_time_minutes: float +class ThermocyclerCycle(TypedDict): + steps: List[ThermocyclerStep] + repetitions: int + + UploadFunction = Callable[[str, str, Dict[str, Any]], Awaitable[Tuple[bool, str]]] diff --git a/api/src/opentrons/protocol_api/core/engine/module_core.py b/api/src/opentrons/protocol_api/core/engine/module_core.py index 729037425a8..47b49c54e23 100644 --- a/api/src/opentrons/protocol_api/core/engine/module_core.py +++ b/api/src/opentrons/protocol_api/core/engine/module_core.py @@ -1,14 +1,13 @@ """Protocol API module implementation logic.""" from __future__ import annotations -from typing import Optional, List, Dict +from typing import Optional, List, Dict, Union from opentrons.hardware_control import SynchronousAdapter, modules as hw_modules from opentrons.hardware_control.modules.types import ( ModuleModel, TemperatureStatus, MagneticStatus, - ThermocyclerStep, SpeedStatus, module_model_from_string, ) @@ -27,7 +26,7 @@ CannotPerformModuleAction, ) -from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.api_support.types import APIVersion, ThermocyclerStep from ... import validation from ..module import ( @@ -327,15 +326,13 @@ def wait_for_lid_temperature(self) -> None: cmd.thermocycler.WaitForLidTemperatureParams(moduleId=self.module_id) ) - def execute_profile( + def _execute_profile_pre_221( self, steps: List[ThermocyclerStep], repetitions: int, - block_max_volume: Optional[float] = None, + block_max_volume: Optional[float], ) -> None: - """Execute a Thermocycler Profile.""" - self._repetitions = repetitions - self._step_count = len(steps) + """Execute a thermocycler profile using thermocycler/runProfile and flattened steps.""" engine_steps = [ cmd.thermocycler.RunProfileStepParams( celsius=step["temperature"], @@ -352,6 +349,49 @@ def execute_profile( ) ) + def _execute_profile_post_221( + self, + steps: List[ThermocyclerStep], + repetitions: int, + block_max_volume: Optional[float], + ) -> None: + """Execute a thermocycler profile using thermocycler/runExtendedProfile.""" + engine_steps: List[ + Union[cmd.thermocycler.ProfileCycle, cmd.thermocycler.ProfileStep] + ] = [ + cmd.thermocycler.ProfileCycle( + repetitions=repetitions, + steps=[ + cmd.thermocycler.ProfileStep( + celsius=step["temperature"], + holdSeconds=step["hold_time_seconds"], + ) + for step in steps + ], + ) + ] + self._engine_client.execute_command( + cmd.thermocycler.RunExtendedProfileParams( + moduleId=self.module_id, + profileElements=engine_steps, + blockMaxVolumeUl=block_max_volume, + ) + ) + + def execute_profile( + self, + steps: List[ThermocyclerStep], + repetitions: int, + block_max_volume: Optional[float] = None, + ) -> None: + """Execute a Thermocycler Profile.""" + self._repetitions = repetitions + self._step_count = len(steps) + if self.api_version >= APIVersion(2, 21): + return self._execute_profile_post_221(steps, repetitions, block_max_volume) + else: + return self._execute_profile_pre_221(steps, repetitions, block_max_volume) + def deactivate_lid(self) -> None: """Turn off the heated lid.""" self._engine_client.execute_command( diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index f9fcc18ca00..5d182843dcc 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -8,10 +8,9 @@ from opentrons_shared_data.module.types import ModuleModel, ModuleType from opentrons.legacy_broker import LegacyBroker -from opentrons.hardware_control.modules import ThermocyclerStep from opentrons.legacy_commands import module_commands as cmds from opentrons.legacy_commands.publisher import CommandPublisher, publish -from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.api_support.types import APIVersion, ThermocyclerStep from opentrons.protocols.api_support.util import ( APIVersionError, requires_version, @@ -629,6 +628,13 @@ def execute_profile( ``hold_time_minutes`` and ``hold_time_seconds`` must be defined and for each step. + .. note: + + Before API Version 2.21, Thermocycler profiles run with this command + would be listed in the app as having a number of repetitions equal to + their step count. At or above API Version 2.21, the structure of the + Thermocycler cycles is preserved. + """ repetitions = validation.ensure_thermocycler_repetition_count(repetitions) validated_steps = validation.ensure_thermocycler_profile_steps(steps) diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index 08e56fdef8f..43c83eca2e0 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -18,7 +18,7 @@ from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.robot.types import RobotType -from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.api_support.types import APIVersion, ThermocyclerStep from opentrons.protocols.api_support.util import APIVersionError from opentrons.protocols.models import LabwareDefinition from opentrons.types import Mount, DeckSlotName, StagingSlotName, Location @@ -30,7 +30,6 @@ HeaterShakerModuleModel, MagneticBlockModel, AbsorbanceReaderModel, - ThermocyclerStep, ) from .disposal_locations import TrashBin, WasteChute diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index f01e10aa63e..2c7f768945f 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -380,6 +380,7 @@ thermocycler.OpenLid, thermocycler.CloseLid, thermocycler.RunProfile, + thermocycler.RunExtendedProfile, absorbance_reader.CloseLid, absorbance_reader.OpenLid, absorbance_reader.Initialize, @@ -456,6 +457,7 @@ thermocycler.OpenLidParams, thermocycler.CloseLidParams, thermocycler.RunProfileParams, + thermocycler.RunExtendedProfileParams, absorbance_reader.CloseLidParams, absorbance_reader.OpenLidParams, absorbance_reader.InitializeParams, @@ -530,6 +532,7 @@ thermocycler.OpenLidCommandType, thermocycler.CloseLidCommandType, thermocycler.RunProfileCommandType, + thermocycler.RunExtendedProfileCommandType, absorbance_reader.CloseLidCommandType, absorbance_reader.OpenLidCommandType, absorbance_reader.InitializeCommandType, @@ -605,6 +608,7 @@ thermocycler.OpenLidCreate, thermocycler.CloseLidCreate, thermocycler.RunProfileCreate, + thermocycler.RunExtendedProfileCreate, absorbance_reader.CloseLidCreate, absorbance_reader.OpenLidCreate, absorbance_reader.InitializeCreate, @@ -681,6 +685,7 @@ thermocycler.OpenLidResult, thermocycler.CloseLidResult, thermocycler.RunProfileResult, + thermocycler.RunExtendedProfileResult, absorbance_reader.CloseLidResult, absorbance_reader.OpenLidResult, absorbance_reader.InitializeResult, diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/__init__.py b/api/src/opentrons/protocol_engine/commands/thermocycler/__init__.py index b0ffdd53ce9..60e5c62591c 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/__init__.py @@ -73,6 +73,16 @@ RunProfileCreate, ) +from .run_extended_profile import ( + RunExtendedProfileCommandType, + RunExtendedProfileParams, + RunExtendedProfileResult, + RunExtendedProfile, + RunExtendedProfileCreate, + ProfileCycle, + ProfileStep, +) + __all__ = [ # Set target block temperature command models @@ -130,4 +140,13 @@ "RunProfileResult", "RunProfile", "RunProfileCreate", + # Run extended profile command models. + "RunExtendedProfileCommandType", + "RunExtendedProfileParams", + "RunExtendedProfileStepParams", + "RunExtendedProfileResult", + "RunExtendedProfile", + "RunExtendedProfileCreate", + "ProfileCycle", + "ProfileStep", ] diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py b/api/src/opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py new file mode 100644 index 00000000000..3cf8a67bf41 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py @@ -0,0 +1,166 @@ +"""Command models to execute a Thermocycler profile.""" +from __future__ import annotations +from typing import List, Optional, TYPE_CHECKING, overload, Union +from typing_extensions import Literal, Type + +from pydantic import BaseModel, Field + +from opentrons.hardware_control.modules.types import ThermocyclerStep, ThermocyclerCycle + +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence + +if TYPE_CHECKING: + from opentrons.protocol_engine.state.state import StateView + from opentrons.protocol_engine.execution import EquipmentHandler + from opentrons.protocol_engine.state.module_substates.thermocycler_module_substate import ( + ThermocyclerModuleSubState, + ) + + +RunExtendedProfileCommandType = Literal["thermocycler/runExtendedProfile"] + + +class ProfileStep(BaseModel): + """An individual step in a Thermocycler extended profile.""" + + celsius: float = Field(..., description="Target temperature in °C.") + holdSeconds: float = Field( + ..., description="Time to hold target temperature in seconds." + ) + + +class ProfileCycle(BaseModel): + """An individual cycle in a Thermocycler extended profile.""" + + steps: List[ProfileStep] = Field(..., description="Steps to repeat.") + repetitions: int = Field(..., description="Number of times to repeat the steps.") + + +class RunExtendedProfileParams(BaseModel): + """Input parameters for an individual Thermocycler profile step.""" + + moduleId: str = Field(..., description="Unique ID of the Thermocycler.") + profileElements: List[Union[ProfileStep, ProfileCycle]] = Field( + ..., + description="Elements of the profile. Each can be either a step or a cycle.", + ) + blockMaxVolumeUl: Optional[float] = Field( + None, + description="Amount of liquid in uL of the most-full well" + " in labware loaded onto the thermocycler.", + ) + + +class RunExtendedProfileResult(BaseModel): + """Result data from running a Thermocycler profile.""" + + +def _transform_profile_step( + step: ProfileStep, thermocycler_state: ThermocyclerModuleSubState +) -> ThermocyclerStep: + + return ThermocyclerStep( + temperature=thermocycler_state.validate_target_block_temperature(step.celsius), + hold_time_seconds=step.holdSeconds, + ) + + +@overload +def _transform_profile_element( + element: ProfileStep, thermocycler_state: ThermocyclerModuleSubState +) -> ThermocyclerStep: + ... + + +@overload +def _transform_profile_element( + element: ProfileCycle, thermocycler_state: ThermocyclerModuleSubState +) -> ThermocyclerCycle: + ... + + +def _transform_profile_element( + element: Union[ProfileStep, ProfileCycle], + thermocycler_state: ThermocyclerModuleSubState, +) -> Union[ThermocyclerStep, ThermocyclerCycle]: + if isinstance(element, ProfileStep): + return _transform_profile_step(element, thermocycler_state) + else: + return ThermocyclerCycle( + steps=[ + _transform_profile_step(step, thermocycler_state) + for step in element.steps + ], + repetitions=element.repetitions, + ) + + +class RunExtendedProfileImpl( + AbstractCommandImpl[ + RunExtendedProfileParams, SuccessData[RunExtendedProfileResult, None] + ] +): + """Execution implementation of a Thermocycler's run profile command.""" + + def __init__( + self, + state_view: StateView, + equipment: EquipmentHandler, + **unused_dependencies: object, + ) -> None: + self._state_view = state_view + self._equipment = equipment + + async def execute( + self, params: RunExtendedProfileParams + ) -> SuccessData[RunExtendedProfileResult, None]: + """Run a Thermocycler profile.""" + thermocycler_state = self._state_view.modules.get_thermocycler_module_substate( + params.moduleId + ) + thermocycler_hardware = self._equipment.get_module_hardware_api( + thermocycler_state.module_id + ) + + profile = [ + _transform_profile_element(element, thermocycler_state) + for element in params.profileElements + ] + target_volume: Optional[float] + if params.blockMaxVolumeUl is not None: + target_volume = thermocycler_state.validate_max_block_volume( + params.blockMaxVolumeUl + ) + else: + target_volume = None + + if thermocycler_hardware is not None: + # TODO(jbl 2022-06-27) hardcoded constant 1 for `repetitions` should be + # moved from HardwareControlAPI to the Python ProtocolContext + await thermocycler_hardware.execute_profile( + profile=profile, volume=target_volume + ) + + return SuccessData(public=RunExtendedProfileResult(), private=None) + + +class RunExtendedProfile( + BaseCommand[RunExtendedProfileParams, RunExtendedProfileResult, ErrorOccurrence] +): + """A command to execute a Thermocycler profile run.""" + + commandType: RunExtendedProfileCommandType = "thermocycler/runExtendedProfile" + params: RunExtendedProfileParams + result: Optional[RunExtendedProfileResult] + + _ImplementationCls: Type[RunExtendedProfileImpl] = RunExtendedProfileImpl + + +class RunExtendedProfileCreate(BaseCommandCreate[RunExtendedProfileParams]): + """A request to execute a Thermocycler profile run.""" + + commandType: RunExtendedProfileCommandType = "thermocycler/runExtendedProfile" + params: RunExtendedProfileParams + + _CommandCls: Type[RunExtendedProfile] = RunExtendedProfile diff --git a/api/src/opentrons/protocols/api_support/types.py b/api/src/opentrons/protocols/api_support/types.py index 6d3af89bcf9..d16fa8ddf73 100644 --- a/api/src/opentrons/protocols/api_support/types.py +++ b/api/src/opentrons/protocols/api_support/types.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import NamedTuple +from typing import NamedTuple, TypedDict class APIVersion(NamedTuple): @@ -17,3 +17,16 @@ def from_string(cls, inp: str) -> APIVersion: def __str__(self) -> str: return f"{self.major}.{self.minor}" + + +class ThermocyclerStepBase(TypedDict): + """Required elements of a thermocycler step: the temperature.""" + + temperature: float + + +class ThermocyclerStep(ThermocyclerStepBase, total=False): + """Optional elements of a thermocycler step: the hold time. One of these must be present.""" + + hold_time_seconds: float + hold_time_minutes: float diff --git a/api/tests/opentrons/hardware_control/modules/test_hc_thermocycler.py b/api/tests/opentrons/hardware_control/modules/test_hc_thermocycler.py index d893d9912d0..6e90068ac1f 100644 --- a/api/tests/opentrons/hardware_control/modules/test_hc_thermocycler.py +++ b/api/tests/opentrons/hardware_control/modules/test_hc_thermocycler.py @@ -329,6 +329,67 @@ async def test_cycle_temperature( ) +async def test_execute_profile( + set_temperature_subject: modules.Thermocycler, set_plate_temp_spy: mock.AsyncMock +) -> None: + """It should send a series of set_plate_temperatures from a profile.""" + await set_temperature_subject.execute_profile( + [ + {"temperature": 42, "hold_time_seconds": 30}, + { + "repetitions": 5, + "steps": [ + {"temperature": 20, "hold_time_minutes": 1}, + {"temperature": 30, "hold_time_seconds": 1}, + ], + }, + {"temperature": 90, "hold_time_seconds": 2}, + { + "repetitions": 10, + "steps": [ + {"temperature": 10, "hold_time_minutes": 2}, + {"temperature": 20, "hold_time_seconds": 5}, + ], + }, + ], + volume=123, + ) + assert set_plate_temp_spy.call_args_list == [ + mock.call(temp=42, hold_time=30, volume=123), + mock.call(temp=20, hold_time=60, volume=123), + mock.call(temp=30, hold_time=1, volume=123), + mock.call(temp=20, hold_time=60, volume=123), + mock.call(temp=30, hold_time=1, volume=123), + mock.call(temp=20, hold_time=60, volume=123), + mock.call(temp=30, hold_time=1, volume=123), + mock.call(temp=20, hold_time=60, volume=123), + mock.call(temp=30, hold_time=1, volume=123), + mock.call(temp=20, hold_time=60, volume=123), + mock.call(temp=30, hold_time=1, volume=123), + mock.call(temp=90, hold_time=2, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + ] + + async def test_sync_error_response_to_poller( subject_mocked_driver: modules.Thermocycler, mock_driver: mock.AsyncMock, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_thermocycler_core.py b/api/tests/opentrons/protocol_api/core/engine/test_thermocycler_core.py index eb429065d0a..1ee868ad84b 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_thermocycler_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_thermocycler_core.py @@ -1,5 +1,7 @@ """Tests for the engine based Protocol API module core implementations.""" +from typing import cast import pytest +from _pytest.fixtures import SubRequest from decoy import Decoy from opentrons.drivers.types import ThermocyclerLidStatus @@ -13,6 +15,8 @@ from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocol_api.core.engine.module_core import ThermocyclerModuleCore from opentrons.protocol_api import MAX_SUPPORTED_VERSION +from opentrons.protocols.api_support.types import APIVersion +from ... import versions_below, versions_at_or_above SyncThermocyclerHardware = SynchronousAdapter[Thermocycler] @@ -34,7 +38,7 @@ def subject( mock_engine_client: EngineClient, mock_sync_module_hardware: SyncThermocyclerHardware, ) -> ThermocyclerModuleCore: - """Get a HeaterShakerModuleCore test subject.""" + """Get a ThermocyclerModuleCore test subject.""" return ThermocyclerModuleCore( module_id="1234", engine_client=mock_engine_client, @@ -43,6 +47,36 @@ def subject( ) +@pytest.fixture(params=versions_below(APIVersion(2, 21), flex_only=False)) +def subject_below_221( + request: SubRequest, + mock_engine_client: EngineClient, + mock_sync_module_hardware: SyncThermocyclerHardware, +) -> ThermocyclerModuleCore: + """Get a ThermocyclerCore below API version 2.21.""" + return ThermocyclerModuleCore( + module_id="1234", + engine_client=mock_engine_client, + api_version=cast(APIVersion, request.param), + sync_module_hardware=mock_sync_module_hardware, + ) + + +@pytest.fixture(params=versions_at_or_above(APIVersion(2, 21))) +def subject_at_or_above_221( + request: SubRequest, + mock_engine_client: EngineClient, + mock_sync_module_hardware: SyncThermocyclerHardware, +) -> ThermocyclerModuleCore: + """Get a ThermocyclerCore below API version 2.21.""" + return ThermocyclerModuleCore( + module_id="1234", + engine_client=mock_engine_client, + api_version=cast(APIVersion, request.param), + sync_module_hardware=mock_sync_module_hardware, + ) + + def test_create( decoy: Decoy, mock_engine_client: EngineClient, @@ -159,11 +193,13 @@ def test_wait_for_lid_temperature( ) -def test_execute_profile( - decoy: Decoy, mock_engine_client: EngineClient, subject: ThermocyclerModuleCore +def test_execute_profile_below_221( + decoy: Decoy, + mock_engine_client: EngineClient, + subject_below_221: ThermocyclerModuleCore, ) -> None: """It should run a thermocycler profile with the engine client.""" - subject.execute_profile( + subject_below_221.execute_profile( steps=[{"temperature": 45.6, "hold_time_seconds": 12.3}], repetitions=2, block_max_volume=78.9, @@ -187,6 +223,43 @@ def test_execute_profile( ) +def test_execute_profile_above_221( + decoy: Decoy, + mock_engine_client: EngineClient, + subject_at_or_above_221: ThermocyclerModuleCore, +) -> None: + """It should run a thermocycler profile with the engine client.""" + subject_at_or_above_221.execute_profile( + steps=[ + {"temperature": 45.6, "hold_time_seconds": 12.3}, + {"temperature": 78.9, "hold_time_seconds": 45.6}, + ], + repetitions=2, + block_max_volume=25, + ) + decoy.verify( + mock_engine_client.execute_command( + cmd.thermocycler.RunExtendedProfileParams( + moduleId="1234", + profileElements=[ + cmd.thermocycler.ProfileCycle( + repetitions=2, + steps=[ + cmd.thermocycler.ProfileStep( + celsius=45.6, holdSeconds=12.3 + ), + cmd.thermocycler.ProfileStep( + celsius=78.9, holdSeconds=45.6 + ), + ], + ) + ], + blockMaxVolumeUl=25, + ) + ) + ) + + def test_deactivate_lid( decoy: Decoy, mock_engine_client: EngineClient, subject: ThermocyclerModuleCore ) -> None: diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_extended_profile.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_extended_profile.py new file mode 100644 index 00000000000..9dcefceb9f1 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_extended_profile.py @@ -0,0 +1,115 @@ +"""Test Thermocycler run profile command implementation.""" +from typing import List, Union + +from decoy import Decoy + +from opentrons.hardware_control.modules import Thermocycler + +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.module_substates import ( + ThermocyclerModuleSubState, + ThermocyclerModuleId, +) +from opentrons.protocol_engine.execution import EquipmentHandler +from opentrons.protocol_engine.commands import thermocycler as tc_commands +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.thermocycler.run_extended_profile import ( + RunExtendedProfileImpl, + ProfileStep, + ProfileCycle, +) + + +async def test_run_extended_profile( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, +) -> None: + """It should be able to execute the specified module's profile run.""" + subject = RunExtendedProfileImpl(state_view=state_view, equipment=equipment) + + step_data: List[Union[ProfileStep, ProfileCycle]] = [ + ProfileStep(celsius=12.3, holdSeconds=45), + ProfileCycle( + steps=[ + ProfileStep(celsius=78.9, holdSeconds=910), + ProfileStep(celsius=12, holdSeconds=1), + ], + repetitions=2, + ), + ProfileStep(celsius=45.6, holdSeconds=78), + ProfileCycle( + steps=[ + ProfileStep(celsius=56, holdSeconds=11), + ProfileStep(celsius=34, holdSeconds=10), + ], + repetitions=1, + ), + ] + data = tc_commands.RunExtendedProfileParams( + moduleId="input-thermocycler-id", + profileElements=step_data, + blockMaxVolumeUl=56.7, + ) + expected_result = tc_commands.RunExtendedProfileResult() + + tc_module_substate = decoy.mock(cls=ThermocyclerModuleSubState) + tc_hardware = decoy.mock(cls=Thermocycler) + + decoy.when( + state_view.modules.get_thermocycler_module_substate("input-thermocycler-id") + ).then_return(tc_module_substate) + + decoy.when(tc_module_substate.module_id).then_return( + ThermocyclerModuleId("thermocycler-id") + ) + + # Stub temperature validation from hs module view + decoy.when(tc_module_substate.validate_target_block_temperature(12.3)).then_return( + 32.1 + ) + decoy.when(tc_module_substate.validate_target_block_temperature(78.9)).then_return( + 78.9 + ) + decoy.when(tc_module_substate.validate_target_block_temperature(12)).then_return(12) + decoy.when(tc_module_substate.validate_target_block_temperature(45.6)).then_return( + 65.4 + ) + decoy.when(tc_module_substate.validate_target_block_temperature(56)).then_return(56) + decoy.when(tc_module_substate.validate_target_block_temperature(34)).then_return(34) + + # Stub volume validation from hs module view + decoy.when(tc_module_substate.validate_max_block_volume(56.7)).then_return(76.5) + + # Get attached hardware modules + decoy.when( + equipment.get_module_hardware_api(ThermocyclerModuleId("thermocycler-id")) + ).then_return(tc_hardware) + + result = await subject.execute(data) + + decoy.verify( + await tc_hardware.execute_profile( + profile=[ + {"temperature": 32.1, "hold_time_seconds": 45}, + { + "steps": [ + {"temperature": 78.9, "hold_time_seconds": 910}, + {"temperature": 12, "hold_time_seconds": 1}, + ], + "repetitions": 2, + }, + {"temperature": 65.4, "hold_time_seconds": 78}, + { + "steps": [ + {"temperature": 56, "hold_time_seconds": 11}, + {"temperature": 34, "hold_time_seconds": 10}, + ], + "repetitions": 1, + }, + ], + volume=76.5, + ), + times=1, + ) + assert result == SuccessData(public=expected_result, private=None) diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index 17d60a8f967..c78aef13785 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -71,8 +71,10 @@ "slot": "Slot {{slot_name}}", "target_temperature": "target temperature", "tc_awaiting_for_duration": "Waiting for Thermocycler profile to complete", - "tc_run_profile_steps": "temperature: {{celsius}}°C, seconds: {{seconds}}", - "tc_starting_profile": "Thermocycler starting {{repetitions}} repetitions of cycle composed of the following steps:", + "tc_run_profile_steps": "Temperature: {{celsius}}°C, hold time: {{duration}}", + "tc_starting_extended_profile_cycle": "{{repetitions}} repetitions of the following steps:", + "tc_starting_extended_profile": "Running thermocycler profile with {{elementCount}} total steps and cycles:", + "tc_starting_profile": "Running thermocycler profile with {{stepCount}} steps:", "touch_tip": "Touching tip", "trash_bin_in_slot": "Trash Bin in {{slot_name}}", "unlatching_hs_latch": "Unlatching labware on Heater-Shaker", diff --git a/app/src/molecules/Command/Command.stories.tsx b/app/src/molecules/Command/Command.stories.tsx index 1fe64215ef3..43e81fa5541 100644 --- a/app/src/molecules/Command/Command.stories.tsx +++ b/app/src/molecules/Command/Command.stories.tsx @@ -18,7 +18,7 @@ interface StorybookArgs { } const availableCommandTypes = uniq( - Fixtures.mockQIASeqTextData.commands.map(command => command.commandType) + Fixtures.mockDoItAllTextData.commands.map(command => command.commandType) ) const commandsByType: Partial> = {} @@ -26,7 +26,7 @@ function commandsOfType(type: CommandType): RunTimeCommand[] { if (type in commandsByType) { return commandsByType[type] } - commandsByType[type] = Fixtures.mockQIASeqTextData.commands.filter( + commandsByType[type] = Fixtures.mockDoItAllTextData.commands.filter( command => command.commandType === type ) return commandsByType[type] @@ -43,8 +43,8 @@ function safeCommandOfType(type: CommandType, index: number): RunTimeCommand { function Wrapper(props: StorybookArgs): JSX.Element { const command = props.selectCommandBy === 'protocol index' - ? Fixtures.mockQIASeqTextData.commands[ - props.commandIndex < Fixtures.mockQIASeqTextData.commands.length + ? Fixtures.mockDoItAllTextData.commands[ + props.commandIndex < Fixtures.mockDoItAllTextData.commands.length ? props.commandIndex : -1 ] @@ -52,7 +52,7 @@ function Wrapper(props: StorybookArgs): JSX.Element { return command == null ? null : ( = { control: { type: 'range', min: 0, - max: Fixtures.mockQIASeqTextData.commands.length - 1, + max: Fixtures.mockDoItAllTextData.commands.length - 1, }, defaultValue: 0, if: { arg: 'selectCommandBy', eq: 'protocol index' }, @@ -161,6 +161,16 @@ export const ThermocyclerProfile: Story = { }, } +export const ThermocyclerExtendedProfile: Story = { + args: { + selectCommandBy: 'command type', + commandType: 'thermocycler/runExtendedProfile', + commandTypeIndex: 0, + aligned: 'left', + state: 'current', + }, +} + export const VeryLongCommand: Story = { args: { selectCommandBy: 'command type', diff --git a/app/src/molecules/Command/CommandText.tsx b/app/src/molecules/Command/CommandText.tsx index 70fb5281817..00c7337104b 100644 --- a/app/src/molecules/Command/CommandText.tsx +++ b/app/src/molecules/Command/CommandText.tsx @@ -16,6 +16,10 @@ import { useCommandTextString } from './hooks' import type { RobotType, RunTimeCommand } from '@opentrons/shared-data' import type { StyleProps } from '@opentrons/components' import type { CommandTextData } from './types' +import type { + GetTCRunExtendedProfileCommandTextResult, + GetTCRunProfileCommandTextResult, +} from './hooks' interface LegacySTProps { as?: React.ComponentProps['as'] @@ -39,22 +43,35 @@ interface BaseProps extends StyleProps { propagateTextLimit?: boolean } export function CommandText(props: BaseProps & STProps): JSX.Element | null { - const { commandText, stepTexts } = useCommandTextString({ + const commandText = useCommandTextString({ ...props, }) - switch (props.command.commandType) { + switch (commandText.kind) { case 'thermocycler/runProfile': { return ( + ) + } + case 'thermocycler/runExtendedProfile': { + return ( + ) } default: { - return {commandText} + return ( + + {commandText.commandText} + + ) } } } @@ -91,11 +108,18 @@ function CommandStyledText( } } +const shouldPropagateCenter = ( + propagateCenter: boolean, + isOnDevice?: boolean +): boolean => isOnDevice === true || propagateCenter +const shouldPropagateTextLimit = ( + propagateTextLimit: boolean, + isOnDevice?: boolean +): boolean => isOnDevice === true || propagateTextLimit + type ThermocyclerRunProfileProps = BaseProps & - STProps & { - commandText: string - stepTexts?: string[] - } + STProps & + Omit function ThermocyclerRunProfile( props: ThermocyclerRunProfileProps @@ -109,9 +133,6 @@ function ThermocyclerRunProfile( ...styleProps } = props - const shouldPropagateCenter = isOnDevice === true || propagateCenter - const shouldPropagateTextLimit = isOnDevice === true || propagateTextLimit - // TODO(sfoster): Command sometimes wraps this in a cascaded display: -webkit-box // to achieve multiline text clipping with an automatically inserted ellipsis, which works // everywhere except for here where it overrides this property in the flex since this is @@ -124,7 +145,11 @@ function ThermocyclerRunProfile(
    - {shouldPropagateTextLimit ? ( + {shouldPropagateTextLimit(propagateTextLimit, isOnDevice) ? (
  • - {stepTexts?.[0]} + {stepTexts[0]}
  • ) : ( - stepTexts?.map((step: string, index: number) => ( + stepTexts.map((step: string, index: number) => (
  • ) } + +type ThermocyclerRunExtendedProfileProps = BaseProps & + STProps & + Omit + +function ThermocyclerRunExtendedProfile( + props: ThermocyclerRunExtendedProfileProps +): JSX.Element { + const { + isOnDevice, + propagateCenter = false, + propagateTextLimit = false, + commandText, + profileElementTexts, + ...styleProps + } = props + + // TODO(sfoster): Command sometimes wraps this in a cascaded display: -webkit-box + // to achieve multiline text clipping with an automatically inserted ellipsis, which works + // everywhere except for here where it overrides this property in the flex since this is + // the only place where CommandText uses a flex. + // The right way to handle this is probably to take the css that's in Command and make it + // live here instead, but that should be done in a followup since it would touch everything. + // See also the margin-left on the
  • s, which is needed to prevent their bullets from + // clipping if a container set overflow: hidden. + return ( + + + {commandText} + + +
      + {shouldPropagateTextLimit(propagateTextLimit, isOnDevice) ? ( +
    • + {profileElementTexts[0].kind === 'step' + ? profileElementTexts[0].stepText + : profileElementTexts[0].cycleText} +
    • + ) : ( + profileElementTexts.map((element, index: number) => + element.kind === 'step' ? ( +
    • + {' '} + {element.stepText} +
    • + ) : ( +
    • + {element.cycleText} +
        + {element.stepTexts.map( + ({ stepText }, stepIndex: number) => ( +
      • + {' '} + {stepText} +
      • + ) + )} +
      +
    • + ) + ) + )} +
    +
    +
    + ) +} diff --git a/app/src/molecules/Command/__fixtures__/doItAllV10.json b/app/src/molecules/Command/__fixtures__/doItAllV10.json new file mode 100644 index 00000000000..83030179e79 --- /dev/null +++ b/app/src/molecules/Command/__fixtures__/doItAllV10.json @@ -0,0 +1,4863 @@ +{ + "id": "lasdlakjjflaksjdlkajsldkasd", + "result": "ok", + "status": "completed", + "createdAt": "2024-06-12T17:21:56.919263+00:00", + "files": [ + { "name": "doItAllV8.json", "role": "main" }, + { "name": "cpx_4_tuberack_100ul.json", "role": "labware" } + ], + "config": { "protocolType": "json", "schemaVersion": 8 }, + "metadata": { + "protocolName": "doItAllV10", + "author": "", + "description": "", + "created": 1701659107408, + "lastModified": 1714570438503, + "category": null, + "subcategory": null, + "tags": [] + }, + "robotType": "OT-3 Standard", + "runTimeParameters": [], + "commands": [ + { + "id": "70fbbc6c-d86a-4e66-9361-25de75552da6", + "createdAt": "2024-06-12T17:21:57.915484+00:00", + "commandType": "thermocycler/runProfile", + "key": "5ec88b6a-2c2c-4ffc-961f-c6e0dd300b49", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", + "profile": [ + { "holdSeconds": 1, "celsius": 9 }, + { "holdSeconds": 2, "celsius": 10 }, + { "holdSeconds": 3, "celsius": 11 }, + { "holdSeconds": 4, "celsius": 12 }, + { "holdSeconds": 5, "celsius": 13 }, + { "holdSeconds": 6, "celsius": 14 }, + { "holdSeconds": 7, "celsius": 15 }, + { "holdSeconds": 8, "celsius": 16 }, + { "holdSeconds": 9, "celsius": 17 }, + { "holdSeconds": 10, "celsius": 18 }, + { "holdSeconds": 11, "celsius": 19 }, + { "holdSeconds": 12, "celsius": 20 }, + { "holdSeconds": 1, "celsius": 9 }, + { "holdSeconds": 2, "celsius": 10 }, + { "holdSeconds": 3, "celsius": 11 }, + { "holdSeconds": 4, "celsius": 12 }, + { "holdSeconds": 5, "celsius": 13 }, + { "holdSeconds": 6, "celsius": 14 }, + { "holdSeconds": 7, "celsius": 15 }, + { "holdSeconds": 8, "celsius": 16 }, + { "holdSeconds": 9, "celsius": 17 }, + { "holdSeconds": 10, "celsius": 18 }, + { "holdSeconds": 11, "celsius": 19 }, + { "holdSeconds": 12, "celsius": 20 } + ], + "blockMaxVolumeUl": 10 + }, + "result": {}, + "startedAt": "2024-06-12T17:21:57.915517+00:00", + "completedAt": "2024-06-12T17:21:57.915543+00:00", + "notes": [] + }, + { + "id": "70fbbc6c-d86a-4e66-9361-25de75552da6", + "createdAt": "2024-06-12T17:21:57.915484+00:00", + "commandType": "thermocycler/runExtendedProfile", + "key": "5ec22b6a-2c2c-4bbc-961f-c6e0aa211b49", + "status": "succeeded", + "params": { + "moduleId": "f99da9f1-d63b-414b-929e-c646b23790fd:thermocyclerModuleType", + "profileElements": [ + { + "repetitions": 10, + "steps": [ + { "holdSeconds": 2, "celsius": 10 }, + { "holdSeconds": 3, "celsius": 11 }, + { "holdSeconds": 4, "celsius": 12 } + ] + }, + { "holdSeconds": 1, "celsius": 9 }, + { "holdSeconds": 5, "celsius": 13 }, + { + "repetitions": 20, + "steps": [ + { "holdSeconds": 6, "celsius": 14 }, + { "holdSeconds": 7, "celsius": 15 }, + { "holdSeconds": 8, "celsius": 16 } + ] + }, + { "holdSeconds": 9, "celsius": 17 }, + { + "repetitions": 30, + "steps": [ + { "holdSeconds": 10, "celsius": 18 }, + { "holdSeconds": 11, "celsius": 19 }, + { "holdSeconds": 12, "celsius": 20 } + ] + }, + { "holdSeconds": 1, "celsius": 9 }, + { + "repetitions": 40, + "steps": [ + { "holdSeconds": 2, "celsius": 10 }, + { "holdSeconds": 3, "celsius": 11 }, + { "holdSeconds": 4, "celsius": 12 } + ] + }, + { "holdSeconds": 5, "celsius": 13 }, + { + "repetitions": 50, + "steps": [ + { "holdSeconds": 6, "celsius": 14 }, + { "holdSeconds": 7, "celsius": 15 }, + { "holdSeconds": 8, "celsius": 16 } + ] + }, + { "holdSeconds": 9, "celsius": 17 }, + { + "repetitions": 60, + "steps": [ + { "holdSeconds": 10, "celsius": 18 }, + { "holdSeconds": 11, "celsius": 19 }, + { "holdSeconds": 12, "celsius": 20 } + ] + } + ], + "blockMaxVolumeUl": 10 + }, + "result": {}, + "startedAt": "2024-06-12T17:21:57.915517+00:00", + "completedAt": "2024-06-12T17:21:57.915543+00:00", + "notes": [] + }, + { + "id": "8f0be368-dc25-4f2a-92b9-a734ad622d4b", + "createdAt": "2024-06-12T17:21:56.894269+00:00", + "commandType": "home", + "key": "50c7ae73a4e3f7129874f39dfb514803", + "status": "succeeded", + "params": {}, + "result": {}, + "startedAt": "2024-06-12T17:21:56.894479+00:00", + "completedAt": "2024-06-12T17:21:56.894522+00:00", + "notes": [] + }, + { + "id": "c744767e-f5a4-408d-bc28-d46a2bd17297", + "createdAt": "2024-06-12T17:21:56.894706+00:00", + "commandType": "loadPipette", + "key": "a1b95079-5b17-428d-b40c-a8236a9890c5", + "status": "succeeded", + "params": { + "pipetteName": "p1000_single_flex", + "mount": "left", + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "startedAt": "2024-06-12T17:21:56.894767+00:00", + "completedAt": "2024-06-12T17:21:56.898018+00:00", + "notes": [] + }, + { + "id": "684793b1-7401-4291-ac0f-e95d61e49e07", + "createdAt": "2024-06-12T17:21:56.898252+00:00", + "commandType": "loadModule", + "key": "6f1e3ad3-8f03-4583-8031-be6be2fcd903", + "status": "succeeded", + "params": { + "model": "heaterShakerModuleV1", + "location": { "slotName": "D1" }, + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", + "definition": { + "otSharedSchema": "module/schemas/2", + "moduleType": "heaterShakerModuleType", + "model": "heaterShakerModuleV1", + "labwareOffset": { "x": -0.125, "y": 1.125, "z": 68.275 }, + "dimensions": { "bareOverallHeight": 82.0, "overLabwareHeight": 0.0 }, + "calibrationPoint": { "x": 12.0, "y": 8.75, "z": 68.275 }, + "displayName": "Heater-Shaker Module GEN1", + "quirks": [], + "slotTransforms": { + "ot2_standard": { + "3": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "6": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "9": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + } + }, + "ot2_short_trash": { + "3": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "6": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "9": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + } + }, + "ot3_standard": { + "D1": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "C1": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "B1": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "A1": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "D3": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "C3": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "B3": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "A3": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + } + } + }, + "compatibleWith": [], + "gripperOffsets": { + "default": { + "pickUpOffset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "dropOffset": { "x": 0.0, "y": 0.0, "z": 1.0 } + } + } + }, + "model": "heaterShakerModuleV1", + "serialNumber": "fake-serial-number-0d06c2c1-3ee4-467d-b1cb-31ce8a260ff5" + }, + "startedAt": "2024-06-12T17:21:56.898296+00:00", + "completedAt": "2024-06-12T17:21:56.898586+00:00", + "notes": [] + }, + { + "id": "a72762a6-d894-4a48-91a0-1e0e46ae41e4", + "createdAt": "2024-06-12T17:21:56.898811+00:00", + "commandType": "loadModule", + "key": "4997a543-7788-434f-8eae-1c4aa3a2a805", + "status": "succeeded", + "params": { + "model": "thermocyclerModuleV2", + "location": { "slotName": "B1" }, + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "result": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", + "definition": { + "otSharedSchema": "module/schemas/2", + "moduleType": "thermocyclerModuleType", + "model": "thermocyclerModuleV2", + "labwareOffset": { "x": 0.0, "y": 68.8, "z": 108.96 }, + "dimensions": { + "bareOverallHeight": 108.96, + "overLabwareHeight": 0.0, + "lidHeight": 61.7 + }, + "calibrationPoint": { "x": 14.4, "y": 64.93, "z": 97.8 }, + "displayName": "Thermocycler Module GEN2", + "quirks": [], + "slotTransforms": { + "ot3_standard": { + "B1": { + "labwareOffset": [ + [1, 0, 0, -20.005], + [0, 1, 0, -0.84], + [0, 0, 1, -98], + [0, 0, 0, 1] + ], + "cornerOffsetFromSlot": [ + [1, 0, 0, -20.005], + [0, 1, 0, -0.84], + [0, 0, 1, -98], + [0, 0, 0, 1] + ] + } + } + }, + "compatibleWith": [], + "gripperOffsets": { + "default": { + "pickUpOffset": { "x": 0.0, "y": 0.0, "z": 4.6 }, + "dropOffset": { "x": 0.0, "y": 0.0, "z": 5.6 } + } + } + }, + "model": "thermocyclerModuleV2", + "serialNumber": "fake-serial-number-abe43d5b-1dd7-4fa9-bf8b-b089236d5adb" + }, + "startedAt": "2024-06-12T17:21:56.898850+00:00", + "completedAt": "2024-06-12T17:21:56.899485+00:00", + "notes": [] + }, + { + "id": "e141cd5f-7c92-4c89-aea6-79057400ee5d", + "createdAt": "2024-06-12T17:21:56.899614+00:00", + "commandType": "loadLabware", + "key": "8bfb6d48-4d08-4ea0-8ce7-f8efe90e202c", + "status": "succeeded", + "params": { + "location": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "loadName": "opentrons_96_pcr_adapter", + "namespace": "opentrons", + "version": 1, + "labwareId": "7c4d59fa-0e50-442f-adce-9e4b0c7f0b88:opentrons/opentrons_96_pcr_adapter/1", + "displayName": "Opentrons 96 PCR Heater-Shaker Adapter" + }, + "result": { + "labwareId": "7c4d59fa-0e50-442f-adce-9e4b0c7f0b88:opentrons/opentrons_96_pcr_adapter/1", + "definition": { + "schemaVersion": 2, + "version": 1, + "namespace": "opentrons", + "metadata": { + "displayName": "Opentrons 96 PCR Heater-Shaker Adapter", + "displayCategory": "adapter", + "displayVolumeUnits": "\u00b5L", + "tags": [] + }, + "brand": { "brand": "Opentrons", "brandId": [] }, + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": false, + "loadName": "opentrons_96_pcr_adapter", + "isMagneticModuleCompatible": false + }, + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "cornerOffsetFromSlot": { "x": 8.5, "y": 5.5, "z": 0 }, + "dimensions": { + "yDimension": 75, + "zDimension": 13.85, + "xDimension": 111 + }, + "wells": { + "A1": { + "depth": 12, + "x": 6, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B1": { + "depth": 12, + "x": 6, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C1": { + "depth": 12, + "x": 6, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D1": { + "depth": 12, + "x": 6, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E1": { + "depth": 12, + "x": 6, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F1": { + "depth": 12, + "x": 6, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G1": { + "depth": 12, + "x": 6, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H1": { + "depth": 12, + "x": 6, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A2": { + "depth": 12, + "x": 15, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B2": { + "depth": 12, + "x": 15, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C2": { + "depth": 12, + "x": 15, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D2": { + "depth": 12, + "x": 15, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E2": { + "depth": 12, + "x": 15, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F2": { + "depth": 12, + "x": 15, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G2": { + "depth": 12, + "x": 15, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H2": { + "depth": 12, + "x": 15, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A3": { + "depth": 12, + "x": 24, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B3": { + "depth": 12, + "x": 24, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C3": { + "depth": 12, + "x": 24, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D3": { + "depth": 12, + "x": 24, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E3": { + "depth": 12, + "x": 24, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F3": { + "depth": 12, + "x": 24, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G3": { + "depth": 12, + "x": 24, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H3": { + "depth": 12, + "x": 24, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A4": { + "depth": 12, + "x": 33, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B4": { + "depth": 12, + "x": 33, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C4": { + "depth": 12, + "x": 33, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D4": { + "depth": 12, + "x": 33, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E4": { + "depth": 12, + "x": 33, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F4": { + "depth": 12, + "x": 33, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G4": { + "depth": 12, + "x": 33, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H4": { + "depth": 12, + "x": 33, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A5": { + "depth": 12, + "x": 42, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B5": { + "depth": 12, + "x": 42, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C5": { + "depth": 12, + "x": 42, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D5": { + "depth": 12, + "x": 42, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E5": { + "depth": 12, + "x": 42, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F5": { + "depth": 12, + "x": 42, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G5": { + "depth": 12, + "x": 42, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H5": { + "depth": 12, + "x": 42, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A6": { + "depth": 12, + "x": 51, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B6": { + "depth": 12, + "x": 51, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C6": { + "depth": 12, + "x": 51, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D6": { + "depth": 12, + "x": 51, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E6": { + "depth": 12, + "x": 51, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F6": { + "depth": 12, + "x": 51, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G6": { + "depth": 12, + "x": 51, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H6": { + "depth": 12, + "x": 51, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A7": { + "depth": 12, + "x": 60, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B7": { + "depth": 12, + "x": 60, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C7": { + "depth": 12, + "x": 60, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D7": { + "depth": 12, + "x": 60, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E7": { + "depth": 12, + "x": 60, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F7": { + "depth": 12, + "x": 60, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G7": { + "depth": 12, + "x": 60, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H7": { + "depth": 12, + "x": 60, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A8": { + "depth": 12, + "x": 69, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B8": { + "depth": 12, + "x": 69, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C8": { + "depth": 12, + "x": 69, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D8": { + "depth": 12, + "x": 69, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E8": { + "depth": 12, + "x": 69, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F8": { + "depth": 12, + "x": 69, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G8": { + "depth": 12, + "x": 69, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H8": { + "depth": 12, + "x": 69, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A9": { + "depth": 12, + "x": 78, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B9": { + "depth": 12, + "x": 78, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C9": { + "depth": 12, + "x": 78, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D9": { + "depth": 12, + "x": 78, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E9": { + "depth": 12, + "x": 78, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F9": { + "depth": 12, + "x": 78, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G9": { + "depth": 12, + "x": 78, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H9": { + "depth": 12, + "x": 78, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A10": { + "depth": 12, + "x": 87, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B10": { + "depth": 12, + "x": 87, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C10": { + "depth": 12, + "x": 87, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D10": { + "depth": 12, + "x": 87, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E10": { + "depth": 12, + "x": 87, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F10": { + "depth": 12, + "x": 87, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G10": { + "depth": 12, + "x": 87, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H10": { + "depth": 12, + "x": 87, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A11": { + "depth": 12, + "x": 96, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B11": { + "depth": 12, + "x": 96, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C11": { + "depth": 12, + "x": 96, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D11": { + "depth": 12, + "x": 96, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E11": { + "depth": 12, + "x": 96, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F11": { + "depth": 12, + "x": 96, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G11": { + "depth": 12, + "x": 96, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H11": { + "depth": 12, + "x": 96, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A12": { + "depth": 12, + "x": 105, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B12": { + "depth": 12, + "x": 105, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C12": { + "depth": 12, + "x": 105, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D12": { + "depth": 12, + "x": 105, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E12": { + "depth": 12, + "x": 105, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F12": { + "depth": 12, + "x": 105, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G12": { + "depth": 12, + "x": 105, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H12": { + "depth": 12, + "x": 105, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + } + }, + "groups": [ + { + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + "metadata": { "wellBottomShape": "v" } + } + ], + "allowedRoles": ["adapter"], + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "gripperOffsets": { + "default": { + "pickUpOffset": { "x": 0, "y": 0, "z": 0 }, + "dropOffset": { "x": 0, "y": 0, "z": 1 } + } + } + } + }, + "startedAt": "2024-06-12T17:21:56.899718+00:00", + "completedAt": "2024-06-12T17:21:56.899792+00:00", + "notes": [] + }, + { + "id": "edd39a48-10ea-4cf1-848f-9484c136714f", + "createdAt": "2024-06-12T17:21:56.899904+00:00", + "commandType": "loadLabware", + "key": "988395e3-9b85-4bb0-89a4-3afc1d7330fd", + "status": "succeeded", + "params": { + "location": { "slotName": "C2" }, + "loadName": "opentrons_flex_96_tiprack_1000ul", + "namespace": "opentrons", + "version": 1, + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "displayName": "Opentrons Flex 96 Tip Rack 1000 \u00b5L" + }, + "result": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "definition": { + "schemaVersion": 2, + "version": 1, + "namespace": "opentrons", + "metadata": { + "displayName": "Opentrons Flex 96 Tip Rack 1000 \u00b5L", + "displayCategory": "tipRack", + "displayVolumeUnits": "\u00b5L", + "tags": [] + }, + "brand": { "brand": "Opentrons", "brandId": [] }, + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": true, + "tipLength": 95.6, + "tipOverlap": 10.5, + "loadName": "opentrons_flex_96_tiprack_1000ul", + "isMagneticModuleCompatible": false + }, + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0 }, + "dimensions": { + "yDimension": 85.75, + "zDimension": 99, + "xDimension": 127.75 + }, + "wells": { + "A1": { + "depth": 97.5, + "x": 14.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B1": { + "depth": 97.5, + "x": 14.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C1": { + "depth": 97.5, + "x": 14.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D1": { + "depth": 97.5, + "x": 14.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E1": { + "depth": 97.5, + "x": 14.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F1": { + "depth": 97.5, + "x": 14.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G1": { + "depth": 97.5, + "x": 14.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H1": { + "depth": 97.5, + "x": 14.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A2": { + "depth": 97.5, + "x": 23.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B2": { + "depth": 97.5, + "x": 23.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C2": { + "depth": 97.5, + "x": 23.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D2": { + "depth": 97.5, + "x": 23.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E2": { + "depth": 97.5, + "x": 23.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F2": { + "depth": 97.5, + "x": 23.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G2": { + "depth": 97.5, + "x": 23.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H2": { + "depth": 97.5, + "x": 23.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A3": { + "depth": 97.5, + "x": 32.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B3": { + "depth": 97.5, + "x": 32.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C3": { + "depth": 97.5, + "x": 32.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D3": { + "depth": 97.5, + "x": 32.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E3": { + "depth": 97.5, + "x": 32.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F3": { + "depth": 97.5, + "x": 32.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G3": { + "depth": 97.5, + "x": 32.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H3": { + "depth": 97.5, + "x": 32.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A4": { + "depth": 97.5, + "x": 41.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B4": { + "depth": 97.5, + "x": 41.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C4": { + "depth": 97.5, + "x": 41.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D4": { + "depth": 97.5, + "x": 41.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E4": { + "depth": 97.5, + "x": 41.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F4": { + "depth": 97.5, + "x": 41.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G4": { + "depth": 97.5, + "x": 41.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H4": { + "depth": 97.5, + "x": 41.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A5": { + "depth": 97.5, + "x": 50.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B5": { + "depth": 97.5, + "x": 50.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C5": { + "depth": 97.5, + "x": 50.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D5": { + "depth": 97.5, + "x": 50.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E5": { + "depth": 97.5, + "x": 50.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F5": { + "depth": 97.5, + "x": 50.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G5": { + "depth": 97.5, + "x": 50.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H5": { + "depth": 97.5, + "x": 50.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A6": { + "depth": 97.5, + "x": 59.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B6": { + "depth": 97.5, + "x": 59.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C6": { + "depth": 97.5, + "x": 59.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D6": { + "depth": 97.5, + "x": 59.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E6": { + "depth": 97.5, + "x": 59.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F6": { + "depth": 97.5, + "x": 59.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G6": { + "depth": 97.5, + "x": 59.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H6": { + "depth": 97.5, + "x": 59.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A7": { + "depth": 97.5, + "x": 68.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B7": { + "depth": 97.5, + "x": 68.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C7": { + "depth": 97.5, + "x": 68.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D7": { + "depth": 97.5, + "x": 68.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E7": { + "depth": 97.5, + "x": 68.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F7": { + "depth": 97.5, + "x": 68.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G7": { + "depth": 97.5, + "x": 68.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H7": { + "depth": 97.5, + "x": 68.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A8": { + "depth": 97.5, + "x": 77.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B8": { + "depth": 97.5, + "x": 77.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C8": { + "depth": 97.5, + "x": 77.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D8": { + "depth": 97.5, + "x": 77.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E8": { + "depth": 97.5, + "x": 77.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F8": { + "depth": 97.5, + "x": 77.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G8": { + "depth": 97.5, + "x": 77.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H8": { + "depth": 97.5, + "x": 77.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A9": { + "depth": 97.5, + "x": 86.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B9": { + "depth": 97.5, + "x": 86.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C9": { + "depth": 97.5, + "x": 86.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D9": { + "depth": 97.5, + "x": 86.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E9": { + "depth": 97.5, + "x": 86.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F9": { + "depth": 97.5, + "x": 86.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G9": { + "depth": 97.5, + "x": 86.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H9": { + "depth": 97.5, + "x": 86.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A10": { + "depth": 97.5, + "x": 95.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B10": { + "depth": 97.5, + "x": 95.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C10": { + "depth": 97.5, + "x": 95.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D10": { + "depth": 97.5, + "x": 95.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E10": { + "depth": 97.5, + "x": 95.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F10": { + "depth": 97.5, + "x": 95.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G10": { + "depth": 97.5, + "x": 95.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H10": { + "depth": 97.5, + "x": 95.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A11": { + "depth": 97.5, + "x": 104.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B11": { + "depth": 97.5, + "x": 104.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C11": { + "depth": 97.5, + "x": 104.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D11": { + "depth": 97.5, + "x": 104.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E11": { + "depth": 97.5, + "x": 104.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F11": { + "depth": 97.5, + "x": 104.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G11": { + "depth": 97.5, + "x": 104.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H11": { + "depth": 97.5, + "x": 104.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A12": { + "depth": 97.5, + "x": 113.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B12": { + "depth": 97.5, + "x": 113.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C12": { + "depth": 97.5, + "x": 113.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D12": { + "depth": 97.5, + "x": 113.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E12": { + "depth": 97.5, + "x": 113.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F12": { + "depth": 97.5, + "x": 113.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G12": { + "depth": 97.5, + "x": 113.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H12": { + "depth": 97.5, + "x": 113.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + } + }, + "groups": [ + { + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + "metadata": {} + } + ], + "allowedRoles": [], + "stackingOffsetWithLabware": { + "opentrons_flex_96_tiprack_adapter": { "x": 0, "y": 0, "z": 121 } + }, + "stackingOffsetWithModule": {}, + "gripperOffsets": {}, + "gripHeightFromLabwareBottom": 23.9, + "gripForce": 16.0 + } + }, + "startedAt": "2024-06-12T17:21:56.899933+00:00", + "completedAt": "2024-06-12T17:21:56.900034+00:00", + "notes": [] + }, + { + "id": "5607c182-a90d-466e-96fd-45aafaf45b7b", + "createdAt": "2024-06-12T17:21:56.900159+00:00", + "commandType": "loadLabware", + "key": "0d60425e-5a6f-4205-ac59-d38a080f2e92", + "status": "succeeded", + "params": { + "location": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "loadName": "opentrons_96_wellplate_200ul_pcr_full_skirt", + "namespace": "opentrons", + "version": 2, + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "displayName": "Opentrons Tough 96 Well Plate 200 \u00b5L PCR Full Skirt" + }, + "result": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "definition": { + "schemaVersion": 2, + "version": 2, + "namespace": "opentrons", + "metadata": { + "displayName": "Opentrons Tough 96 Well Plate 200 \u00b5L PCR Full Skirt", + "displayCategory": "wellPlate", + "displayVolumeUnits": "\u00b5L", + "tags": [] + }, + "brand": { + "brand": "Opentrons", + "brandId": ["991-00076"], + "links": [ + "https://shop.opentrons.com/tough-0.2-ml-96-well-pcr-plate-full-skirt/" + ] + }, + "parameters": { + "format": "96Standard", + "isTiprack": false, + "loadName": "opentrons_96_wellplate_200ul_pcr_full_skirt", + "isMagneticModuleCompatible": true + }, + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0 }, + "dimensions": { + "yDimension": 85.48, + "zDimension": 16, + "xDimension": 127.76 + }, + "wells": { + "A1": { + "depth": 14.95, + "x": 14.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B1": { + "depth": 14.95, + "x": 14.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C1": { + "depth": 14.95, + "x": 14.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D1": { + "depth": 14.95, + "x": 14.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E1": { + "depth": 14.95, + "x": 14.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F1": { + "depth": 14.95, + "x": 14.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G1": { + "depth": 14.95, + "x": 14.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H1": { + "depth": 14.95, + "x": 14.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A2": { + "depth": 14.95, + "x": 23.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B2": { + "depth": 14.95, + "x": 23.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C2": { + "depth": 14.95, + "x": 23.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D2": { + "depth": 14.95, + "x": 23.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E2": { + "depth": 14.95, + "x": 23.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F2": { + "depth": 14.95, + "x": 23.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G2": { + "depth": 14.95, + "x": 23.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H2": { + "depth": 14.95, + "x": 23.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A3": { + "depth": 14.95, + "x": 32.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B3": { + "depth": 14.95, + "x": 32.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C3": { + "depth": 14.95, + "x": 32.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D3": { + "depth": 14.95, + "x": 32.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E3": { + "depth": 14.95, + "x": 32.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F3": { + "depth": 14.95, + "x": 32.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G3": { + "depth": 14.95, + "x": 32.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H3": { + "depth": 14.95, + "x": 32.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A4": { + "depth": 14.95, + "x": 41.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B4": { + "depth": 14.95, + "x": 41.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C4": { + "depth": 14.95, + "x": 41.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D4": { + "depth": 14.95, + "x": 41.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E4": { + "depth": 14.95, + "x": 41.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F4": { + "depth": 14.95, + "x": 41.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G4": { + "depth": 14.95, + "x": 41.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H4": { + "depth": 14.95, + "x": 41.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A5": { + "depth": 14.95, + "x": 50.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B5": { + "depth": 14.95, + "x": 50.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C5": { + "depth": 14.95, + "x": 50.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D5": { + "depth": 14.95, + "x": 50.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E5": { + "depth": 14.95, + "x": 50.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F5": { + "depth": 14.95, + "x": 50.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G5": { + "depth": 14.95, + "x": 50.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H5": { + "depth": 14.95, + "x": 50.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A6": { + "depth": 14.95, + "x": 59.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B6": { + "depth": 14.95, + "x": 59.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C6": { + "depth": 14.95, + "x": 59.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D6": { + "depth": 14.95, + "x": 59.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E6": { + "depth": 14.95, + "x": 59.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F6": { + "depth": 14.95, + "x": 59.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G6": { + "depth": 14.95, + "x": 59.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H6": { + "depth": 14.95, + "x": 59.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A7": { + "depth": 14.95, + "x": 68.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B7": { + "depth": 14.95, + "x": 68.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C7": { + "depth": 14.95, + "x": 68.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D7": { + "depth": 14.95, + "x": 68.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E7": { + "depth": 14.95, + "x": 68.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F7": { + "depth": 14.95, + "x": 68.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G7": { + "depth": 14.95, + "x": 68.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H7": { + "depth": 14.95, + "x": 68.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A8": { + "depth": 14.95, + "x": 77.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B8": { + "depth": 14.95, + "x": 77.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C8": { + "depth": 14.95, + "x": 77.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D8": { + "depth": 14.95, + "x": 77.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E8": { + "depth": 14.95, + "x": 77.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F8": { + "depth": 14.95, + "x": 77.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G8": { + "depth": 14.95, + "x": 77.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H8": { + "depth": 14.95, + "x": 77.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A9": { + "depth": 14.95, + "x": 86.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B9": { + "depth": 14.95, + "x": 86.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C9": { + "depth": 14.95, + "x": 86.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D9": { + "depth": 14.95, + "x": 86.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E9": { + "depth": 14.95, + "x": 86.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F9": { + "depth": 14.95, + "x": 86.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G9": { + "depth": 14.95, + "x": 86.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H9": { + "depth": 14.95, + "x": 86.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A10": { + "depth": 14.95, + "x": 95.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B10": { + "depth": 14.95, + "x": 95.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C10": { + "depth": 14.95, + "x": 95.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D10": { + "depth": 14.95, + "x": 95.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E10": { + "depth": 14.95, + "x": 95.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F10": { + "depth": 14.95, + "x": 95.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G10": { + "depth": 14.95, + "x": 95.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H10": { + "depth": 14.95, + "x": 95.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A11": { + "depth": 14.95, + "x": 104.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B11": { + "depth": 14.95, + "x": 104.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C11": { + "depth": 14.95, + "x": 104.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D11": { + "depth": 14.95, + "x": 104.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E11": { + "depth": 14.95, + "x": 104.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F11": { + "depth": 14.95, + "x": 104.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G11": { + "depth": 14.95, + "x": 104.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H11": { + "depth": 14.95, + "x": 104.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A12": { + "depth": 14.95, + "x": 113.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B12": { + "depth": 14.95, + "x": 113.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C12": { + "depth": 14.95, + "x": 113.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D12": { + "depth": 14.95, + "x": 113.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E12": { + "depth": 14.95, + "x": 113.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F12": { + "depth": 14.95, + "x": 113.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G12": { + "depth": 14.95, + "x": 113.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H12": { + "depth": 14.95, + "x": 113.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + } + }, + "groups": [ + { + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + "metadata": { "wellBottomShape": "v" } + } + ], + "allowedRoles": [], + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { "x": 0, "y": 0, "z": 10.95 }, + "opentrons_96_well_aluminum_block": { "x": 0, "y": 0, "z": 11.91 } + }, + "stackingOffsetWithModule": { + "magneticBlockV1": { "x": 0, "y": 0, "z": 3.54 }, + "thermocyclerModuleV2": { "x": 0, "y": 0, "z": 10.7 } + }, + "gripperOffsets": {}, + "gripHeightFromLabwareBottom": 10.0, + "gripForce": 15.0 + } + }, + "startedAt": "2024-06-12T17:21:56.900184+00:00", + "completedAt": "2024-06-12T17:21:56.900240+00:00", + "notes": [] + }, + { + "id": "8632c181-e545-42a6-8379-9f1feb0dc46f", + "createdAt": "2024-06-12T17:21:56.900318+00:00", + "commandType": "loadLabware", + "key": "eba272e9-3eed-46bb-91aa-d1aee8da58da", + "status": "succeeded", + "params": { + "location": { "addressableAreaName": "A4" }, + "loadName": "axygen_1_reservoir_90ml", + "namespace": "opentrons", + "version": 1, + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "displayName": "Axygen 1 Well Reservoir 90 mL" + }, + "result": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "definition": { + "schemaVersion": 2, + "version": 1, + "namespace": "opentrons", + "metadata": { + "displayName": "Axygen 1 Well Reservoir 90 mL", + "displayCategory": "reservoir", + "displayVolumeUnits": "mL", + "tags": [] + }, + "brand": { + "brand": "Axygen", + "brandId": ["RES-SW1-LP"], + "links": [ + "https://ecatalog.corning.com/life-sciences/b2c/US/en/Genomics-%26-Molecular-Biology/Automation-Consumables/Automation-Reservoirs/Axygen%C2%AE-Reagent-Reservoirs/p/RES-SW1-LP?clear=true" + ] + }, + "parameters": { + "format": "trough", + "quirks": ["centerMultichannelOnWells", "touchTipDisabled"], + "isTiprack": false, + "loadName": "axygen_1_reservoir_90ml", + "isMagneticModuleCompatible": false + }, + "ordering": [["A1"]], + "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0 }, + "dimensions": { + "yDimension": 85.47, + "zDimension": 19.05, + "xDimension": 127.76 + }, + "wells": { + "A1": { + "depth": 12.42, + "x": 63.88, + "y": 42.735, + "z": 6.63, + "totalLiquidVolume": 90000, + "xDimension": 106.76, + "yDimension": 70.52, + "shape": "rectangular" + } + }, + "groups": [ + { "wells": ["A1"], "metadata": { "wellBottomShape": "flat" } } + ], + "allowedRoles": [], + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "gripperOffsets": {} + } + }, + "startedAt": "2024-06-12T17:21:56.900343+00:00", + "completedAt": "2024-06-12T17:21:56.900400+00:00", + "notes": [] + }, + { + "id": "30aea4e1-6750-4933-9937-525cf52352fc", + "createdAt": "2024-06-12T17:21:56.900502+00:00", + "commandType": "loadLiquid", + "key": "45d432f8-581b-4272-9813-e73b9168a0ad", + "status": "succeeded", + "params": { + "liquidId": "1", + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "volumeByWell": { + "A1": 100.0, + "B1": 100.0, + "C1": 100.0, + "D1": 100.0, + "E1": 100.0, + "F1": 100.0, + "G1": 100.0, + "H1": 100.0 + } + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.900534+00:00", + "completedAt": "2024-06-12T17:21:56.900568+00:00", + "notes": [] + }, + { + "id": "efe0a627-243f-4144-9eb4-6653b7592119", + "createdAt": "2024-06-12T17:21:56.900654+00:00", + "commandType": "loadLiquid", + "key": "7ec93f2a-3d22-4d30-b37a-e9f0d41a1847", + "status": "succeeded", + "params": { + "liquidId": "0", + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "volumeByWell": { "A1": 10000.0 } + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.900680+00:00", + "completedAt": "2024-06-12T17:21:56.900703+00:00", + "notes": [] + }, + { + "id": "b6461e22-2c6c-4869-8b12-3818bd259f7f", + "createdAt": "2024-06-12T17:21:56.900769+00:00", + "commandType": "thermocycler/openLid", + "key": "ba1731c6-2906-4987-b948-ea1931ad3e64", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.900799+00:00", + "completedAt": "2024-06-12T17:21:56.900827+00:00", + "notes": [] + }, + { + "id": "60097671-da13-4e65-8a39-b56ee46eaeaf", + "createdAt": "2024-06-12T17:21:56.900923+00:00", + "commandType": "moveLabware", + "key": "134cdae8-8ba1-45e4-98d7-cb931358eb01", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "newLocation": { "slotName": "C1" }, + "strategy": "usingGripper" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.900952+00:00", + "completedAt": "2024-06-12T17:21:56.901073+00:00", + "notes": [] + }, + { + "id": "9dcd7b3e-c893-4509-b7d3-29915655a3d6", + "createdAt": "2024-06-12T17:21:56.901207+00:00", + "commandType": "pickUpTip", + "key": "6a5f30cc-8bea-4899-b058-7bf2095efe86", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 181.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.901247+00:00", + "completedAt": "2024-06-12T17:21:56.901658+00:00", + "notes": [] + }, + { + "id": "14bca710-bce8-48ec-adf2-2667fbb7a84e", + "createdAt": "2024-06-12T17:21:56.901839+00:00", + "commandType": "aspirate", + "key": "71fc15e9-ad19-4c77-a32f-abba4ea5e6f9", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.901881+00:00", + "completedAt": "2024-06-12T17:21:56.902199+00:00", + "notes": [] + }, + { + "id": "d96a443f-7246-4666-81b5-fa1ffa345421", + "createdAt": "2024-06-12T17:21:56.902289+00:00", + "commandType": "dispense", + "key": "a94a08b1-ed23-4c91-a853-27192da2aa70", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 356.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.902318+00:00", + "completedAt": "2024-06-12T17:21:56.902634+00:00", + "notes": [] + }, + { + "id": "6f9b30ba-63ea-4528-9a5b-c03886bc1126", + "createdAt": "2024-06-12T17:21:56.902719+00:00", + "commandType": "moveToAddressableArea", + "key": "9f8c952b-88e2-4a6d-b6a2-e943f9b032e0", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.902754+00:00", + "completedAt": "2024-06-12T17:21:56.903132+00:00", + "notes": [] + }, + { + "id": "d9de157e-fad0-4ec5-950a-c0a96d1406de", + "createdAt": "2024-06-12T17:21:56.903240+00:00", + "commandType": "dropTipInPlace", + "key": "734f7c4e-be2c-4a45-ae26-d81fb6b58729", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.903270+00:00", + "completedAt": "2024-06-12T17:21:56.903297+00:00", + "notes": [] + }, + { + "id": "028f033a-64de-4e23-bb81-2de8a3c7e1e2", + "createdAt": "2024-06-12T17:21:56.903385+00:00", + "commandType": "pickUpTip", + "key": "e3f54bb0-ef58-4e56-ad44-1dc944d2ebd8", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 172.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.903411+00:00", + "completedAt": "2024-06-12T17:21:56.903695+00:00", + "notes": [] + }, + { + "id": "37b9334c-3e0c-47dd-8d64-f0ae37fa243d", + "createdAt": "2024-06-12T17:21:56.903769+00:00", + "commandType": "aspirate", + "key": "d5dee037-06a2-4f63-a5dd-08f285db802f", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.903798+00:00", + "completedAt": "2024-06-12T17:21:56.904073+00:00", + "notes": [] + }, + { + "id": "e3ccca85-cb2b-4a62-be22-f5d2d14c1d65", + "createdAt": "2024-06-12T17:21:56.904141+00:00", + "commandType": "dispense", + "key": "db77cb48-9d63-4eb9-bac9-82c7137c7940", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "B1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 347.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.904171+00:00", + "completedAt": "2024-06-12T17:21:56.904472+00:00", + "notes": [] + }, + { + "id": "22dd3872-7d25-4673-bbe1-e30b33388525", + "createdAt": "2024-06-12T17:21:56.904542+00:00", + "commandType": "moveToAddressableArea", + "key": "c4a205b9-6a31-4993-a7dd-de84e3c40fab", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.904571+00:00", + "completedAt": "2024-06-12T17:21:56.904811+00:00", + "notes": [] + }, + { + "id": "e034f4a1-ec53-46fb-8f99-a23d97641764", + "createdAt": "2024-06-12T17:21:56.904883+00:00", + "commandType": "dropTipInPlace", + "key": "c1a58bc4-c922-4989-8259-3a011cb6548e", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.904912+00:00", + "completedAt": "2024-06-12T17:21:56.904934+00:00", + "notes": [] + }, + { + "id": "9846e62c-626b-4fdc-a6dd-419841b0df4b", + "createdAt": "2024-06-12T17:21:56.905004+00:00", + "commandType": "pickUpTip", + "key": "4660a8b7-c24f-4cbd-b5f7-0fff091af818", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 163.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.905029+00:00", + "completedAt": "2024-06-12T17:21:56.905304+00:00", + "notes": [] + }, + { + "id": "cb7b007b-eebc-441b-b4be-8be684988eaf", + "createdAt": "2024-06-12T17:21:56.905374+00:00", + "commandType": "aspirate", + "key": "9ac1cb1d-2876-4816-87bc-bcbeb3d0cc45", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.905402+00:00", + "completedAt": "2024-06-12T17:21:56.905662+00:00", + "notes": [] + }, + { + "id": "9962d0e7-e491-4515-b243-35322e0c263c", + "createdAt": "2024-06-12T17:21:56.905725+00:00", + "commandType": "dispense", + "key": "1e8856de-95c7-483f-bf9a-a8a08dbd51b5", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "C1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 338.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.905754+00:00", + "completedAt": "2024-06-12T17:21:56.906051+00:00", + "notes": [] + }, + { + "id": "9167fce8-9c39-40d2-af5c-635f0b356428", + "createdAt": "2024-06-12T17:21:56.906114+00:00", + "commandType": "moveToAddressableArea", + "key": "df45d90b-b122-4a73-8166-7c36cb4b1739", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.906142+00:00", + "completedAt": "2024-06-12T17:21:56.906377+00:00", + "notes": [] + }, + { + "id": "8bc60fba-40d9-4693-b7e0-0a4f5f1e9137", + "createdAt": "2024-06-12T17:21:56.906448+00:00", + "commandType": "dropTipInPlace", + "key": "893249ff-853b-4294-bd2c-12da0e5cb8af", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.906475+00:00", + "completedAt": "2024-06-12T17:21:56.906496+00:00", + "notes": [] + }, + { + "id": "837a423d-9154-44f1-b872-9637498b8688", + "createdAt": "2024-06-12T17:21:56.906563+00:00", + "commandType": "pickUpTip", + "key": "2e4913f4-1f2e-4039-964b-ca6f8905e551", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "D1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 154.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.906589+00:00", + "completedAt": "2024-06-12T17:21:56.906909+00:00", + "notes": [] + }, + { + "id": "cfe2c27a-9840-443e-80c4-daf21ab4fc81", + "createdAt": "2024-06-12T17:21:56.907016+00:00", + "commandType": "aspirate", + "key": "bd2ac396-b44d-41a8-b050-ff8ab4a25575", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.907054+00:00", + "completedAt": "2024-06-12T17:21:56.907338+00:00", + "notes": [] + }, + { + "id": "561e682c-adfa-4317-8aa0-190d58bca085", + "createdAt": "2024-06-12T17:21:56.907406+00:00", + "commandType": "dispense", + "key": "df68ab20-61c0-4077-bf0e-b1ef2997251a", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "D1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 329.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.907432+00:00", + "completedAt": "2024-06-12T17:21:56.907727+00:00", + "notes": [] + }, + { + "id": "f23327d3-7e6f-44df-9917-73e9afe63ffc", + "createdAt": "2024-06-12T17:21:56.907789+00:00", + "commandType": "moveToAddressableArea", + "key": "4b7f1a58-2bf5-45e8-a312-e165130f208c", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.907816+00:00", + "completedAt": "2024-06-12T17:21:56.908053+00:00", + "notes": [] + }, + { + "id": "1d3981ee-2c87-4a95-b81c-1c7213809f07", + "createdAt": "2024-06-12T17:21:56.908118+00:00", + "commandType": "dropTipInPlace", + "key": "2fc06e3a-f20d-47b9-ac7f-0a062b45beeb", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.908144+00:00", + "completedAt": "2024-06-12T17:21:56.908164+00:00", + "notes": [] + }, + { + "id": "d9bfc9d0-2c6f-490d-900e-cf1c6d5b8a25", + "createdAt": "2024-06-12T17:21:56.908230+00:00", + "commandType": "pickUpTip", + "key": "9b4955da-0d09-40da-83b2-6c398dcf5e6e", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "E1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 145.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.908255+00:00", + "completedAt": "2024-06-12T17:21:56.908508+00:00", + "notes": [] + }, + { + "id": "b94fd309-ceea-4f83-a40a-86be60a5fb75", + "createdAt": "2024-06-12T17:21:56.908576+00:00", + "commandType": "aspirate", + "key": "05a4a082-6381-4107-bb26-0e64351d3263", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.908601+00:00", + "completedAt": "2024-06-12T17:21:56.908860+00:00", + "notes": [] + }, + { + "id": "614b8fb5-7979-4e01-9616-525906ed8b2a", + "createdAt": "2024-06-12T17:21:56.908927+00:00", + "commandType": "dispense", + "key": "a494e205-1cf5-4718-b5f0-43fe74c962bc", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "E1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 320.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.908956+00:00", + "completedAt": "2024-06-12T17:21:56.909249+00:00", + "notes": [] + }, + { + "id": "40abff26-69b1-4910-8d89-1cd97c3eb39a", + "createdAt": "2024-06-12T17:21:56.909312+00:00", + "commandType": "moveToAddressableArea", + "key": "e4cf4c42-d1c3-40e7-9848-3e02e01250a8", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.909338+00:00", + "completedAt": "2024-06-12T17:21:56.909575+00:00", + "notes": [] + }, + { + "id": "7d6c20be-e94a-4e87-a231-9b6a50827add", + "createdAt": "2024-06-12T17:21:56.909639+00:00", + "commandType": "dropTipInPlace", + "key": "397d6c15-97ae-4ab5-a2dc-e0fe75562d17", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.909665+00:00", + "completedAt": "2024-06-12T17:21:56.909684+00:00", + "notes": [] + }, + { + "id": "1e6e1c47-ac38-4233-8e1a-58b4524ae3cc", + "createdAt": "2024-06-12T17:21:56.909751+00:00", + "commandType": "pickUpTip", + "key": "86178307-33f6-4902-9207-51fc704d579c", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "F1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 136.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.909776+00:00", + "completedAt": "2024-06-12T17:21:56.910096+00:00", + "notes": [] + }, + { + "id": "d19101fe-7e76-4d06-a45e-16a6037e7b7b", + "createdAt": "2024-06-12T17:21:56.910198+00:00", + "commandType": "aspirate", + "key": "f2964ad3-9dac-4566-b636-afb59de61116", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.910236+00:00", + "completedAt": "2024-06-12T17:21:56.910619+00:00", + "notes": [] + }, + { + "id": "56cfc2a6-75e0-4e54-998f-a70f1ae513ce", + "createdAt": "2024-06-12T17:21:56.910690+00:00", + "commandType": "dispense", + "key": "68c9104b-3796-4ca1-9bc5-22afec8024d9", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "F1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 311.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.910718+00:00", + "completedAt": "2024-06-12T17:21:56.911011+00:00", + "notes": [] + }, + { + "id": "9c48d4ca-f623-48ef-91d6-3a551e1b80c3", + "createdAt": "2024-06-12T17:21:56.911135+00:00", + "commandType": "moveToAddressableArea", + "key": "9a10a801-1aaa-4238-89a9-c256f09deea0", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.911164+00:00", + "completedAt": "2024-06-12T17:21:56.911590+00:00", + "notes": [] + }, + { + "id": "453fc49c-d8c6-4d7d-b30e-7d0c85e839e3", + "createdAt": "2024-06-12T17:21:56.911701+00:00", + "commandType": "dropTipInPlace", + "key": "73d1b9c9-4c1f-40a2-8932-7f0110da78dc", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.911737+00:00", + "completedAt": "2024-06-12T17:21:56.911762+00:00", + "notes": [] + }, + { + "id": "076b217b-516b-4dcd-9e52-c8b3816612b3", + "createdAt": "2024-06-12T17:21:56.911841+00:00", + "commandType": "pickUpTip", + "key": "5818e249-0b61-4f76-af80-c835a4ad0033", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "G1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 127.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.911869+00:00", + "completedAt": "2024-06-12T17:21:56.912289+00:00", + "notes": [] + }, + { + "id": "76aea1a4-0b4f-4ecc-9833-e52849ec71f5", + "createdAt": "2024-06-12T17:21:56.912368+00:00", + "commandType": "aspirate", + "key": "38df8344-789d-4490-bd8a-cbe9121b2692", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.912396+00:00", + "completedAt": "2024-06-12T17:21:56.912745+00:00", + "notes": [] + }, + { + "id": "4ade0517-70d3-4003-8c19-8a3f0ab36d58", + "createdAt": "2024-06-12T17:21:56.912846+00:00", + "commandType": "dispense", + "key": "13593038-b554-447e-9963-0f3666ccd11a", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "G1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 302.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.912881+00:00", + "completedAt": "2024-06-12T17:21:56.913205+00:00", + "notes": [] + }, + { + "id": "837623e0-a69e-4b04-bba9-2e9668edfc8e", + "createdAt": "2024-06-12T17:21:56.913278+00:00", + "commandType": "moveToAddressableArea", + "key": "361985e0-7e23-4651-b0ed-5277cb5f1bec", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.913306+00:00", + "completedAt": "2024-06-12T17:21:56.913538+00:00", + "notes": [] + }, + { + "id": "375f3fae-d802-437b-92ea-48dcbb0c42b7", + "createdAt": "2024-06-12T17:21:56.913608+00:00", + "commandType": "dropTipInPlace", + "key": "0d1c0aa2-d5f6-45d9-9341-bc623c07f366", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.913640+00:00", + "completedAt": "2024-06-12T17:21:56.913662+00:00", + "notes": [] + }, + { + "id": "43d96227-c3bb-475d-a7f9-f53d28c9e6f6", + "createdAt": "2024-06-12T17:21:56.913730+00:00", + "commandType": "pickUpTip", + "key": "ef384b08-03fd-4ec1-8ea9-f7741ac9050e", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "H1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 118.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.913755+00:00", + "completedAt": "2024-06-12T17:21:56.914009+00:00", + "notes": [] + }, + { + "id": "00a7bfbe-fe2e-41b4-ab1c-21f12b39fe5c", + "createdAt": "2024-06-12T17:21:56.914082+00:00", + "commandType": "aspirate", + "key": "29bcc74a-cbba-4d19-9150-889378a34530", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.914107+00:00", + "completedAt": "2024-06-12T17:21:56.914395+00:00", + "notes": [] + }, + { + "id": "56e93ce7-7974-4471-ac60-a02b05f279da", + "createdAt": "2024-06-12T17:21:56.914488+00:00", + "commandType": "dispense", + "key": "e1f51c21-1522-4538-af60-b97dc37d7b9a", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "H1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 293.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.914523+00:00", + "completedAt": "2024-06-12T17:21:56.914832+00:00", + "notes": [] + }, + { + "id": "d6c42331-c874-4b92-ad6e-148493aab2f3", + "createdAt": "2024-06-12T17:21:56.914898+00:00", + "commandType": "moveToAddressableArea", + "key": "93516cec-406e-41e8-8c4c-9b2b145509f7", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.914924+00:00", + "completedAt": "2024-06-12T17:21:56.915153+00:00", + "notes": [] + }, + { + "id": "694e494a-6df4-4780-9547-e09f902be8bf", + "createdAt": "2024-06-12T17:21:56.915216+00:00", + "commandType": "dropTipInPlace", + "key": "d9a0a1d2-f813-488e-a28a-daae69cbc072", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.915242+00:00", + "completedAt": "2024-06-12T17:21:56.915269+00:00", + "notes": [] + }, + { + "id": "c28f4796-8853-4438-958f-84a85a120cf1", + "createdAt": "2024-06-12T17:21:56.915340+00:00", + "commandType": "thermocycler/closeLid", + "key": "6c34d1f1-bfeb-46d9-9669-c9b71732b6ab", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.915370+00:00", + "completedAt": "2024-06-12T17:21:56.915396+00:00", + "notes": [] + }, + { + "id": "70faac6c-d96a-4d66-9361-25de74162da6", + "createdAt": "2024-06-12T17:21:56.915484+00:00", + "commandType": "thermocycler/setTargetBlockTemperature", + "key": "5ec65b6a-2b1c-4f8c-961f-c6e0ee700b49", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", + "celsius": 40.0 + }, + "result": { "targetBlockTemperature": 40.0 }, + "startedAt": "2024-06-12T17:21:56.915517+00:00", + "completedAt": "2024-06-12T17:21:56.915543+00:00", + "notes": [] + }, + { + "id": "21ccc284-a287-4cdd-9045-cbf74b752723", + "createdAt": "2024-06-12T17:21:56.915625+00:00", + "commandType": "thermocycler/waitForBlockTemperature", + "key": "9f90e933-131f-44eb-ab12-efb152c9cb83", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.915651+00:00", + "completedAt": "2024-06-12T17:21:56.915674+00:00", + "notes": [] + }, + { + "id": "fbfe223a-9ca6-43ae-aff5-d784e84877a1", + "createdAt": "2024-06-12T17:21:56.915765+00:00", + "commandType": "waitForDuration", + "key": "f580c50f-08bb-42c4-b4a2-2764ed2fc090", + "status": "succeeded", + "params": { "seconds": 60.0, "message": "" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.915794+00:00", + "completedAt": "2024-06-12T17:21:56.915816+00:00", + "notes": [] + }, + { + "id": "4b76af6d-3e05-4b56-85de-a49bab41e3c7", + "createdAt": "2024-06-12T17:21:56.915900+00:00", + "commandType": "thermocycler/openLid", + "key": "f739bfc8-f438-4fa2-8d57-dc839ac29f24", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.915925+00:00", + "completedAt": "2024-06-12T17:21:56.915947+00:00", + "notes": [] + }, + { + "id": "d678170a-be83-466e-b898-2aa00d963086", + "createdAt": "2024-06-12T17:21:56.916011+00:00", + "commandType": "thermocycler/deactivateBlock", + "key": "4561d98c-b565-48db-a7af-6bcd31520340", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.916037+00:00", + "completedAt": "2024-06-12T17:21:56.916059+00:00", + "notes": [] + }, + { + "id": "81f0bd93-bae3-449b-904d-027fbe7d4864", + "createdAt": "2024-06-12T17:21:56.916138+00:00", + "commandType": "heaterShaker/deactivateHeater", + "key": "79dd17bf-f86a-4fe9-990a-e4e567798c87", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.916171+00:00", + "completedAt": "2024-06-12T17:21:56.916195+00:00", + "notes": [] + }, + { + "id": "5d194127-7eb6-4ef4-a26e-bbf25ef5af7f", + "createdAt": "2024-06-12T17:21:56.916279+00:00", + "commandType": "heaterShaker/openLabwareLatch", + "key": "995a2630-7a9c-4b70-aef8-ddccb7ce26ce", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": { "pipetteRetracted": true }, + "startedAt": "2024-06-12T17:21:56.916307+00:00", + "completedAt": "2024-06-12T17:21:56.916335+00:00", + "notes": [] + }, + { + "id": "e0a7f92e-affe-481c-8749-93909ddbfb3b", + "createdAt": "2024-06-12T17:21:56.916454+00:00", + "commandType": "moveLabware", + "key": "9d1035a4-617f-4fcc-a7a3-1b7a8c52b4c6", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "newLocation": { + "labwareId": "7c4d59fa-0e50-442f-adce-9e4b0c7f0b88:opentrons/opentrons_96_pcr_adapter/1" + }, + "strategy": "usingGripper" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.916480+00:00", + "completedAt": "2024-06-12T17:21:56.916597+00:00", + "notes": [] + }, + { + "id": "57399cf0-794d-4dfa-a38b-e5d678454f0f", + "createdAt": "2024-06-12T17:21:56.916667+00:00", + "commandType": "heaterShaker/closeLabwareLatch", + "key": "a244eacc-4cbc-48af-b54a-6c08cd534a51", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.916694+00:00", + "completedAt": "2024-06-12T17:21:56.916715+00:00", + "notes": [] + }, + { + "id": "487eee54-9fdc-4dcd-b948-6ff860855887", + "createdAt": "2024-06-12T17:21:56.916808+00:00", + "commandType": "heaterShaker/deactivateHeater", + "key": "a6970f26-4800-4949-8592-d977df547d8b", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.916832+00:00", + "completedAt": "2024-06-12T17:21:56.916851+00:00", + "notes": [] + }, + { + "id": "917f4e1b-d622-4602-9c13-75177b2119b2", + "createdAt": "2024-06-12T17:21:56.916913+00:00", + "commandType": "heaterShaker/setAndWaitForShakeSpeed", + "key": "ef808dac-1e14-47a1-843d-ce4ce63bdfce", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", + "rpm": 200.0 + }, + "result": { "pipetteRetracted": true }, + "startedAt": "2024-06-12T17:21:56.916939+00:00", + "completedAt": "2024-06-12T17:21:56.916969+00:00", + "notes": [] + }, + { + "id": "0807dfa8-3c0c-4125-964d-985c642afc34", + "createdAt": "2024-06-12T17:21:56.917053+00:00", + "commandType": "waitForDuration", + "key": "5b47f11e-0755-47d2-b844-f1363e28a54e", + "status": "succeeded", + "params": { "seconds": 60.0 }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.917077+00:00", + "completedAt": "2024-06-12T17:21:56.917095+00:00", + "notes": [] + }, + { + "id": "0a40eee2-8902-4914-86f1-af3d961e7dcd", + "createdAt": "2024-06-12T17:21:56.917156+00:00", + "commandType": "heaterShaker/deactivateShaker", + "key": "614ec8d0-8abf-4aa4-b771-23ff2bde2881", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.917187+00:00", + "completedAt": "2024-06-12T17:21:56.917208+00:00", + "notes": [] + }, + { + "id": "ddffeaa1-9f97-4bd3-99ee-5e0d8aee28bc", + "createdAt": "2024-06-12T17:21:56.917290+00:00", + "commandType": "heaterShaker/deactivateHeater", + "key": "dbbe307e-d361-4cb9-afe7-afeab944bfce", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.917314+00:00", + "completedAt": "2024-06-12T17:21:56.917333+00:00", + "notes": [] + }, + { + "id": "176fe5ea-4a4f-4a78-a2b9-160fdd1a7be1", + "createdAt": "2024-06-12T17:21:56.917404+00:00", + "commandType": "heaterShaker/deactivateHeater", + "key": "62f98610-cbff-4acb-ba36-a3fbb9527ba9", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.917427+00:00", + "completedAt": "2024-06-12T17:21:56.917445+00:00", + "notes": [] + }, + { + "id": "26022284-0a3c-48ad-b864-91f7c48ea19c", + "createdAt": "2024-06-12T17:21:56.917509+00:00", + "commandType": "heaterShaker/openLabwareLatch", + "key": "81cfeab1-175f-4501-8732-1ea1bc9b528b", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": { "pipetteRetracted": true }, + "startedAt": "2024-06-12T17:21:56.917532+00:00", + "completedAt": "2024-06-12T17:21:56.917554+00:00", + "notes": [] + }, + { + "id": "5e8711de-7231-47d1-ab55-541dd21cef48", + "createdAt": "2024-06-12T17:21:56.917627+00:00", + "commandType": "moveLabware", + "key": "279df4d0-2c87-4f01-b016-5c42d5edce96", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "newLocation": { "addressableAreaName": "B4" }, + "strategy": "usingGripper" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.917651+00:00", + "completedAt": "2024-06-12T17:21:56.917738+00:00", + "notes": [] + }, + { + "id": "abf5283b-dc04-457e-b1ea-4dc848620954", + "createdAt": "2024-06-12T17:21:56.917841+00:00", + "commandType": "moveLabware", + "key": "f88f41dc-ddf9-4242-9ba4-21bd728ca25f", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "newLocation": { "addressableAreaName": "gripperWasteChute" }, + "strategy": "usingGripper" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.917866+00:00", + "completedAt": "2024-06-12T17:21:56.917941+00:00", + "notes": [] + } + ], + "labware": [ + { + "id": "7c4d59fa-0e50-442f-adce-9e4b0c7f0b88:opentrons/opentrons_96_pcr_adapter/1", + "loadName": "opentrons_96_pcr_adapter", + "definitionUri": "opentrons/opentrons_96_pcr_adapter/1", + "location": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "displayName": "Opentrons 96 PCR Heater-Shaker Adapter" + }, + { + "id": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "loadName": "opentrons_flex_96_tiprack_1000ul", + "definitionUri": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "location": "offDeck", + "displayName": "Opentrons Flex 96 Tip Rack 1000 \u00b5L" + }, + { + "id": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "loadName": "opentrons_96_wellplate_200ul_pcr_full_skirt", + "definitionUri": "opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "location": { "addressableAreaName": "B4" }, + "displayName": "Opentrons Tough 96 Well Plate 200 \u00b5L PCR Full Skirt" + }, + { + "id": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "loadName": "axygen_1_reservoir_90ml", + "definitionUri": "opentrons/axygen_1_reservoir_90ml/1", + "location": { "slotName": "C1" }, + "displayName": "Axygen 1 Well Reservoir 90 mL" + } + ], + "pipettes": [ + { + "id": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "pipetteName": "p1000_single_flex", + "mount": "left" + } + ], + "modules": [ + { + "id": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", + "model": "heaterShakerModuleV1", + "location": { "slotName": "D1" }, + "serialNumber": "fake-serial-number-0d06c2c1-3ee4-467d-b1cb-31ce8a260ff5" + }, + { + "id": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", + "model": "thermocyclerModuleV2", + "location": { "slotName": "B1" }, + "serialNumber": "fake-serial-number-abe43d5b-1dd7-4fa9-bf8b-b089236d5adb" + } + ], + "liquids": [ + { + "id": "0", + "displayName": "h20", + "description": "", + "displayColor": "#b925ff" + }, + { + "id": "1", + "displayName": "sample", + "description": "", + "displayColor": "#ffd600" + } + ], + "errors": [] +} diff --git a/app/src/molecules/Command/__fixtures__/index.ts b/app/src/molecules/Command/__fixtures__/index.ts index 447a935d3dc..ba988a5197a 100644 --- a/app/src/molecules/Command/__fixtures__/index.ts +++ b/app/src/molecules/Command/__fixtures__/index.ts @@ -1,5 +1,5 @@ import robotSideAnalysis from './mockRobotSideAnalysis.json' -import doItAllAnalysis from './doItAllV8.json' +import doItAllAnalysis from './doItAllV10.json' import qiaseqAnalysis from './analysis_QIAseqFX24xv4_8.json' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' import type { CommandTextData } from '../types' diff --git a/app/src/molecules/Command/__tests__/CommandText.test.tsx b/app/src/molecules/Command/__tests__/CommandText.test.tsx index 7425c2e1853..a6614c6b330 100644 --- a/app/src/molecules/Command/__tests__/CommandText.test.tsx +++ b/app/src/molecules/Command/__tests__/CommandText.test.tsx @@ -821,11 +821,9 @@ describe('CommandText', () => { i18nInstance: i18n, } ) - screen.getByText( - 'Thermocycler starting 2 repetitions of cycle composed of the following steps:' - ) - screen.getByText('temperature: 20°C, seconds: 10') - screen.getByText('temperature: 40°C, seconds: 30') + screen.getByText('Running thermocycler profile with 2 steps:') + screen.getByText('Temperature: 20°C, hold time: 0h 00m 10s') + screen.getByText('Temperature: 40°C, hold time: 0h 00m 30s') }) it('renders correct text for thermocycler/runProfile on ODD', () => { const mockProfileSteps = [ @@ -853,12 +851,128 @@ describe('CommandText', () => { i18nInstance: i18n, } ) + screen.getByText('Running thermocycler profile with 2 steps:') + screen.getByText('Temperature: 20°C, hold time: 0h 00m 10s') + expect( + screen.queryByText('Temperature: 40°C, hold time: 0h 00m 30s') + ).not.toBeInTheDocument() + }) + it('renders correct text for thermocycler/runExtendedProfile on Desktop', () => { + const mockProfileSteps = [ + { holdSeconds: 10, celsius: 20 }, + { + repetitions: 10, + steps: [ + { holdSeconds: 15, celsius: 10 }, + { holdSeconds: 12, celsius: 11 }, + ], + }, + { holdSeconds: 30, celsius: 40 }, + { + repetitions: 9, + steps: [ + { holdSeconds: 13000, celsius: 12 }, + { holdSeconds: 14, celsius: 13 }, + ], + }, + ] + renderWithProviders( + , + { + i18nInstance: i18n, + } + ) screen.getByText( - 'Thermocycler starting 2 repetitions of cycle composed of the following steps:' + 'Running thermocycler profile with 4 total steps and cycles:' + ) + screen.getByText('Temperature: 20°C, hold time: 0h 00m 10s') + screen.getByText('10 repetitions of the following steps:') + screen.getByText('Temperature: 10°C, hold time: 0h 00m 15s') + screen.getByText('Temperature: 11°C, hold time: 0h 00m 12s') + screen.getByText('Temperature: 40°C, hold time: 0h 00m 30s') + screen.getByText('9 repetitions of the following steps:') + screen.getByText('Temperature: 12°C, hold time: 3h 36m 40s') + screen.getByText('Temperature: 13°C, hold time: 0h 00m 14s') + }) + it('renders correct text for thermocycler/runExtendedProfile on ODD', () => { + const mockProfileSteps = [ + { holdSeconds: 10, celsius: 20 }, + { + repetitions: 10, + steps: [ + { holdSeconds: 15, celsius: 10 }, + { holdSeconds: 12, celsius: 11 }, + ], + }, + { holdSeconds: 30, celsius: 40 }, + { + repetitions: 9, + steps: [ + { holdSeconds: 13, celsius: 12 }, + { holdSeconds: 14, celsius: 13 }, + ], + }, + ] + renderWithProviders( + , + { + i18nInstance: i18n, + } + ) + screen.getByText( + 'Running thermocycler profile with 4 total steps and cycles:' ) - screen.getByText('temperature: 20°C, seconds: 10') + screen.getByText('Temperature: 20°C, hold time: 0h 00m 10s') + + expect( + screen.queryByText('10 repetitions of the following steps:') + ).not.toBeInTheDocument() + expect( + screen.queryByText('Temperature: 10°C, hold time: 0h 00m 15s') + ).not.toBeInTheDocument() + expect( + screen.queryByText('Temperature: 11°C, hold time: 0h 00m 12s') + ).not.toBeInTheDocument() + expect( + screen.queryByText('Temperature: 40°C, hold time: 0h 00m 30s') + ).not.toBeInTheDocument() + expect( + screen.queryByText('9 repetitions of the following steps:') + ).not.toBeInTheDocument() + expect( + screen.queryByText('Temperature: 12°C, hold time: 0h 00m 13s') + ).not.toBeInTheDocument() expect( - screen.queryByText('temperature: 40°C, seconds: 30') + screen.queryByText('Temperature: 13°C, hold time: 0h 00m 14s') ).not.toBeInTheDocument() }) it('renders correct text for heaterShaker/setAndWaitForShakeSpeed', () => { diff --git a/app/src/molecules/Command/hooks/index.ts b/app/src/molecules/Command/hooks/index.ts index 6b6545c7689..6f0457c174f 100644 --- a/app/src/molecules/Command/hooks/index.ts +++ b/app/src/molecules/Command/hooks/index.ts @@ -4,4 +4,6 @@ export type { UseCommandTextStringParams, GetCommandText, GetCommandTextResult, + GetTCRunExtendedProfileCommandTextResult, + GetTCRunProfileCommandTextResult, } from './useCommandTextString' diff --git a/app/src/molecules/Command/hooks/useCommandTextString/index.tsx b/app/src/molecules/Command/hooks/useCommandTextString/index.tsx index d10a9aa3211..1cf39cc0d1f 100644 --- a/app/src/molecules/Command/hooks/useCommandTextString/index.tsx +++ b/app/src/molecules/Command/hooks/useCommandTextString/index.tsx @@ -5,6 +5,10 @@ import type { TFunction } from 'i18next' import type { RunTimeCommand, RobotType } from '@opentrons/shared-data' import type { CommandTextData } from '../../types' import type { GetDirectTranslationCommandText } from './utils/getDirectTranslationCommandText' +import type { + TCProfileStepText, + TCProfileCycleText, +} from './utils/getTCRunExtendedProfileCommandText' export interface UseCommandTextStringParams { command: RunTimeCommand | null @@ -12,13 +16,32 @@ export interface UseCommandTextStringParams { robotType: RobotType } +export type CommandTextKind = + | 'generic' + | 'thermocycler/runProfile' + | 'thermocycler/runExtendedProfile' + export type GetCommandText = UseCommandTextStringParams & { t: TFunction } -export interface GetCommandTextResult { +export interface GetGenericCommandTextResult { + kind: 'generic' /* The actual command text. Ex "Homing all gantry, pipette, and plunger axes" */ commandText: string +} +export interface GetTCRunProfileCommandTextResult { + kind: 'thermocycler/runProfile' + commandText: string /* The TC run profile steps. */ - stepTexts?: string[] + stepTexts: string[] } +export interface GetTCRunExtendedProfileCommandTextResult { + kind: 'thermocycler/runExtendedProfile' + commandText: string + profileElementTexts: Array +} +export type GetCommandTextResult = + | GetGenericCommandTextResult + | GetTCRunProfileCommandTextResult + | GetTCRunExtendedProfileCommandTextResult // TODO(jh, 07-18-24): Move the testing that covers this from CommandText to a new file, and verify that all commands are // properly tested. @@ -52,6 +75,7 @@ export function useCommandTextString( case 'heaterShaker/deactivateShaker': case 'heaterShaker/waitForTemperature': return { + kind: 'generic', commandText: utils.getDirectTranslationCommandText( fullParams as GetDirectTranslationCommandText ), @@ -67,6 +91,7 @@ export function useCommandTextString( case 'dropTipInPlace': case 'pickUpTip': return { + kind: 'generic', commandText: utils.getPipettingCommandText(fullParams), } @@ -76,12 +101,14 @@ export function useCommandTextString( case 'loadModule': case 'loadLiquid': return { + kind: 'generic', commandText: utils.getLoadCommandText(fullParams), } case 'liquidProbe': case 'tryLiquidProbe': return { + kind: 'generic', commandText: utils.getLiquidProbeCommandText({ ...fullParams, command, @@ -94,6 +121,7 @@ export function useCommandTextString( case 'thermocycler/setTargetLidTemperature': case 'heaterShaker/setTargetTemperature': return { + kind: 'generic', commandText: utils.getTemperatureCommandText({ ...fullParams, command, @@ -103,8 +131,15 @@ export function useCommandTextString( case 'thermocycler/runProfile': return utils.getTCRunProfileCommandText({ ...fullParams, command }) + case 'thermocycler/runExtendedProfile': + return utils.getTCRunExtendedProfileCommandText({ + ...fullParams, + command, + }) + case 'heaterShaker/setAndWaitForShakeSpeed': return { + kind: 'generic', commandText: utils.getHSShakeSpeedCommandText({ ...fullParams, command, @@ -113,11 +148,13 @@ export function useCommandTextString( case 'moveToSlot': return { + kind: 'generic', commandText: utils.getMoveToSlotCommandText({ ...fullParams, command }), } case 'moveRelative': return { + kind: 'generic', commandText: utils.getMoveRelativeCommandText({ ...fullParams, command, @@ -126,6 +163,7 @@ export function useCommandTextString( case 'moveToCoordinates': return { + kind: 'generic', commandText: utils.getMoveToCoordinatesCommandText({ ...fullParams, command, @@ -134,11 +172,13 @@ export function useCommandTextString( case 'moveToWell': return { + kind: 'generic', commandText: utils.getMoveToWellCommandText({ ...fullParams, command }), } case 'moveLabware': return { + kind: 'generic', commandText: utils.getMoveLabwareCommandText({ ...fullParams, command, @@ -147,6 +187,7 @@ export function useCommandTextString( case 'configureForVolume': return { + kind: 'generic', commandText: utils.getConfigureForVolumeCommandText({ ...fullParams, command, @@ -155,6 +196,7 @@ export function useCommandTextString( case 'configureNozzleLayout': return { + kind: 'generic', commandText: utils.getConfigureNozzleLayoutCommandText({ ...fullParams, command, @@ -163,6 +205,7 @@ export function useCommandTextString( case 'prepareToAspirate': return { + kind: 'generic', commandText: utils.getPrepareToAspirateCommandText({ ...fullParams, command, @@ -171,6 +214,7 @@ export function useCommandTextString( case 'moveToAddressableArea': return { + kind: 'generic', commandText: utils.getMoveToAddressableAreaCommandText({ ...fullParams, command, @@ -179,6 +223,7 @@ export function useCommandTextString( case 'moveToAddressableAreaForDropTip': return { + kind: 'generic', commandText: utils.getMoveToAddressableAreaForDropTipCommandText({ ...fullParams, command, @@ -187,6 +232,7 @@ export function useCommandTextString( case 'waitForDuration': return { + kind: 'generic', commandText: utils.getWaitForDurationCommandText({ ...fullParams, command, @@ -196,6 +242,7 @@ export function useCommandTextString( case 'pause': // legacy pause command case 'waitForResume': return { + kind: 'generic', commandText: utils.getWaitForResumeCommandText({ ...fullParams, command, @@ -204,27 +251,31 @@ export function useCommandTextString( case 'delay': return { + kind: 'generic', commandText: utils.getDelayCommandText({ ...fullParams, command }), } case 'comment': return { + kind: 'generic', commandText: utils.getCommentCommandText({ ...fullParams, command }), } case 'custom': return { + kind: 'generic', commandText: utils.getCustomCommandText({ ...fullParams, command }), } case 'setRailLights': return { + kind: 'generic', commandText: utils.getRailLightsCommandText({ ...fullParams, command }), } case undefined: case null: - return { commandText: '' } + return { kind: 'generic', commandText: '' } default: console.warn( @@ -232,6 +283,7 @@ export function useCommandTextString( command ) return { + kind: 'generic', commandText: utils.getUnknownCommandText({ ...fullParams, command }), } } diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunExtendedProfileCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunExtendedProfileCommandText.ts new file mode 100644 index 00000000000..4c4acde0b6f --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunExtendedProfileCommandText.ts @@ -0,0 +1,67 @@ +import { formatDurationLabeled } from '/app/transformations/commands' +import type { + TCRunExtendedProfileRunTimeCommand, + TCProfileCycle, + AtomicProfileStep, +} from '@opentrons/shared-data/command' +import type { GetTCRunExtendedProfileCommandTextResult } from '..' +import type { HandlesCommands } from './types' + +export interface TCProfileStepText { + kind: 'step' + stepText: string +} + +export interface TCProfileCycleText { + kind: 'cycle' + cycleText: string + stepTexts: TCProfileStepText[] +} + +export function getTCRunExtendedProfileCommandText({ + command, + t, +}: HandlesCommands): GetTCRunExtendedProfileCommandTextResult { + const { profileElements } = command.params + + const stepText = ({ + celsius, + holdSeconds, + }: AtomicProfileStep): TCProfileStepText => ({ + kind: 'step', + stepText: t('tc_run_profile_steps', { + celsius, + duration: formatDurationLabeled({ seconds: holdSeconds }), + }).trim(), + }) + + const stepTexts = (cycle: AtomicProfileStep[]): TCProfileStepText[] => + cycle.map(stepText) + + const startingCycleText = (cycle: TCProfileCycle): string => + t('tc_starting_extended_profile_cycle', { + repetitions: cycle.repetitions, + }) + + const cycleText = (cycle: TCProfileCycle): TCProfileCycleText => ({ + kind: 'cycle', + cycleText: startingCycleText(cycle), + stepTexts: stepTexts(cycle.steps), + }) + const profileElementTexts = ( + profile: Array + ): Array => + profile.map(element => + Object.hasOwn(element, 'repetitions') + ? cycleText(element as TCProfileCycle) + : stepText(element as AtomicProfileStep) + ) + + return { + kind: 'thermocycler/runExtendedProfile', + commandText: t('tc_starting_extended_profile', { + elementCount: profileElements.length, + }), + profileElementTexts: profileElementTexts(profileElements), + } +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts index 2d279fca850..cbc56b02635 100644 --- a/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts @@ -1,24 +1,29 @@ +import { formatDurationLabeled } from '/app/transformations/commands' import type { TCRunProfileRunTimeCommand } from '@opentrons/shared-data/command' -import type { GetCommandTextResult } from '..' +import type { GetTCRunProfileCommandTextResult } from '..' import type { HandlesCommands } from './types' export function getTCRunProfileCommandText({ command, t, -}: HandlesCommands): GetCommandTextResult { +}: HandlesCommands): GetTCRunProfileCommandTextResult { const { profile } = command.params const stepTexts = profile.map( ({ holdSeconds, celsius }: { holdSeconds: number; celsius: number }) => t('tc_run_profile_steps', { celsius, - seconds: holdSeconds, + duration: formatDurationLabeled({ seconds: holdSeconds }), }).trim() ) const startingProfileText = t('tc_starting_profile', { - repetitions: Object.keys(stepTexts).length, + stepCount: Object.keys(stepTexts).length, }) - return { commandText: startingProfileText, stepTexts } + return { + kind: 'thermocycler/runProfile', + commandText: startingProfileText, + stepTexts, + } } diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts index ff3ad43fc8c..590824e558d 100644 --- a/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts @@ -1,6 +1,7 @@ export { getLoadCommandText } from './getLoadCommandText' export { getTemperatureCommandText } from './getTemperatureCommandText' export { getTCRunProfileCommandText } from './getTCRunProfileCommandText' +export { getTCRunExtendedProfileCommandText } from './getTCRunExtendedProfileCommandText' export { getHSShakeSpeedCommandText } from './getHSShakeSpeedCommandText' export { getMoveToSlotCommandText } from './getMoveToSlotCommandText' export { getMoveRelativeCommandText } from './getMoveRelativeCommandText' diff --git a/app/src/molecules/InterventionModal/CategorizedStepContent.stories.tsx b/app/src/molecules/InterventionModal/CategorizedStepContent.stories.tsx index 04c04c0f3c6..79d83fd57ef 100644 --- a/app/src/molecules/InterventionModal/CategorizedStepContent.stories.tsx +++ b/app/src/molecules/InterventionModal/CategorizedStepContent.stories.tsx @@ -14,7 +14,7 @@ import type { Meta, StoryObj } from '@storybook/react' type CommandType = RunTimeCommand['commandType'] const availableCommandTypes = uniq( - Fixtures.mockQIASeqTextData.commands.map(command => command.commandType) + Fixtures.mockDoItAllTextData.commands.map(command => command.commandType) ) const commandsByType: Partial> = {} @@ -22,7 +22,7 @@ function commandsOfType(type: CommandType): RunTimeCommand[] { if (type in commandsByType) { return commandsByType[type] } - commandsByType[type] = Fixtures.mockQIASeqTextData.commands.filter( + commandsByType[type] = Fixtures.mockDoItAllTextData.commands.filter( command => command.commandType === type ) return commandsByType[type] @@ -62,22 +62,22 @@ function Wrapper(props: WrapperProps): JSX.Element { const topCommandIndex = topCommand == null ? undefined - : Fixtures.mockQIASeqTextData.commands.indexOf(topCommand) + : Fixtures.mockDoItAllTextData.commands.indexOf(topCommand) const bottomCommand1Index = bottomCommand1 == null ? undefined - : Fixtures.mockQIASeqTextData.commands.indexOf(bottomCommand1) + : Fixtures.mockDoItAllTextData.commands.indexOf(bottomCommand1) const bottomCommand2Index = bottomCommand2 == null ? undefined - : Fixtures.mockQIASeqTextData.commands.indexOf(bottomCommand2) + : Fixtures.mockDoItAllTextData.commands.indexOf(bottomCommand2) return ( { mockMakeToast = vi.fn() vi.mocked(useToaster).mockReturnValue({ makeToast: mockMakeToast } as any) vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'generic', commandText: TEST_COMMAND, }) }) @@ -69,6 +71,7 @@ describe('useRecoveryToasts', () => { it('should make toast with correct parameters for desktop', () => { vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'generic', commandText: TEST_COMMAND, }) @@ -80,8 +83,8 @@ describe('useRecoveryToasts', () => { ) vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'generic', commandText: TEST_COMMAND, - stepTexts: undefined, }) result.current.makeSuccessToast() @@ -120,8 +123,8 @@ describe('useRecoveryToasts', () => { it('should use recoveryToastText when desktopFullCommandText is null', () => { vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'generic', commandText: '', - stepTexts: undefined, }) const { result } = renderHook(() => @@ -196,8 +199,8 @@ describe('getStepNumber', () => { describe('useRecoveryFullCommandText', () => { it('should return the correct command text', () => { vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'generic', commandText: TEST_COMMAND, - stepTexts: undefined, }) const { result } = renderHook(() => @@ -213,8 +216,8 @@ describe('useRecoveryFullCommandText', () => { it('should return null when relevantCmd is null', () => { vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'generic', commandText: '', - stepTexts: undefined, }) const { result } = renderHook(() => @@ -242,6 +245,7 @@ describe('useRecoveryFullCommandText', () => { it('should truncate TC command', () => { vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'thermocycler/runProfile', commandText: TC_COMMAND, stepTexts: ['step'], }) @@ -255,7 +259,25 @@ describe('useRecoveryFullCommandText', () => { } as any, }) ) + expect(result.current).toBe('tc starting profile of 1231231 element steps') + }) - expect(result.current).toBe('tc command cycle') + it('should truncate new TC command', () => { + vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'thermocycler/runExtendedProfile', + commandText: TC_COMMAND, + profileElementTexts: [{ kind: 'step', stepText: 'blah blah blah' }], + }) + + const { result } = renderHook(() => + useRecoveryFullCommandText({ + robotType: FLEX_ROBOT_TYPE, + stepNumber: 0, + commandTextData: { + commands: [TC_COMMAND], + } as any, + }) + ) + expect(result.current).toBe('tc starting profile of 1231231 element steps') }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts index ff530c5fdb0..ed5aaaeaae5 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts @@ -105,7 +105,7 @@ export function useRecoveryFullCommandText( const relevantCmdIdx = typeof stepNumber === 'number' ? stepNumber : -1 const relevantCmd = commandTextData?.commands[relevantCmdIdx] ?? null - const { commandText, stepTexts } = useCommandTextString({ + const { commandText, kind } = useCommandTextString({ ...props, command: relevantCmd, }) @@ -117,7 +117,12 @@ export function useRecoveryFullCommandText( else if (relevantCmd === null) { return null } else { - return truncateIfTCCommand(commandText, stepTexts != null) + return truncateIfTCCommand( + commandText, + ['thermocycler/runProfile', 'thermocycler/runExtendedProfile'].includes( + kind + ) + ) } } @@ -164,17 +169,20 @@ function handleRecoveryOptionAction( } // Special case the TC text, so it make sense in a success toast. -function truncateIfTCCommand(commandText: string, isTCText: boolean): string { - if (isTCText) { - const indexOfCycle = commandText.indexOf('cycle') - - if (indexOfCycle === -1) { +function truncateIfTCCommand( + commandText: string, + isTCCommand: boolean +): string { + if (isTCCommand) { + const indexOfProfile = commandText.indexOf('steps') + + if (indexOfProfile === -1) { console.warn( 'TC cycle text has changed. Update Error Recovery TC text utility.' ) } - return commandText.slice(0, indexOfCycle + 5) // +5 to include "cycle" + return commandText.slice(0, indexOfProfile + 5) // +5 to include "steps" } else { return commandText } diff --git a/app/src/transformations/commands/transformations/__tests__/formatDuration.test.tsx b/app/src/transformations/commands/transformations/__tests__/formatDuration.test.tsx index 21ffb0f565b..fa6c3a954d2 100644 --- a/app/src/transformations/commands/transformations/__tests__/formatDuration.test.tsx +++ b/app/src/transformations/commands/transformations/__tests__/formatDuration.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { formatDuration } from '../formatDuration' +import { formatDuration, formatDurationLabeled } from '../formatDuration' describe('formatDuration', () => { it('should format a duration', () => { @@ -36,4 +36,57 @@ describe('formatDuration', () => { expect(formatDuration(duration)).toEqual(expected) }) + + it('should format a non-normalized duration', () => { + const duration = { + seconds: 360002, + } + const expected = '100:00:02' + expect(formatDuration(duration)).toEqual(expected) + }) +}) + +describe('formatDurationLabeled', () => { + it('should format a duration', () => { + const duration = { + hours: 2, + minutes: 40, + seconds: 2, + } + + const expected = '2h 40m 02s' + + expect(formatDurationLabeled(duration)).toEqual(expected) + }) + + it('should format a short duration with plenty of zeroes', () => { + const duration = { + seconds: 2, + } + + const expected = '0h 00m 02s' + + expect(formatDurationLabeled(duration)).toEqual(expected) + }) + + it('should format a longer duration', () => { + const duration = { + days: 3, + hours: 2, + minutes: 40, + seconds: 2, + } + + const expected = '74h 40m 02s' + + expect(formatDurationLabeled(duration)).toEqual(expected) + }) + + it('should format a non-normalized duration', () => { + const duration = { + seconds: 360002, + } + const expected = '100h 00m 02s' + expect(formatDurationLabeled(duration)).toEqual(expected) + }) }) diff --git a/app/src/transformations/commands/transformations/formatDuration.ts b/app/src/transformations/commands/transformations/formatDuration.ts index f744d9dba5d..1bef1591874 100644 --- a/app/src/transformations/commands/transformations/formatDuration.ts +++ b/app/src/transformations/commands/transformations/formatDuration.ts @@ -6,14 +6,39 @@ import type { Duration } from 'date-fns' * @returns string in format hh:mm:ss, e.g. 03:15:45 */ export function formatDuration(duration: Duration): string { - const { days, hours, minutes, seconds } = duration + const { hours, minutes, seconds } = timestampDetails(duration) - // edge case: protocol runs (or is paused) for over 24 hours - const hoursWithDays = days != null ? days * 24 + (hours ?? 0) : hours + return `${hours}:${minutes}:${seconds}` +} + +export function formatDurationLabeled(duration: Duration): string { + const { hours, minutes, seconds } = timestampDetails(duration, 1) + + return `${hours}h ${minutes}m ${seconds}s` +} + +function timestampDetails( + duration: Duration, + padHoursTo?: number +): { hours: string; minutes: string; seconds: string } { + const paddingWithDefault = padHoursTo ?? 2 + const days = duration?.days ?? 0 + const hours = duration?.hours ?? 0 + const minutes = duration?.minutes ?? 0 + const seconds = duration?.seconds ?? 0 + + const totalSeconds = seconds + minutes * 60 + hours * 3600 + days * 24 * 3600 - const paddedHours = padStart(hoursWithDays?.toString(), 2, '0') - const paddedMinutes = padStart(minutes?.toString(), 2, '0') - const paddedSeconds = padStart(seconds?.toString(), 2, '0') + const normalizedHours = Math.floor(totalSeconds / 3600) + const normalizedMinutes = Math.floor((totalSeconds % 3600) / 60) + const normalizedSeconds = totalSeconds % 60 - return `${paddedHours}:${paddedMinutes}:${paddedSeconds}` + const paddedHours = padStart( + normalizedHours.toString(), + paddingWithDefault, + '0' + ) + const paddedMinutes = padStart(normalizedMinutes.toString(), 2, '0') + const paddedSeconds = padStart(normalizedSeconds.toString(), 2, '0') + return { hours: paddedHours, minutes: paddedMinutes, seconds: paddedSeconds } } diff --git a/shared-data/command/schemas/10.json b/shared-data/command/schemas/10.json index 07afff241a1..6eb524b9a45 100644 --- a/shared-data/command/schemas/10.json +++ b/shared-data/command/schemas/10.json @@ -63,6 +63,7 @@ "thermocycler/openLid": "#/definitions/opentrons__protocol_engine__commands__thermocycler__open_lid__OpenLidCreate", "thermocycler/closeLid": "#/definitions/opentrons__protocol_engine__commands__thermocycler__close_lid__CloseLidCreate", "thermocycler/runProfile": "#/definitions/RunProfileCreate", + "thermocycler/runExtendedProfile": "#/definitions/RunExtendedProfileCreate", "absorbanceReader/closeLid": "#/definitions/opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidCreate", "absorbanceReader/openLid": "#/definitions/opentrons__protocol_engine__commands__absorbance_reader__open_lid__OpenLidCreate", "absorbanceReader/initialize": "#/definitions/InitializeCreate", @@ -253,6 +254,9 @@ { "$ref": "#/definitions/RunProfileCreate" }, + { + "$ref": "#/definitions/RunExtendedProfileCreate" + }, { "$ref": "#/definitions/opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidCreate" }, @@ -3911,6 +3915,108 @@ }, "required": ["params"] }, + "ProfileStep": { + "title": "ProfileStep", + "description": "An individual step in a Thermocycler extended profile.", + "type": "object", + "properties": { + "celsius": { + "title": "Celsius", + "description": "Target temperature in \u00b0C.", + "type": "number" + }, + "holdSeconds": { + "title": "Holdseconds", + "description": "Time to hold target temperature in seconds.", + "type": "number" + } + }, + "required": ["celsius", "holdSeconds"] + }, + "ProfileCycle": { + "title": "ProfileCycle", + "description": "An individual cycle in a Thermocycler extended profile.", + "type": "object", + "properties": { + "steps": { + "title": "Steps", + "description": "Steps to repeat.", + "type": "array", + "items": { + "$ref": "#/definitions/ProfileStep" + } + }, + "repetitions": { + "title": "Repetitions", + "description": "Number of times to repeat the steps.", + "type": "integer" + } + }, + "required": ["steps", "repetitions"] + }, + "RunExtendedProfileParams": { + "title": "RunExtendedProfileParams", + "description": "Input parameters for an individual Thermocycler profile step.", + "type": "object", + "properties": { + "moduleId": { + "title": "Moduleid", + "description": "Unique ID of the Thermocycler.", + "type": "string" + }, + "profileElements": { + "title": "Profileelements", + "description": "Elements of the profile. Each can be either a step or a cycle.", + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/ProfileStep" + }, + { + "$ref": "#/definitions/ProfileCycle" + } + ] + } + }, + "blockMaxVolumeUl": { + "title": "Blockmaxvolumeul", + "description": "Amount of liquid in uL of the most-full well in labware loaded onto the thermocycler.", + "type": "number" + } + }, + "required": ["moduleId", "profileElements"] + }, + "RunExtendedProfileCreate": { + "title": "RunExtendedProfileCreate", + "description": "A request to execute a Thermocycler profile run.", + "type": "object", + "properties": { + "commandType": { + "title": "Commandtype", + "default": "thermocycler/runExtendedProfile", + "enum": ["thermocycler/runExtendedProfile"], + "type": "string" + }, + "params": { + "$ref": "#/definitions/RunExtendedProfileParams" + }, + "intent": { + "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.", + "allOf": [ + { + "$ref": "#/definitions/CommandIntent" + } + ] + }, + "key": { + "title": "Key", + "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.", + "type": "string" + } + }, + "required": ["params"] + }, "opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidParams": { "title": "CloseLidParams", "description": "Input parameters to close the lid on an absorbance reading.", diff --git a/shared-data/command/types/module.ts b/shared-data/command/types/module.ts index ff2787bfa25..b0df1ae6b6b 100644 --- a/shared-data/command/types/module.ts +++ b/shared-data/command/types/module.ts @@ -15,6 +15,7 @@ export type ModuleRunTimeCommand = | TCDeactivateBlockRunTimeCommand | TCDeactivateLidRunTimeCommand | TCRunProfileRunTimeCommand + | TCRunExtendedProfileRunTimeCommand | TCAwaitProfileCompleteRunTimeCommand | HeaterShakerSetTargetTemperatureRunTimeCommand | HeaterShakerWaitForTemperatureRunTimeCommand @@ -39,6 +40,7 @@ export type ModuleCreateCommand = | TCDeactivateBlockCreateCommand | TCDeactivateLidCreateCommand | TCRunProfileCreateCommand + | TCRunExtendedProfileCreateCommand | TCAwaitProfileCompleteCreateCommand | HeaterShakerWaitForTemperatureCreateCommand | HeaterShakerSetAndWaitForShakeSpeedCreateCommand @@ -189,6 +191,16 @@ export interface TCRunProfileRunTimeCommand TCRunProfileCreateCommand { result?: any } +export interface TCRunExtendedProfileCreateCommand + extends CommonCommandCreateInfo { + commandType: 'thermocycler/runExtendedProfile' + params: TCExtendedProfileParams +} +export interface TCRunExtendedProfileRunTimeCommand + extends CommonCommandRunTimeInfo, + TCRunExtendedProfileCreateCommand { + result?: any +} export interface TCAwaitProfileCompleteCreateCommand extends CommonCommandCreateInfo { commandType: 'thermocycler/awaitProfileComplete' @@ -305,3 +317,14 @@ export interface ThermocyclerSetTargetBlockTemperatureParams { volume?: number holdTimeSeconds?: number } + +export interface TCProfileCycle { + steps: AtomicProfileStep[] + repetitions: number +} + +export interface TCExtendedProfileParams { + moduleId: string + profileElements: Array + blockMaxVolumeUl?: number +}