diff --git a/api/release-notes.md b/api/release-notes.md index b904c432720..5f9b53bbc45 100644 --- a/api/release-notes.md +++ b/api/release-notes.md @@ -8,15 +8,32 @@ By installing and using Opentrons software, you agree to the Opentrons End-User --- +## Opentrons Robot Software Changes in 8.7.0 + +Welcome to the v8.7.0 release of the Opentrons robot software! This release adds improvements and bug fixes. + +### New Features + +Use Opentrons Tough Universal Lids on compatible well plates and reservoirs. + +### Improvements + +Transfer aqueous, viscous, or volatile liquids with the Opentrons Flex 96-Channel Pipette (1–200 μL) to apply optimized, liquid class transfer behavior to volumes as low as 1 µL. + +### Bug Fixes + +- Default pipette flow rates are correctly applied in all liquid transfers. Use `configure_for_volume()` to clear customized pipette flow rates. +- The API no longer raises an error when partial tip pickup occurs in a slot adjacent to a loaded Flex Stacker Module. + ## Opentrons Robot Software Changes in 8.6.0 Welcome to the v8.6.0 release of the Opentrons robot software! This release adds support for the Flex Stacker Module, along with other new features and improvements. -### New Features +### New Features -- Automate labware storage with the Flex Stacker Module. Use new commands like `retrieve()` and `store()` to move well plates, reservoirs, or Flex tip racks to and from the Stacker during a protocol. -- This release adds support for the Opentrons Flex 96-Channel Pipette (1–200 μL) to transfer as little as 1 µL in a protocol. -- Control individual robot motors, like the gantry, extension mount, or gripper, with new commands. +- Automate labware storage with the Flex Stacker Module. Use new commands like `retrieve()` and `store()` to move well plates, reservoirs, or Flex tip racks to and from the Stacker during a protocol. +- This release adds support for the Opentrons Flex 96-Channel Pipette (1–200 μL) to transfer as little as 1 µL in a protocol. +- Control individual robot motors, like the gantry, extension mount, or gripper, with new commands. ### Known Limitations @@ -69,10 +86,10 @@ Welcome to the v8.4.0 release of the Opentrons robot software! This release incl ### New Features -- Use new ``transfer_liquid``, ``distribute_liquid``, and ``consolidate_liquid`` commands on Flex to optimize liquid handling based on Opentrons-verified liquid classes. -- Stack multiple Opentrons Tough Auto-Sealing Lids on the deck. -- Move Opentrons Tough Auto-Sealing Lids or remove tip rack lids with the Flex Gripper. -- Aspirate or dispense in a well based on the liquid meniscus. +- Use new `transfer_liquid`, `distribute_liquid`, and `consolidate_liquid` commands on Flex to optimize liquid handling based on Opentrons-verified liquid classes. +- Stack multiple Opentrons Tough Auto-Sealing Lids on the deck. +- Move Opentrons Tough Auto-Sealing Lids or remove tip rack lids with the Flex Gripper. +- Aspirate or dispense in a well based on the liquid meniscus. ### Improvements diff --git a/api/src/opentrons/drivers/asyncio/communication/serial_connection.py b/api/src/opentrons/drivers/asyncio/communication/serial_connection.py index 840ad5034d0..44737659ccd 100644 --- a/api/src/opentrons/drivers/asyncio/communication/serial_connection.py +++ b/api/src/opentrons/drivers/asyncio/communication/serial_connection.py @@ -462,7 +462,10 @@ def __init__( self._async_error_ack = async_error_ack.lower() async def send_command( - self, command: CommandBuilder, retries: int = 0, timeout: Optional[float] = None + self, + command: CommandBuilder, + retries: int | None = None, + timeout: float | None = None, ) -> str: """ Send a command and return the response. @@ -478,12 +481,12 @@ async def send_command( """ return await self.send_data( data=command.build(), - retries=retries or self._number_of_retries, + retries=retries if retries is not None else self._number_of_retries, timeout=timeout, ) async def send_data( - self, data: str, retries: int = 0, timeout: Optional[float] = None + self, data: str, retries: int | None = None, timeout: float | None = None ) -> str: """ Send data and return the response. @@ -501,7 +504,8 @@ async def send_data( "timeout", timeout ): return await self._send_data( - data=data, retries=retries or self._number_of_retries + data=data, + retries=retries if retries is not None else self._number_of_retries, ) async def _send_data(self, data: str, retries: int = 0) -> str: @@ -517,7 +521,6 @@ async def _send_data(self, data: str, retries: int = 0) -> str: Raises: SerialException """ data_encode = data.encode() - retries = retries or self._number_of_retries for retry in range(retries + 1): log.debug(f"{self._name}: Write -> {data_encode!r}") diff --git a/api/src/opentrons/drivers/flex_stacker/driver.py b/api/src/opentrons/drivers/flex_stacker/driver.py index 1c1a74b917a..a1204cf4f20 100644 --- a/api/src/opentrons/drivers/flex_stacker/driver.py +++ b/api/src/opentrons/drivers/flex_stacker/driver.py @@ -461,7 +461,12 @@ async def _get_tof_histogram_frame( command = GCODE.GET_TOF_MEASUREMENT.build_command().add_element(sensor.name) if resend: command.add_element("R") - resp = await self._connection.send_command(command) + + # Note: We DONT want to auto resend the request if it fails, because the + # firmware will send the next frame id instead of the current one missed. + # So lets set `retries=0` so we only send the frame once and we can + # use the retry mechanism of the `get_tof_histogram` method instead. + resp = await self._connection.send_command(command, retries=0) return self.parse_get_tof_measurement(resp) async def get_tof_histogram(self, sensor: TOFSensor) -> TOFMeasurementResult: diff --git a/api/src/opentrons/hardware_control/backends/flex_protocol.py b/api/src/opentrons/hardware_control/backends/flex_protocol.py index 5c4aad79366..3a72c22ea5e 100644 --- a/api/src/opentrons/hardware_control/backends/flex_protocol.py +++ b/api/src/opentrons/hardware_control/backends/flex_protocol.py @@ -451,6 +451,7 @@ def check_gripper_position_within_bounds( max_allowed_grip_error: float, hard_limit_lower: float, hard_limit_upper: float, + disable_geometry_grip_check: bool = False, ) -> None: ... diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 913473db1b4..be6b6faa84c 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -686,9 +686,9 @@ def _build_move_node_axis_runner( return ( MoveGroupRunner( move_groups=[move_group], - ignore_stalls=True - if not self._feature_flags.stall_detection_enabled - else False, + ignore_stalls=( + True if not self._feature_flags.stall_detection_enabled else False + ), ), False, ) @@ -712,9 +712,9 @@ def _build_move_gear_axis_runner( return ( MoveGroupRunner( move_groups=[tip_motor_move_group], - ignore_stalls=True - if not self._feature_flags.stall_detection_enabled - else False, + ignore_stalls=( + True if not self._feature_flags.stall_detection_enabled else False + ), ), True, ) @@ -939,9 +939,9 @@ async def home_tip_motors( runner = MoveGroupRunner( move_groups=[move_group], - ignore_stalls=True - if not self._feature_flags.stall_detection_enabled - else False, + ignore_stalls=( + True if not self._feature_flags.stall_detection_enabled else False + ), ) try: positions = await runner.run(can_messenger=self._messenger) @@ -976,9 +976,9 @@ async def tip_action( move_group = self._build_tip_action_group(origin, targets) runner = MoveGroupRunner( move_groups=[move_group], - ignore_stalls=True - if not self._feature_flags.stall_detection_enabled - else False, + ignore_stalls=( + True if not self._feature_flags.stall_detection_enabled else False + ), ) try: positions = await runner.run(can_messenger=self._messenger) @@ -1763,6 +1763,7 @@ def check_gripper_position_within_bounds( max_allowed_grip_error: float, hard_limit_lower: float, hard_limit_upper: float, + disable_geometry_grip_check: bool = False, ) -> None: """ Check if the gripper is at the expected location. @@ -1777,7 +1778,16 @@ def check_gripper_position_within_bounds( expected_grip_width + grip_width_uncertainty_wider ) current_gripper_position = jaw_width - if isclose(current_gripper_position, hard_limit_lower): + log.info( + f"Checking gripper position: current {jaw_width}; max error {max_allowed_grip_error}; hard limits {hard_limit_lower}, {hard_limit_upper}; expected {expected_gripper_position_min}, {expected_grip_width}, {expected_gripper_position_max}; uncertainty {grip_width_uncertainty_narrower}, {grip_width_uncertainty_wider}" + ) + if ( + isclose(current_gripper_position, hard_limit_lower) + # this odd check handles internal backlash that can lead the position to read as if + # the gripper has overshot its lower bound; this is physically impossible and an + # artifact of the gearing, so it always indicates a hard stop + or current_gripper_position < hard_limit_lower + ): raise FailedGripperPickupError( message="Failed to grip: jaws all the way closed", details={ @@ -1796,6 +1806,7 @@ def check_gripper_position_within_bounds( if ( current_gripper_position - expected_gripper_position_min < -max_allowed_grip_error + and not disable_geometry_grip_check ): raise FailedGripperPickupError( message="Failed to grip: jaws closed too far", @@ -1809,6 +1820,7 @@ def check_gripper_position_within_bounds( if ( current_gripper_position - expected_gripper_position_max > max_allowed_grip_error + and not disable_geometry_grip_check ): raise FailedGripperPickupError( message="Failed to grip: jaws could not close far enough", diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index d6e59345789..838d6a221de 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -781,7 +781,7 @@ def subsystems(self) -> Dict[SubSystem, SubSystemState]: next_fw_version=1, fw_update_needed=False, current_fw_sha="simulated", - pcba_revision="A1", + pcba_revision="A1.0", update_state=None, ) for axis in self._present_axes @@ -848,6 +848,7 @@ def check_gripper_position_within_bounds( max_allowed_grip_error: float, hard_limit_lower: float, hard_limit_upper: float, + disable_geometry_grip_check: bool = False, ) -> None: # This is a (pretty bad) simulation of the gripper actually gripping something, # but it should work. diff --git a/api/src/opentrons/hardware_control/dev_types.py b/api/src/opentrons/hardware_control/dev_types.py index 981e95e114e..487fe531f09 100644 --- a/api/src/opentrons/hardware_control/dev_types.py +++ b/api/src/opentrons/hardware_control/dev_types.py @@ -14,8 +14,9 @@ PipetteModel, PipetteName, ChannelCount, + PipetteTipType, + LiquidClasses, ) -from opentrons_shared_data.pipette.types import PipetteTipType from opentrons_shared_data.pipette.pipette_definition import ( PipetteConfigurations, SupportedTipsDefinition, @@ -104,6 +105,7 @@ class PipetteDict(InstrumentDict): plunger_positions: Dict[str, float] shaft_ul_per_mm: float available_sensors: AvailableSensorDefinition + volume_mode: LiquidClasses # LiquidClasses refer to volume mode in this context class PipetteStateDict(TypedDict): diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py index 9da14196d52..0787f436497 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py @@ -267,6 +267,7 @@ def get_attached_instrument(self, mount: MountType) -> PipetteDict: "drop_tip": instr.plunger_positions.drop_tip, } result["shaft_ul_per_mm"] = instr.config.shaft_ul_per_mm + result["volume_mode"] = instr.liquid_class_name return cast(PipetteDict, result) @property diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py index 9cba7d8e40b..6e22d9527a3 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py @@ -294,6 +294,7 @@ def get_attached_instrument(self, mount: OT3Mount) -> PipetteDict: } result["shaft_ul_per_mm"] = instr.config.shaft_ul_per_mm result["available_sensors"] = instr.config.available_sensors + result["volume_mode"] = instr.liquid_class_name return cast(PipetteDict, result) @property diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index c9ca51a42b1..9b6f6809301 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -1462,6 +1462,7 @@ def raise_error_if_gripper_pickup_failed( expected_grip_width: float, grip_width_uncertainty_wider: float, grip_width_uncertainty_narrower: float, + disable_geometry_grip_check: bool = False, ) -> None: """Ensure that a gripper pickup succeeded. @@ -1480,8 +1481,9 @@ def raise_error_if_gripper_pickup_failed( grip_width_uncertainty_narrower, gripper.jaw_width, gripper.max_allowed_grip_error, - gripper.max_jaw_width, gripper.min_jaw_width, + gripper.max_jaw_width, + disable_geometry_grip_check, ) def gripper_jaw_can_home(self) -> bool: diff --git a/api/src/opentrons/hardware_control/protocols/gripper_controller.py b/api/src/opentrons/hardware_control/protocols/gripper_controller.py index 482430d97e5..e5d30067f0b 100644 --- a/api/src/opentrons/hardware_control/protocols/gripper_controller.py +++ b/api/src/opentrons/hardware_control/protocols/gripper_controller.py @@ -41,6 +41,7 @@ def raise_error_if_gripper_pickup_failed( expected_grip_width: float, grip_width_uncertainty_wider: float, grip_width_uncertainty_narrower: float, + disable_geometry_grip_check: bool = False, ) -> None: """Ensure that a gripper pickup succeeded.""" diff --git a/api/src/opentrons/protocol_api/core/engine/_default_liquid_class_versions.py b/api/src/opentrons/protocol_api/core/engine/_default_liquid_class_versions.py new file mode 100644 index 00000000000..d45d0032987 --- /dev/null +++ b/api/src/opentrons/protocol_api/core/engine/_default_liquid_class_versions.py @@ -0,0 +1,56 @@ +"""The versions of standard liquid classes that the Protocol API should load by default.""" + +from typing import TypeAlias +from opentrons.protocols.api_support.types import APIVersion + + +DefaultLiquidClassVersions: TypeAlias = dict[APIVersion, dict[str, int]] + + +# This: +# +# { +# APIVersion(2, 100): { +# "foo_liquid": 3, +# }, +# APIVersion(2, 105): { +# "foo_liquid": 7 +# } +# } +# +# Means this: +# +# apiLevels name Default liquid class version +# --------------------------------------------------------------- +# <2.100 foo_liquid 1 +# >=2.100,<2.105 foo_liquid 3 +# >=2.105 foo_liquid 7 +# [any] [anything else] 1 +DEFAULT_LIQUID_CLASS_VERSIONS: DefaultLiquidClassVersions = { + APIVersion(2, 26): { + "ethanol_80": 2, + "glycerol_50": 2, + "water": 2, + }, +} + + +def get_liquid_class_version( + api_version: APIVersion, + liquid_class_name: str, +) -> int: + """Return what version of a liquid class the Protocol API should load by default.""" + default_lc_versions_newest_to_oldest = sorted( + DEFAULT_LIQUID_CLASS_VERSIONS.items(), key=lambda kv: kv[0], reverse=True + ) + for ( + breakpoint_api_version, + breakpoint_liquid_class_versions, + ) in default_lc_versions_newest_to_oldest: + if ( + api_version >= breakpoint_api_version + and liquid_class_name in breakpoint_liquid_class_versions + ): + return breakpoint_liquid_class_versions[liquid_class_name] + + return 1 diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 82648ceb420..de24791497c 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -66,7 +66,6 @@ ) from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError from opentrons.protocol_engine.clients import SyncClient as EngineClient -from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION from opentrons_shared_data.pipette.types import ( PIPETTE_API_NAMES_MAP, LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP, @@ -101,6 +100,9 @@ _FLEX_PIPETTE_NAMES_FIXED_IN = APIVersion(2, 23) """The version after which InstrumentContext.name returns the correct API-specific names of Flex pipettes.""" +_DEFAULT_FLOW_RATE_BUG_FIXED_IN = APIVersion(2, 26) +"""The version after which default flow rates correctly update when pipette tip or volume changes.""" + class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]): """Instrument API core using a ProtocolEngine. @@ -122,18 +124,27 @@ def __init__( self._sync_hardware_api = sync_hardware_api self._protocol_core = protocol_core - # TODO(jbl 2022-11-03) flow_rates should not live in the cores, and should be moved to the protocol context - # along with other rate related refactors (for the hardware API) - flow_rates = self._engine_client.state.pipettes.get_flow_rates(pipette_id) - self._aspirate_flow_rate = find_value_for_api_version( - MAX_SUPPORTED_VERSION, flow_rates.default_aspirate - ) - self._dispense_flow_rate = find_value_for_api_version( - MAX_SUPPORTED_VERSION, flow_rates.default_dispense - ) - self._blow_out_flow_rate = find_value_for_api_version( - MAX_SUPPORTED_VERSION, flow_rates.default_blow_out + self._initial_default_flow_rates = ( + self._engine_client.state.pipettes.get_flow_rates(pipette_id) ) + self._user_aspirate_flow_rate: Optional[float] = None + self._user_dispense_flow_rate: Optional[float] = None + self._user_blow_out_flow_rate: Optional[float] = None + + if self._protocol_core.api_version < _DEFAULT_FLOW_RATE_BUG_FIXED_IN: + # Set to the initial defaults to preserve buggy behavior where the default was not correctly updated + self._user_aspirate_flow_rate = find_value_for_api_version( + self._protocol_core.api_version, + self._initial_default_flow_rates.default_aspirate, + ) + self._user_dispense_flow_rate = find_value_for_api_version( + self._protocol_core.api_version, + self._initial_default_flow_rates.default_dispense, + ) + self._user_blow_out_flow_rate = find_value_for_api_version( + self._protocol_core.api_version, + self._initial_default_flow_rates.default_blow_out, + ) self._flow_rates = FlowRates(self) self.set_default_speed(speed=default_movement_speed) @@ -1031,13 +1042,64 @@ def get_flow_rate(self) -> FlowRates: return self._flow_rates def get_aspirate_flow_rate(self, rate: float = 1.0) -> float: - return self._aspirate_flow_rate * rate + """Returns the user-set aspirate flow rate if that's been modified, otherwise return the default. + + Note that in API versions 2.25 and below `_user_aspirate_flow_rate` will automatically be set to the initial + default flow rate when the pipette is loaded (which is the same as the max tip capacity). This is to preserve + buggy behavior in which the default was never correctly updated when the pipette picked up or dropped a tip or + had its volume configuration changed. + """ + aspirate_flow_rate = ( + self._user_aspirate_flow_rate + or find_value_for_api_version( + self._protocol_core.api_version, + self._engine_client.state.pipettes.get_flow_rates( + self._pipette_id + ).default_aspirate, + ) + ) + + return aspirate_flow_rate * rate def get_dispense_flow_rate(self, rate: float = 1.0) -> float: - return self._dispense_flow_rate * rate + """Returns the user-set dispense flow rate if that's been modified, otherwise return the default. + + Note that in API versions 2.25 and below `_user_dispense_flow_rate` will automatically be set to the initial + default flow rate when the pipette is loaded (which is the same as the max tip capacity). This is to preserve + buggy behavior in which the default was never correctly updated when the pipette picked up or dropped a tip or + had its volume configuration changed. + """ + dispense_flow_rate = ( + self._user_dispense_flow_rate + or find_value_for_api_version( + self._protocol_core.api_version, + self._engine_client.state.pipettes.get_flow_rates( + self._pipette_id + ).default_dispense, + ) + ) + + return dispense_flow_rate * rate def get_blow_out_flow_rate(self, rate: float = 1.0) -> float: - return self._blow_out_flow_rate * rate + """Returns the user-set blow-out flow rate if that's been modified, otherwise return the default. + + Note that in API versions 2.25 and below `_user_dispense_flow_rate` will automatically be set to the initial + default flow rate when the pipette is loaded (which is the same as the max tip capacity). This is to preserve + buggy behavior in which the default was never correctly updated when the pipette picked up or dropped a tip or + had its volume configuration changed. + """ + blow_out_flow_rate = ( + self._user_blow_out_flow_rate + or find_value_for_api_version( + self._protocol_core.api_version, + self._engine_client.state.pipettes.get_flow_rates( + self._pipette_id + ).default_blow_out, + ) + ) + + return blow_out_flow_rate * rate def get_nozzle_configuration(self) -> NozzleConfigurationType: return self._engine_client.state.pipettes.get_nozzle_layout_type( @@ -1084,13 +1146,13 @@ def set_flow_rate( ) -> None: if aspirate is not None: assert aspirate > 0 - self._aspirate_flow_rate = aspirate + self._user_aspirate_flow_rate = aspirate if dispense is not None: assert dispense > 0 - self._dispense_flow_rate = dispense + self._user_dispense_flow_rate = dispense if blow_out is not None: assert blow_out > 0 - self._blow_out_flow_rate = blow_out + self._user_blow_out_flow_rate = blow_out def set_liquid_presence_detection(self, enable: bool) -> None: self._liquid_presence_detection = enable @@ -1105,6 +1167,10 @@ def configure_for_volume(self, volume: float) -> None: ), ) ) + if self._protocol_core.api_version >= _DEFAULT_FLOW_RATE_BUG_FIXED_IN: + self._user_aspirate_flow_rate = None + self._user_dispense_flow_rate = None + self._user_blow_out_flow_rate = None def prepare_to_aspirate(self) -> None: self._engine_client.execute_command( @@ -1273,6 +1339,13 @@ def transfer_with_liquid_class( # noqa: C901 tiprack_uri=tiprack_uri_for_transfer_props, ) + original_aspirate_flow_rate = self._user_aspirate_flow_rate + original_dispense_flow_rate = self._user_dispense_flow_rate + original_blow_out_flow_rate = self._user_blow_out_flow_rate + in_low_volume_mode = self._engine_client.state.pipettes.get_is_low_volume_mode( + self._pipette_id + ) + target_destinations: Sequence[ Union[Tuple[Location, WellCore], TrashBin, WasteChute] ] @@ -1367,6 +1440,14 @@ def transfer_with_liquid_class( # noqa: C901 if not keep_last_tip: self._drop_tip_for_liquid_class(trash_location, return_tip) + if self._protocol_core.api_version >= _DEFAULT_FLOW_RATE_BUG_FIXED_IN: + self._restore_pipette_flow_rates_and_volume_mode( + aspirate_flow_rate=original_aspirate_flow_rate, + dispense_flow_rate=original_dispense_flow_rate, + blow_out_flow_rate=original_blow_out_flow_rate, + is_low_volume=in_low_volume_mode, + ) + def distribute_with_liquid_class( # noqa: C901 self, liquid_class: LiquidClass, @@ -1474,6 +1555,13 @@ def distribute_with_liquid_class( # noqa: C901 tiprack_uri=tiprack_uri_for_transfer_props, ) + original_aspirate_flow_rate = self._user_aspirate_flow_rate + original_dispense_flow_rate = self._user_dispense_flow_rate + original_blow_out_flow_rate = self._user_blow_out_flow_rate + in_low_volume_mode = self._engine_client.state.pipettes.get_is_low_volume_mode( + self._pipette_id + ) + # This will return a generator that provides pairs of destination well and # the volume to dispense into it dest_per_volume_step = ( @@ -1617,6 +1705,14 @@ def distribute_with_liquid_class( # noqa: C901 if not keep_last_tip: self._drop_tip_for_liquid_class(trash_location, return_tip) + if self._protocol_core.api_version >= _DEFAULT_FLOW_RATE_BUG_FIXED_IN: + self._restore_pipette_flow_rates_and_volume_mode( + aspirate_flow_rate=original_aspirate_flow_rate, + dispense_flow_rate=original_dispense_flow_rate, + blow_out_flow_rate=original_blow_out_flow_rate, + is_low_volume=in_low_volume_mode, + ) + def _tip_can_hold_volume_for_multi_dispensing( self, transfer_volume: float, @@ -1712,6 +1808,13 @@ def consolidate_with_liquid_class( # noqa: C901 tiprack_uri=tiprack_uri_for_transfer_props, ) + original_aspirate_flow_rate = self._user_aspirate_flow_rate + original_dispense_flow_rate = self._user_dispense_flow_rate + original_blow_out_flow_rate = self._user_blow_out_flow_rate + in_low_volume_mode = self._engine_client.state.pipettes.get_is_low_volume_mode( + self._pipette_id + ) + working_volume = self.get_working_volume_for_tip_rack(tip_racks[0][1]) source_per_volume_step = ( @@ -1796,6 +1899,14 @@ def consolidate_with_liquid_class( # noqa: C901 if not keep_last_tip: self._drop_tip_for_liquid_class(trash_location, return_tip) + if self._protocol_core.api_version >= _DEFAULT_FLOW_RATE_BUG_FIXED_IN: + self._restore_pipette_flow_rates_and_volume_mode( + aspirate_flow_rate=original_aspirate_flow_rate, + dispense_flow_rate=original_dispense_flow_rate, + blow_out_flow_rate=original_blow_out_flow_rate, + is_low_volume=in_low_volume_mode, + ) + def _get_location_and_well_core_from_next_tip_info( self, tip_info: NextTipInfo, @@ -1904,6 +2015,20 @@ def _drop_tip_for_liquid_class( alternate_drop_location=True, ) + def _restore_pipette_flow_rates_and_volume_mode( + self, + aspirate_flow_rate: Optional[float], + dispense_flow_rate: Optional[float], + blow_out_flow_rate: Optional[float], + is_low_volume: bool, + ) -> None: + # TODO(jbl 2025-09-17) this works for p50 low volume mode but is not guaranteed to work for future low volume + # modes, this should be replaced with something less flaky + self.configure_for_volume(self.get_max_volume() if not is_low_volume else 1) + self._user_aspirate_flow_rate = aspirate_flow_rate + self._user_dispense_flow_rate = dispense_flow_rate + self._user_blow_out_flow_rate = blow_out_flow_rate + def aspirate_liquid_class( self, volume: float, diff --git a/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py b/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py index 14970f7a005..1562f05d708 100644 --- a/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py +++ b/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py @@ -21,7 +21,11 @@ OnLabwareLocation, DropTipWellLocation, ) -from opentrons.protocol_engine.types import StagingSlotLocation, WellLocationType +from opentrons.protocol_engine.types import ( + StagingSlotLocation, + WellLocationType, + LoadedModule, +) from opentrons.types import DeckSlotName, StagingSlotName, Point from . import point_calculations @@ -136,22 +140,47 @@ def check_safe_for_pipette_movement( # noqa: C901 f" will result in collision with thermocycler lid in deck slot A1." ) + def _check_for_column_4_module_collision(slot: DeckSlotName) -> None: + slot_module = engine_state.modules.get_by_slot(slot) + if ( + slot_module + and engine_state.modules.is_column_4_module(slot_module.model) + and _slot_has_potential_colliding_object( + engine_state=engine_state, + pipette_bounds=pipette_bounds_at_well_location, + surrounding_location=slot_module, + ) + ): + raise PartialTipMovementNotAllowedError( + f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot" + f" {slot} with {primary_nozzle} nozzle partial configuration will" + f" result in collision with items on {slot_module.model} mounted in {slot}." + ) + + # We check the labware slot for a module that is mounted in the same cutout + # as the labwares slot but does not occupy the same heirarchy (like the stacker). + _check_for_column_4_module_collision(labware_slot) + for regular_slot in surrounding_slots.regular_slots: if _slot_has_potential_colliding_object( engine_state=engine_state, pipette_bounds=pipette_bounds_at_well_location, - surrounding_slot=regular_slot, + surrounding_location=regular_slot, ): raise PartialTipMovementNotAllowedError( f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot" f" {labware_slot} with {primary_nozzle} nozzle partial configuration" f" will result in collision with items in deck slot {regular_slot}." ) + + # Check for Column 4 Modules that may be descendants of a given surrounding slot + _check_for_column_4_module_collision(regular_slot) + for staging_slot in surrounding_slots.staging_slots: if _slot_has_potential_colliding_object( engine_state=engine_state, pipette_bounds=pipette_bounds_at_well_location, - surrounding_slot=staging_slot, + surrounding_location=staging_slot, ): raise PartialTipMovementNotAllowedError( f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot" @@ -178,18 +207,45 @@ def _get_critical_point_to_use( def _slot_has_potential_colliding_object( engine_state: StateView, pipette_bounds: Tuple[Point, Point, Point, Point], - surrounding_slot: Union[DeckSlotName, StagingSlotName], + surrounding_location: Union[DeckSlotName, StagingSlotName, LoadedModule], ) -> bool: - """Return the slot, if any, that has an item that the pipette might collide into.""" - # Check if slot overlaps with pipette position - slot_pos = engine_state.addressable_areas.get_addressable_area_position( - addressable_area_name=surrounding_slot.id, - do_compatibility_check=False, - ) - slot_bounds = engine_state.addressable_areas.get_addressable_area_bounding_box( - addressable_area_name=surrounding_slot.id, - do_compatibility_check=False, - ) + """Return the slot, if any, that has an item that the pipette might collide into. + Can be provided a Deck Slot, Staging Slot, or Column 4 Module. + """ + if isinstance(surrounding_location, LoadedModule): + if ( + engine_state.modules.is_column_4_module(surrounding_location.model) + and surrounding_location.location is not None + ): + module_area = ( + engine_state.modules.ensure_and_convert_module_fixture_location( + surrounding_location.location.slotName, surrounding_location.model + ) + ) + slot_pos = engine_state.addressable_areas.get_addressable_area_position( + addressable_area_name=module_area, + do_compatibility_check=False, + ) + slot_bounds = ( + engine_state.addressable_areas.get_addressable_area_bounding_box( + addressable_area_name=module_area, + do_compatibility_check=False, + ) + ) + else: + raise ValueError( + f"Error during collision validation, Module {surrounding_location.model} must be in Column 4." + ) + else: + # Check if slot overlaps with pipette position + slot_pos = engine_state.addressable_areas.get_addressable_area_position( + addressable_area_name=surrounding_location.id, + do_compatibility_check=False, + ) + slot_bounds = engine_state.addressable_areas.get_addressable_area_bounding_box( + addressable_area_name=surrounding_location.id, + do_compatibility_check=False, + ) slot_back_left_coords = Point(slot_pos.x, slot_pos.y + slot_bounds.y, slot_pos.z) slot_front_right_coords = Point(slot_pos.x + slot_bounds.x, slot_pos.y, slot_pos.z) @@ -199,13 +255,17 @@ def _slot_has_potential_colliding_object( rectangle2=(slot_back_left_coords, slot_front_right_coords), ): # Check z-height of items in overlapping slot - if isinstance(surrounding_slot, DeckSlotName): + if isinstance(surrounding_location, DeckSlotName): slot_highest_z = engine_state.geometry.get_highest_z_in_slot( - DeckSlotLocation(slotName=surrounding_slot) + DeckSlotLocation(slotName=surrounding_location) + ) + elif isinstance(surrounding_location, LoadedModule): + slot_highest_z = engine_state.geometry.get_highest_z_of_column_4_module( + surrounding_location ) else: slot_highest_z = engine_state.geometry.get_highest_z_in_slot( - StagingSlotLocation(slotName=surrounding_slot) + StagingSlotLocation(slotName=surrounding_location) ) return slot_highest_z >= pipette_bounds[0].z return False diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index 6b4dd6f74c3..3d7f7e1e211 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -78,7 +78,12 @@ FlexStackerCore, ) from .exceptions import InvalidModuleLocationError -from . import load_labware_params, deck_conflict, overlap_versions +from . import ( + load_labware_params, + deck_conflict, + overlap_versions, + _default_liquid_class_versions, +) from opentrons.protocol_engine.resources import labware_validation if TYPE_CHECKING: @@ -492,13 +497,28 @@ def move_lid( # noqa: C901 ) # if this is a labware with a lid, we just need to find its lid_id else: - lid = self._engine_client.state.labware.get_lid_by_labware_id( - labware.labware_id + # we need to check to see if this labware is hosting a lid stack + potential_lid_stack = ( + self._engine_client.state.labware.get_next_child_labware( + labware.labware_id + ) ) - if lid is not None: - lid_id = lid.id + if potential_lid_stack and labware_validation.is_lid_stack( + self._engine_client.state.labware.get_load_name(potential_lid_stack) + ): + lid_id = self._engine_client.state.labware.get_highest_child_labware( + labware.labware_id + ) else: - raise ValueError("Cannot move a lid off of a labware with no lid.") + lid = self._engine_client.state.labware.get_lid_by_labware_id( + labware.labware_id + ) + if lid is not None: + lid_id = lid.id + else: + raise ValueError( + f"Cannot move a lid off of {labware.get_display_name()} because it has no lid." + ) _pick_up_offset = ( LabwareOffsetVector( @@ -602,6 +622,9 @@ def move_lid( # noqa: C901 ) # Handle leftover empty lid stack if there is one + potential_lid_stack = self._engine_client.state.labware.get_next_child_labware( + labware.labware_id + ) if ( labware_validation.is_lid_stack(labware.load_name) and self._engine_client.state.labware.get_highest_child_labware( @@ -619,6 +642,25 @@ def move_lid( # noqa: C901 dropOffset=None, ) ) + elif ( + potential_lid_stack + and labware_validation.is_lid_stack( + self._engine_client.state.labware.get_load_name(potential_lid_stack) + ) + and self._engine_client.state.labware.get_highest_child_labware( + potential_lid_stack + ) + == potential_lid_stack + ): + self._engine_client.execute_command( + cmd.MoveLabwareParams( + labwareId=potential_lid_stack, + newLocation=SYSTEM_LOCATION, + strategy=LabwareMovementStrategy.MANUAL_MOVE_WITHOUT_PAUSE, + pickUpOffset=None, + dropOffset=None, + ) + ) if strategy == LabwareMovementStrategy.USING_GRIPPER: # Clear out last location since it is not relevant to pipetting @@ -1068,8 +1110,12 @@ def define_liquid( display_color=(liquid.displayColor.root if liquid.displayColor else None), ) - def get_liquid_class(self, name: str, version: int) -> LiquidClass: + def get_liquid_class(self, name: str, version: Optional[int]) -> LiquidClass: """Get an instance of a built-in liquid class.""" + if version is None: + version = _default_liquid_class_versions.get_liquid_class_version( + self._api_version, name + ) try: # Check if we have already loaded this liquid class' definition liquid_class_def = self._liquid_class_def_cache[(name, version)] diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py index 3a5e950ad8b..42c9b9dea93 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py @@ -599,7 +599,7 @@ def define_liquid( """Define a liquid to load into a well.""" assert False, "define_liquid only supported on engine core" - def get_liquid_class(self, name: str, version: int) -> LiquidClass: + def get_liquid_class(self, name: str, version: Optional[int]) -> LiquidClass: """Get an instance of a built-in liquid class.""" assert False, "define_liquid_class is only supported on engine core" diff --git a/api/src/opentrons/protocol_api/core/protocol.py b/api/src/opentrons/protocol_api/core/protocol.py index 1c020ddeaa6..4aee85bbc15 100644 --- a/api/src/opentrons/protocol_api/core/protocol.py +++ b/api/src/opentrons/protocol_api/core/protocol.py @@ -297,7 +297,7 @@ def define_liquid( """Define a liquid to load into a well.""" @abstractmethod - def get_liquid_class(self, name: str, version: int) -> LiquidClass: + def get_liquid_class(self, name: str, version: Optional[int]) -> LiquidClass: """Get an instance of a built-in liquid class.""" @abstractmethod diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 32da4833252..6619605720b 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -640,6 +640,9 @@ def load_labware( lid: Optional[str] = None, namespace: Optional[str] = None, version: Optional[int] = None, + *, + lid_namespace: Optional[str] = None, + lid_version: Optional[int] = None, ) -> Labware: """Load a compatible labware onto the labware using its load parameters. @@ -650,6 +653,24 @@ def load_labware( :returns: The initialized and loaded labware object. """ + if self._api_version < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE: + if lid_namespace is not None: + raise APIVersionError( + api_element="The `lid_namespace` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + if lid_version is not None: + raise APIVersionError( + api_element="The `lid_version` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + labware_core = self._protocol_core.load_labware( load_name=name, label=label, @@ -674,11 +695,24 @@ def load_labware( until_version="2.23", current_version=f"{self._api_version}", ) + if ( + self._api_version + < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ): + checked_lid_namespace = namespace + checked_lid_version = version + else: + # This is currently impossible to reach because of the + # `if self._api_version < validation.validation.LID_STACK_VERSION_GATE` + # check above. This is here for now in case that check is removed in + # the future, and for symmetry with the other labware load methods. + checked_lid_namespace = lid_namespace + checked_lid_version = lid_version self._protocol_core.load_lid( load_name=lid, location=labware_core, - namespace=namespace, - version=version, + namespace=checked_lid_namespace, + version=checked_lid_version, ) return labware diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index 7086c02fedc..dab4f9fffc9 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -123,7 +123,7 @@ def load_labware_object(self, labware: Labware) -> Labware: return core.geometry.add_labware(labware) - def load_labware( + def load_labware( # noqa: C901 self, name: str, label: Optional[str] = None, @@ -131,6 +131,11 @@ def load_labware( version: Optional[int] = None, adapter: Optional[str] = None, lid: Optional[str] = None, + *, + adapter_namespace: Optional[str] = None, + adapter_version: Optional[int] = None, + lid_namespace: Optional[str] = None, + lid_version: Optional[int] = None, ) -> Labware: """Load a labware onto the module using its load parameters. @@ -142,7 +147,11 @@ def load_labware( :returns: The initialized and loaded labware object. .. versionadded:: 2.1 - The *label,* *namespace,* and *version* parameters. + The ``label``, ``namespace``, and ``version`` parameters. + + .. versionadded:: 2.26 + The ``adapter_namespace``, ``adapter_version``, + ``lid_namespace``, and ``lid_version`` parameters. """ if self._api_version < APIVersion(2, 1) and ( label is not None or namespace is not None or version != 1 @@ -152,6 +161,40 @@ def load_labware( "are trying to utilize new load_labware parameters in 2.1" ) + if self._api_version < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE: + if adapter_namespace is not None: + raise APIVersionError( + api_element="The `adapter_namespace` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + if adapter_version is not None: + raise APIVersionError( + api_element="The `adapter_version` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + if lid_namespace is not None: + raise APIVersionError( + api_element="The `lid_namespace` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + if lid_version is not None: + raise APIVersionError( + api_element="The `lid_version` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + load_location: Union[ModuleCore, LabwareCore] if adapter is not None: if self._api_version < APIVersion(2, 15): @@ -160,9 +203,21 @@ def load_labware( until_version="2.15", current_version=f"{self._api_version}", ) + + if ( + self._api_version + < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ): + checked_adapter_namespace = namespace + checked_adapter_version = None + else: + checked_adapter_namespace = adapter_namespace + checked_adapter_version = adapter_version + loaded_adapter = self.load_adapter( name=adapter, - namespace=namespace, + namespace=checked_adapter_namespace, + version=checked_adapter_version, ) load_location = loaded_adapter._core else: @@ -193,11 +248,22 @@ def load_labware( until_version="2.23", current_version=f"{self._api_version}", ) + + if ( + self._api_version + < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ): + checked_lid_namespace = namespace + checked_lid_version = None + else: + checked_lid_namespace = lid_namespace + checked_lid_version = lid_version + self._protocol_core.load_lid( load_name=lid, location=labware_core, - namespace=namespace, - version=version, + namespace=checked_lid_namespace, + version=checked_lid_version, ) if isinstance(self._core, LegacyModuleCore): @@ -1272,7 +1338,7 @@ def get_current_storable_labware(self) -> int: def set_stored_labware_items( self, labware: list[Labware], - stacking_offset_z: float | None, + stacking_offset_z: float | None = None, ) -> None: """Configure the labware the Flex Stacker will store during a protocol by providing an initial list of stored labware objects. The start of the list represents the bottom of the Stacker, and the end of the list represents the top of the Stacker. @@ -1330,6 +1396,11 @@ def set_stored_labware( lid: str | None = None, count: int | None = None, stacking_offset_z: float | None = None, + *, + adapter_namespace: str | None = None, + adapter_version: int | None = None, + lid_namespace: str | None = None, + lid_version: int | None = None, ) -> None: """Configure the type and starting quantity of labware the Flex Stacker will store during a protocol. This is the only type of labware you'll be able to store in the Stacker until it's reconfigured. @@ -1338,6 +1409,7 @@ def set_stored_labware( :param str load_name: A string to use for looking up a labware definition. You can find the ``load_name`` for any Opentrons-verified labware on the `Labware Library `__. + :param str namespace: The namespace that the labware definition belongs to. If unspecified, the API will automatically search two namespaces: @@ -1348,19 +1420,34 @@ def set_stored_labware( You might need to specify an explicit ``namespace`` if you have a custom definition whose ``load_name`` is the same as an Opentrons-verified definition, and you want to explicitly choose one or the other. + :param version: The version of the labware definition. You should normally leave this unspecified to let the method choose a version automatically. + :param adapter: An adapter to load the labware on top of. Accepts the same - values as the ``load_name`` parameter of :py:meth:`.load_adapter`. The - adapter will use the same namespace as the labware, and the API will - choose the adapter's version automatically. + values as the ``load_name`` parameter of :py:meth:`.load_adapter`. + + :param adapter_namespace: Applies to ``adapter`` the same way that ``namespace`` + applies to ``load_name``. + + :param adapter_version: Applies to ``adapter`` the same way that ``version`` + applies to ``load_name``. + :param lid: A lid to load the on top of the main labware. Accepts the same values as the ``load_name`` parameter of :py:meth:`~.ProtocolContext.load_lid_stack`. The lid will use the same namespace as the labware, and the API will choose the lid's version automatically. + + :param lid_namespace: Applies to ``lid`` the same way that ``namespace`` + applies to ``load_name``. + + :param lid_version: Applies to ``lid`` the same way that ``version`` + applies to ``load_name``. + :param count: The number of labware that the Flex Stacker should store. If not specified, this will be the maximum amount of this kind of labware that the Flex Stacker is capable of storing. + :param stacking_offset_z: Stacking ``z`` offset in mm of stored labware. If specified, this overrides the calculated value in the labware definition. @@ -1378,18 +1465,63 @@ def set_stored_labware( - Labware on adapter: the adapter (bottom side) of the upper labware unit overlaps with the top side of the labware below. - Labware with lid: the labware (bottom side) of the upper labware unit overlaps with the lid (top side) of the unit below. - Labware with lid and adapter: the adapter (bottom side) of the upper labware unit overlaps with the lid (top side) of the unit below. - """ + + if self._api_version < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE: + if adapter_namespace is not None: + raise APIVersionError( + api_element="The `adapter_namespace` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + if adapter_version is not None: + raise APIVersionError( + api_element="The `adapter_version` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + if lid_namespace is not None: + raise APIVersionError( + api_element="The `lid_namespace` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + if lid_version is not None: + raise APIVersionError( + api_element="The `lid_version` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + + if self._api_version < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE: + checked_adapter_namespace = namespace + checked_adapter_version = version + checked_lid_namespace = namespace + checked_lid_version = version + else: + checked_adapter_namespace = adapter_namespace + checked_adapter_version = adapter_version + checked_lid_namespace = lid_namespace + checked_lid_version = lid_version + self._core.set_stored_labware( main_load_name=load_name, main_namespace=namespace, main_version=version, lid_load_name=lid, - lid_namespace=namespace, - lid_version=version, + lid_namespace=checked_lid_namespace, + lid_version=checked_lid_version, adapter_load_name=adapter, - adapter_namespace=namespace, - adapter_version=version, + adapter_namespace=checked_adapter_namespace, + adapter_version=checked_adapter_version, count=count, stacking_offset_z=stacking_offset_z, ) diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index fc0ea2c1f73..2f6b842f71a 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -405,7 +405,7 @@ def load_labware_from_definition( ) @requires_version(2, 0) - def load_labware( + def load_labware( # noqa: C901 self, load_name: str, location: Union[DeckLocation, OffDeckType], @@ -414,6 +414,11 @@ def load_labware( version: Optional[int] = None, adapter: Optional[str] = None, lid: Optional[str] = None, + *, + adapter_namespace: Optional[str] = None, + adapter_version: Optional[int] = None, + lid_namespace: Optional[str] = None, + lid_version: Optional[int] = None, ) -> Labware: """Load a labware onto a location. @@ -454,18 +459,52 @@ def load_labware( :param version: The version of the labware definition. You should normally leave this unspecified to let ``load_labware()`` choose a version automatically. - :param adapter: An adapter to load the labware on top of. Accepts the same - values as the ``load_name`` parameter of :py:meth:`.load_adapter`. The - adapter will use the same namespace as the labware, and the API will - choose the adapter's version automatically. - .. versionadded:: 2.15 + :param adapter: The load name of an adapter to load the labware on top of. Accepts + the same values as the ``load_name`` parameter of :py:meth:`.load_adapter`. + + .. versionadded:: 2.15 + + :param adapter_namespace: The namespace of the adapter being loaded. + Applies to ``adapter`` the same way that ``namespace`` applies to ``load_name``. + + .. versionchanged:: 2.26 + ``adapter_namespace`` may now be specified explicitly. + Also, when you've specified ``namespace`` but not ``adapter_namespace``, + ``adapter_namespace`` will now independently follow the same search rules + described in ``namespace``. Formerly, it took ``namespace``'s exact value. + + :param adapter_version: The version of the adapter being loaded. + Applies to ``adapter`` the same way that ``version`` applies to ``load_name``. + + .. versionchanged:: 2.26 + ``adapter_version`` may now be specified explicitly. Also, when it's unspecified, + the algorithm to select a version automatically has improved to avoid + selecting versions that do not exist. + :param lid: A lid to load on the top of the main labware. Accepts the same values as the ``load_name`` parameter of :py:meth:`.load_lid_stack`. The lid will use the same namespace as the labware, and the API will choose the lid's version automatically. - .. versionadded:: 2.23 + .. versionadded:: 2.23 + + :param lid_namespace: The namespace of the lid being loaded. + Applies to ``lid`` the same way that ``namespace`` applies to ``load_name``. + + .. versionchanged:: 2.26 + ``lid_namespace`` may now be specified explicitly. + Also, when you've specified ``namespace`` but not ``lid_namespace``, + ``lid_namespace`` will now independently follow the same search rules + described in ``namespace``. Formerly, it took ``namespace``'s exact value. + + :param lid_version: The version of the adapter being loaded. + Applies to ``lid`` the same way that ``version`` applies to ``load_name``. + + .. versionchanged:: 2.26 + ``lid_version`` may now be specified explicitly. Also, when it's unspecified, + the algorithm to select a version automatically has improved to avoid + selecting versions that do not exist. """ if isinstance(location, OffDeckType) and self._api_version < APIVersion(2, 15): @@ -475,6 +514,40 @@ def load_labware( current_version=f"{self._api_version}", ) + if self._api_version < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE: + if adapter_namespace is not None: + raise APIVersionError( + api_element="The `adapter_namespace` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + if adapter_version is not None: + raise APIVersionError( + api_element="The `adapter_version` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + if lid_namespace is not None: + raise APIVersionError( + api_element="The `lid_namespace` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + if lid_version is not None: + raise APIVersionError( + api_element="The `lid_version` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + load_name = validation.ensure_lowercase_name(load_name) load_location: Union[OffDeckType, DeckSlotName, StagingSlotName, LabwareCore] if adapter is not None: @@ -484,10 +557,22 @@ def load_labware( until_version="2.15", current_version=f"{self._api_version}", ) + + if ( + self._api_version + < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ): + checked_adapter_namespace = namespace + checked_adapter_version = None + else: + checked_adapter_namespace = adapter_namespace + checked_adapter_version = adapter_version + loaded_adapter = self.load_adapter( load_name=adapter, location=location, - namespace=namespace, + namespace=checked_adapter_namespace, + version=checked_adapter_version, ) load_location = loaded_adapter._core elif isinstance(location, OffDeckType): @@ -512,11 +597,22 @@ def load_labware( until_version=f"{validation.LID_STACK_VERSION_GATE}", current_version=f"{self._api_version}", ) + + if ( + self._api_version + < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ): + checked_lid_namespace = namespace + checked_lid_version = version + else: + checked_lid_namespace = lid_namespace + checked_lid_version = lid_version + self._core.load_lid( load_name=lid, location=labware_core, - namespace=namespace, - version=version, + namespace=checked_lid_namespace, + version=checked_lid_version, ) labware = Labware( @@ -1377,6 +1473,7 @@ def define_liquid( def get_liquid_class( self, name: str, + version: Optional[int] = None, ) -> LiquidClass: """ Get an instance of an Opentrons-verified liquid class for use in a Flex protocol. @@ -1386,12 +1483,14 @@ def get_liquid_class( - ``"water"``: an Opentrons-verified liquid class based on deionized water. - ``"glycerol_50"``: an Opentrons-verified liquid class for viscous liquid. Based on 50% glycerol. - ``"ethanol_80"``: an Opentrons-verified liquid class for volatile liquid. Based on 80% ethanol. + :param version: The version of the liquid class to retrieve. If left unspecified, the latest definition for the + protocol's API version will be loaded. :raises: ``LiquidClassDefinitionDoesNotExist``: if the specified liquid class does not exist. :returns: A new LiquidClass object. """ - return self._core.get_liquid_class(name=name, version=DEFAULT_LC_VERSION) + return self._core.get_liquid_class(name=name, version=version) @requires_version(2, 24) def define_liquid_class( @@ -1461,6 +1560,9 @@ def load_lid_stack( adapter: Optional[str] = None, namespace: Optional[str] = None, version: Optional[int] = None, + *, + adapter_namespace: Optional[str] = None, + adapter_version: Optional[int] = None, ) -> Labware: """ Load a stack of Opentrons Tough Auto-Sealing Lids onto a valid deck location or adapter. @@ -1468,13 +1570,17 @@ def load_lid_stack( :param str load_name: A string to use for looking up a lid definition. You can find the ``load_name`` for any compatible lid on the Opentrons `Labware Library `_. + :param location: Either a :ref:`deck slot `, like ``1``, ``"1"``, or ``"D1"``, or a valid Opentrons Adapter. + :param int quantity: The quantity of lids to be loaded in the stack. + :param adapter: An adapter to load the lid stack on top of. Accepts the same values as the ``load_name`` parameter of :py:meth:`.load_adapter`. The adapter will use the same namespace as the lid labware, and the API will choose the adapter's version automatically. + :param str namespace: The namespace that the lid labware definition belongs to. If unspecified, the API will automatically search two namespaces: @@ -1490,6 +1596,21 @@ def load_lid_stack( leave this unspecified to let ``load_lid_stack()`` choose a version automatically. + :param adapter_namespace: The namespace of the adapter being loaded. + Applies to ``adapter`` the same way that ``namespace`` applies to ``load_name``. + + .. versionchanged:: 2.26 + ``adapter_namespace`` may now be specified explicitly. + Also, when you've specified ``namespace`` but not ``adapter_namespace``, + ``adapter_namespace`` will now independently follow the same search rules + described in ``namespace``. Formerly, it took ``namespace``'s exact value. + + :param adapter_version: The version of the adapter being loaded. + Applies to ``adapter`` the same way that ``version`` applies to ``load_name``. + + .. versionadded:: 2.26 + ``adapter_version`` may now be specified explicitly. + :return: The initialized and loaded labware object representing the lid stack. .. versionadded:: 2.23 @@ -1502,6 +1623,24 @@ def load_lid_stack( current_version=f"{self._api_version}", ) + if self._api_version < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE: + if adapter_namespace is not None: + raise APIVersionError( + api_element="The `adapter_namespace` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + if adapter_version is not None: + raise APIVersionError( + api_element="The `adapter_version` parameter", + until_version=str( + validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ), + current_version=str(self._api_version), + ) + load_location: Union[DeckSlotName, StagingSlotName, LabwareCore] if isinstance(location, Labware): load_location = location._core @@ -1514,10 +1653,21 @@ def load_lid_stack( if isinstance(load_location, DeckSlotName) or isinstance( load_location, StagingSlotName ): + if ( + self._api_version + < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE + ): + checked_adapter_namespace = namespace + checked_adapter_version = None + else: + checked_adapter_namespace = adapter_namespace + checked_adapter_version = adapter_version + loaded_adapter = self.load_adapter( load_name=adapter, location=load_location.value, - namespace=namespace, + namespace=checked_adapter_namespace, + version=checked_adapter_version, ) load_location = loaded_adapter._core else: diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index c3c3f923624..57fabc02fac 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -62,6 +62,10 @@ # The first APIVersion where Python protocols can use the Flex Stacker module. FLEX_STACKER_VERSION_GATE = APIVersion(2, 23) +# The first APIVersion where various "multi labware load" methods allow you to specify +# the namespace and version of adapters and lids separately from the main labware. +NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE = APIVersion(2, 26) + class InvalidPipetteMountError(ValueError): """An error raised when attempting to load pipettes on an invalid mount.""" diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index 755a23a4f1c..b29c1bed0bf 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -20,6 +20,7 @@ FlexStackerHopperError, FlexStackerLabwareRetrieveError, FlexStackerShuttleOccupiedError, + FlexStackerLabwareStoreError, ) from . import absorbance_reader @@ -948,6 +949,7 @@ DefinedErrorData[FlexStackerHopperError], DefinedErrorData[FlexStackerLabwareRetrieveError], DefinedErrorData[FlexStackerShuttleOccupiedError], + DefinedErrorData[FlexStackerLabwareStoreError], ] diff --git a/api/src/opentrons/protocol_engine/commands/flex_stacker/common.py b/api/src/opentrons/protocol_engine/commands/flex_stacker/common.py index 7eff9a7c29e..f629fe5ddbe 100644 --- a/api/src/opentrons/protocol_engine/commands/flex_stacker/common.py +++ b/api/src/opentrons/protocol_engine/commands/flex_stacker/common.py @@ -148,6 +148,19 @@ class FlexStackerLabwareRetrieveError(ErrorOccurrence): errorInfo: FailedLabware +class FlexStackerLabwareStoreError(ErrorOccurrence): + """Returned when the labware was not able to get to the shuttle.""" + + isDefined: bool = True + errorType: Literal[ + "flexStackerLabwareStoreFailed" + ] = "flexStackerLabwareStoreFailed" + + errorCode: str = ErrorCodes.STACKER_SHUTTLE_LABWARE_FAILED.value.code + detail: str = ErrorCodes.STACKER_SHUTTLE_LABWARE_FAILED.value.detail + errorInfo: FailedLabware + + class FlexStackerShuttleOccupiedError(ErrorOccurrence): """Returned when the Flex Stacker Shuttle is occupied when it shouldn't be.""" diff --git a/api/src/opentrons/protocol_engine/commands/flex_stacker/store.py b/api/src/opentrons/protocol_engine/commands/flex_stacker/store.py index 5072809dde1..b8b2bfa874f 100644 --- a/api/src/opentrons/protocol_engine/commands/flex_stacker/store.py +++ b/api/src/opentrons/protocol_engine/commands/flex_stacker/store.py @@ -10,6 +10,7 @@ from opentrons_shared_data.errors.exceptions import ( FlexStackerStallError, FlexStackerShuttleMissingError, + FlexStackerShuttleLabwareError, ) from ..command import ( @@ -22,6 +23,7 @@ from ..flex_stacker.common import ( FlexStackerStallOrCollisionError, FlexStackerShuttleError, + FlexStackerLabwareStoreError, labware_locations_for_group, labware_location_base_sequence, primary_location_sequence, @@ -115,7 +117,8 @@ class StoreResult(BaseModel): _ExecuteReturn = Union[ SuccessData[StoreResult], DefinedErrorData[FlexStackerStallOrCollisionError] - | DefinedErrorData[FlexStackerShuttleError], + | DefinedErrorData[FlexStackerShuttleError] + | DefinedErrorData[FlexStackerLabwareStoreError], ] @@ -180,7 +183,7 @@ def _verify_labware_to_store( ) return labware_ids[0], None, lid_id - async def execute(self, params: StoreParams) -> _ExecuteReturn: + async def execute(self, params: StoreParams) -> _ExecuteReturn: # noqa: C901 """Execute the labware storage command.""" stacker_state = self._state_view.modules.get_flex_stacker_substate( params.moduleId @@ -250,6 +253,21 @@ async def execute(self, params: StoreParams) -> _ExecuteReturn: errorInfo={"labwareId": primary_id}, ), ) + except FlexStackerShuttleLabwareError as e: + return DefinedErrorData( + public=FlexStackerLabwareStoreError( + id=self._model_utils.generate_id(), + createdAt=self._model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=self._model_utils.generate_id(), + createdAt=self._model_utils.get_timestamp(), + error=e, + ) + ], + errorInfo={"labwareId": primary_id}, + ), + ) id_list = [ id for id in (primary_id, maybe_adapter_id, maybe_lid_id) if id is not None diff --git a/api/src/opentrons/protocol_engine/execution/labware_movement.py b/api/src/opentrons/protocol_engine/execution/labware_movement.py index 9d3d929e48e..351b76922d6 100644 --- a/api/src/opentrons/protocol_engine/execution/labware_movement.py +++ b/api/src/opentrons/protocol_engine/execution/labware_movement.py @@ -4,7 +4,7 @@ from typing import Optional, TYPE_CHECKING, overload -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition, Quirks from opentrons.types import Point @@ -234,23 +234,25 @@ async def move_labware_with_gripper( # noqa: C901 # we only want to check position after the gripper has opened and # should be holding labware if holding_labware: - labware_bbox = self._state_store.labware.get_dimensions( - labware_definition=labware_definition - ) - well_bbox = self._state_store.labware.get_well_bbox( + grip_specs = self._state_store.labware.get_gripper_width_specs( labware_definition=labware_definition ) + + disable_geometry_grip_check = False + if labware_definition.parameters.quirks is not None: + disable_geometry_grip_check = ( + Quirks.disableGeometryBasedGripCheck.value + in labware_definition.parameters.quirks + ) + # todo(mm, 2024-09-26): This currently raises a lower-level 2015 FailedGripperPickupError. # Convert this to a higher-level 3001 LabwareDroppedError or 3002 LabwareNotPickedUpError, # depending on what waypoint we're at, to propagate a more specific error code to users. ot3api.raise_error_if_gripper_pickup_failed( - expected_grip_width=labware_bbox.y, - grip_width_uncertainty_wider=abs( - max(well_bbox.y - labware_bbox.y, 0) - ), - grip_width_uncertainty_narrower=abs( - min(well_bbox.y - labware_bbox.y, 0) - ), + expected_grip_width=grip_specs.targetY, + grip_width_uncertainty_wider=grip_specs.uncertaintyWider, + grip_width_uncertainty_narrower=grip_specs.uncertaintyNarrower, + disable_geometry_grip_check=disable_geometry_grip_check, ) await ot3api.move_to( mount=gripper_mount, abs_position=waypoint_data.position diff --git a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py index ee721c88f2c..3853629ce14 100644 --- a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py @@ -70,6 +70,7 @@ class LoadedStaticPipetteData: plunger_positions: Dict[str, float] shaft_ul_per_mm: float available_sensors: pipette_definition.AvailableSensorDefinition + volume_mode: pip_types.LiquidClasses # pip_types Liquid Classes refers to volume modes class VirtualPipetteDataProvider: @@ -298,6 +299,7 @@ def _get_virtual_pipette_static_config_by_model( # noqa: C901 shaft_ul_per_mm=config.shaft_ul_per_mm, available_sensors=config.available_sensors or pipette_definition.AvailableSensorDefinition(sensors=[]), + volume_mode=liquid_class, ) def get_virtual_pipette_static_config( @@ -353,6 +355,7 @@ def get_pipette_static_config( plunger_positions=pipette_dict["plunger_positions"], shaft_ul_per_mm=pipette_dict["shaft_ul_per_mm"], available_sensors=available_sensors, + volume_mode=pipette_dict["volume_mode"], ) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 55feb16e78f..176d7c5e83b 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -274,12 +274,20 @@ def get_highest_z_in_slot( try: labware_id = self._labware.get_id_by_module(module_id=module_id) except LabwareNotLoadedOnModuleError: - return self._modules.get_module_highest_z( - module_id=module_id, - addressable_areas=self._addressable_areas, - ) + # For the time being we will ignore column 4 modules in this check to avoid conflating results + if self._modules.is_column_4_module(slot_item.model) is False: + return self._modules.get_module_highest_z( + module_id=module_id, + addressable_areas=self._addressable_areas, + ) else: - return self.get_highest_z_of_labware_stack(labware_id) + # For the time being we will ignore column 4 modules in this check to avoid conflating results + if self._modules.is_column_4_module(slot_item.model) is False: + return self.get_highest_z_of_labware_stack(labware_id) + # todo (cb, 2025-09-15): For now we skip column 4 modules and handle them seperately in + # get_highest_z_of_column_4_module, so this will return 0. In the future we may want to consolidate + # this to make it more apparently at this point in the query process. + return 0 elif isinstance(slot_item, LoadedLabware): # get stacked heights of all labware in the slot return self.get_highest_z_of_labware_stack(slot_item.id) @@ -301,6 +309,26 @@ def get_highest_z_of_labware_stack(self, labware_id: str) -> float: return self.get_labware_highest_z(labware_id) return self.get_highest_z_of_labware_stack(stacked_labware_id) + def get_highest_z_of_column_4_module(self, module: LoadedModule) -> float: + """Get the highest Z-point of the topmost labware in the stack of labware on the given column 4 module. + + If there is no labware on the given module, returns highest z of the module. + """ + if self._modules.is_column_4_module(module.model): + try: + labware_id = self._labware.get_id_by_module(module_id=module.id) + except LabwareNotLoadedOnModuleError: + return self._modules.get_module_highest_z( + module_id=module.id, + addressable_areas=self._addressable_areas, + ) + else: + return self.get_highest_z_of_labware_stack(labware_id) + else: + raise ValueError( + "Module must be a Column 4 Module to determine maximum z height." + ) + def get_min_travel_z( self, pipette_id: str, diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 93e38106f78..14f383f6d39 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -46,6 +46,7 @@ AddressableAreaLocation, NonStackedLocation, Dimensions, + GripSpecs, LabwareOffset, LabwareOffsetVector, LabwareOffsetLocationSequence, @@ -1430,3 +1431,68 @@ def get_well_bbox(self, labware_definition: LabwareDefinition) -> Dimensions: ): return Dimensions(0, 0, 0) return Dimensions(max_x - min_x, max_y - min_y, max_z) + + def _gripper_uncertainty_narrower( + self, labware_bbox: Dimensions, well_bbox: Dimensions, target_grip_width: float + ) -> float: + """Most narrower the gripper can be than the target while still likely gripping successfully. + + This number can't just be the 0, because that is not going to be accurate if the labware is + skirted - the dimensions are a full bounding box including the skirt, and the labware is + narrower than that at the point where it is gripped. The general heuristic is that we can't + get to the wells; but some labware don't have wells, so we need alternate values. + + The number will be interpreted relative to the target width, which is (for now) the labware + outer bounding box. + + TODO: This should be a number looked up from the definition. + """ + if well_bbox.y == 0: + # This labware has no wells; use a fixed minimum + return 5 + if well_bbox.y > labware_bbox.y: + # This labware has a very odd definition with wells outside its dimensions. + # Return the smaller value. + return 0 + # An ok heuristic for successful grip is if we don't get all the way to the wells. + return target_grip_width - well_bbox.y + + def _gripper_uncertainty_wider( + self, labware_bbox: Dimensions, well_bbox: Dimensions, target_grip_width: float + ) -> float: + """Most wider the gripper can be than the target while still likely gripping successfully. + + This can be a lot closer to 0, since the bounding box of the labware will certainly be the + widest point (if it's defined without error), but since there might be error in the + definition we allow some slop. + + The number will be interpreted relative to the target width, which is (for now) the labware + outer bounding box. + + TODO: This should be a number looked up from the definition. + """ + # This will be 0 unless the wells are wider than the labware + return max(well_bbox.y - target_grip_width, 0) + + def get_gripper_width_specs( + self, labware_definition: LabwareDefinition + ) -> GripSpecs: + """Get the target and bounds for a successful grip of this labware.""" + outer_bounds = self.get_dimensions(labware_definition=labware_definition) + well_bounds = self.get_well_bbox(labware_definition=labware_definition) + narrower = self._gripper_uncertainty_narrower( + labware_bbox=outer_bounds, + well_bbox=well_bounds, + target_grip_width=outer_bounds.y, + ) + wider = self._gripper_uncertainty_wider( + labware_bbox=outer_bounds, + well_bbox=well_bounds, + target_grip_width=outer_bounds.y, + ) + return GripSpecs( + # TODO: This should be a number looked up from the definition. + targetY=outer_bounds.y, + uncertaintyNarrower=narrower, + uncertaintyWider=wider, + ) diff --git a/api/src/opentrons/protocol_engine/state/modules.py b/api/src/opentrons/protocol_engine/state/modules.py index 598e11bb567..4bec637cf2e 100644 --- a/api/src/opentrons/protocol_engine/state/modules.py +++ b/api/src/opentrons/protocol_engine/state/modules.py @@ -1330,6 +1330,12 @@ def raise_if_module_in_location( f"Module {module.model} is already present at {location}." ) + def is_column_4_module(self, model: ModuleModel) -> bool: + """Determine whether or not a module is a Column 4 Module.""" + if model in _COLUMN_4_MODULES: + return True + return False + def get_default_gripper_offsets( self, module_id: str ) -> Optional[LabwareMovementOffsetData]: diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index b21f826fd15..953dabfcb1a 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -17,7 +17,10 @@ from opentrons_shared_data.pipette import pipette_definition from opentrons_shared_data.pipette.ul_per_mm import calculate_ul_per_mm -from opentrons_shared_data.pipette.types import UlPerMmAction +from opentrons_shared_data.pipette.types import ( + UlPerMmAction, + LiquidClasses as VolumeModes, +) from opentrons.config.defaults_ot2 import Z_RETRACT_DISTANCE from opentrons.hardware_control.dev_types import PipetteDict @@ -107,6 +110,7 @@ class StaticPipetteConfig: plunger_positions: Dict[str, float] shaft_ul_per_mm: float available_sensors: pipette_definition.AvailableSensorDefinition + volume_mode: VolumeModes @dataclasses.dataclass @@ -212,7 +216,7 @@ def _update_tip_state(self, state_update: update_types.StateUpdate) -> None: # we identify tip classes - looking things up by volume is not enough. tip_configuration = list( static_config.tip_configuration_lookup_table.values() - )[0] + )[-1] self._state.flow_rates_by_id[pipette_id] = FlowRates( default_blow_out=tip_configuration.default_blowout_flowrate.values_by_api_level, default_aspirate=tip_configuration.default_aspirate_flowrate.values_by_api_level, @@ -230,7 +234,7 @@ def _update_tip_state(self, state_update: update_types.StateUpdate) -> None: # TODO(seth,9/11/2023): bad way to do defaulting, see above. tip_configuration = list( static_config.tip_configuration_lookup_table.values() - )[0] + )[-1] self._state.flow_rates_by_id[pipette_id] = FlowRates( default_blow_out=tip_configuration.default_blowout_flowrate.values_by_api_level, default_aspirate=tip_configuration.default_aspirate_flowrate.values_by_api_level, @@ -313,6 +317,7 @@ def _update_pipette_config(self, state_update: update_types.StateUpdate) -> None plunger_positions=config.plunger_positions, shaft_ul_per_mm=config.shaft_ul_per_mm, available_sensors=config.available_sensors, + volume_mode=config.volume_mode, ) self._state.flow_rates_by_id[ state_update.pipette_config.pipette_id @@ -867,6 +872,10 @@ def get_nozzle_configuration_supports_lld(self, pipette_id: str) -> bool: return False return True + def get_is_low_volume_mode(self, pipette_id: str) -> bool: + """Determine if the pipette is currently in low volume mode.""" + return self.get_config(pipette_id).volume_mode == VolumeModes.lowVolumeDefault + def lookup_volume_to_mm_conversion( self, pipette_id: str, volume: float, action: str ) -> float: diff --git a/api/src/opentrons/protocol_engine/types/__init__.py b/api/src/opentrons/protocol_engine/types/__init__.py index a33b0ecd02e..0ac1cafa02e 100644 --- a/api/src/opentrons/protocol_engine/types/__init__.py +++ b/api/src/opentrons/protocol_engine/types/__init__.py @@ -102,6 +102,7 @@ LoadedLabware, LabwareParentDefinition, LabwareWellId, + GripSpecs, ) from .liquid import HexColor, EmptyLiquidId, LiquidId, Liquid, FluidKind, AspiratedFluid from .labware_offset_location import ( @@ -257,6 +258,7 @@ "LabwareOffsetVector", "LabwareParentDefinition", "LabwareWellId", + "GripSpecs", # Liquids "HexColor", "EmptyLiquidId", diff --git a/api/src/opentrons/protocol_engine/types/labware.py b/api/src/opentrons/protocol_engine/types/labware.py index c4d0091e63b..f5e45a80acd 100644 --- a/api/src/opentrons/protocol_engine/types/labware.py +++ b/api/src/opentrons/protocol_engine/types/labware.py @@ -23,6 +23,15 @@ from .deck_configuration import DeckLocationDefinition +@dataclass(frozen=True) +class GripSpecs: + """Data for how a labware should be gripped.""" + + uncertaintyWider: float + uncertaintyNarrower: float + targetY: float + + class OverlapOffset(Vec3f): """Offset representing overlap space of one labware on top of another labware or module.""" diff --git a/api/src/opentrons/protocols/api_support/definitions.py b/api/src/opentrons/protocols/api_support/definitions.py index b44cd3b8ea9..460100f032b 100644 --- a/api/src/opentrons/protocols/api_support/definitions.py +++ b/api/src/opentrons/protocols/api_support/definitions.py @@ -1,6 +1,6 @@ from .types import APIVersion -MAX_SUPPORTED_VERSION = APIVersion(2, 25) +MAX_SUPPORTED_VERSION = APIVersion(2, 26) """The maximum supported protocol API version in this release.""" MIN_SUPPORTED_VERSION = APIVersion(2, 0) diff --git a/api/tests/opentrons/drivers/asyncio/communication/test_serial_connection.py b/api/tests/opentrons/drivers/asyncio/communication/test_serial_connection.py index a14b7cc5cbf..c65480fd4bb 100644 --- a/api/tests/opentrons/drivers/asyncio/communication/test_serial_connection.py +++ b/api/tests/opentrons/drivers/asyncio/communication/test_serial_connection.py @@ -129,6 +129,27 @@ async def test_send_command_with_retry( ) +async def test_send_command_with_zero_retries( + mock_serial_port: AsyncMock, async_subject: AsyncResponseSerialConnection, ack: str +) -> None: + """It should a command once""" + mock_serial_port.read_until.side_effect = (b"", b"") + + # Set the default number of retries to 1, we want to overide this with + # the retries from the subject.send_data(data, retries=0) method call. + async_subject._number_of_retries = 1 + + with pytest.raises(NoResponse): + # We want this to overwrite the internal `_number_of_retries` + await async_subject.send_data(data="send data", retries=0) + + mock_serial_port.timeout_override.assert_called_once_with("timeout", None) + mock_serial_port.write.assert_called_once_with(data=b"send data") + mock_serial_port.read_until.assert_called_once_with(match=ack.encode()) + mock_serial_port.close.assert_called_once() + mock_serial_port.open.assert_called_once() + + async def test_send_command_with_retry_exhausted( mock_serial_port: AsyncMock, subject: SerialKind ) -> None: diff --git a/api/tests/opentrons/drivers/flex_stacker/test_driver.py b/api/tests/opentrons/drivers/flex_stacker/test_driver.py index 662db54f17e..a7e509f3a3f 100644 --- a/api/tests/opentrons/drivers/flex_stacker/test_driver.py +++ b/api/tests/opentrons/drivers/flex_stacker/test_driver.py @@ -4,6 +4,7 @@ from binascii import Error as BinError from decoy import Decoy from mock import AsyncMock, MagicMock +from opentrons.drivers.asyncio.communication.errors import NoResponse from opentrons.drivers.asyncio.communication.serial_connection import ( AsyncResponseSerialConnection, ) @@ -591,7 +592,7 @@ async def test_get_tof_histogram_frame( get_measurement = types.GCODE.GET_TOF_MEASUREMENT.build_command().add_element( types.TOFSensor.X.name ) - connection.send_command.assert_any_call(get_measurement) + connection.send_command.assert_any_call(get_measurement, retries=0) connection.reset_mock() # Test cancel transfer @@ -611,7 +612,7 @@ async def test_get_tof_histogram_frame( .add_element(types.TOFSensor.X.name) .add_element("R") ) - connection.send_command.assert_any_call(get_measurement) + connection.send_command.assert_any_call(get_measurement, retries=0) connection.reset_mock() # Test invalid index response @@ -675,7 +676,7 @@ async def test_get_tof_histogram( types.TOFSensor.X.name ) connection.send_command.assert_any_call(manage_measurement) - connection.send_command.assert_any_call(get_measurement) + connection.send_command.assert_any_call(get_measurement, retries=0) connection.reset_mock() # Test invalid frame_id @@ -699,7 +700,33 @@ async def test_get_tof_histogram( types.TOFSensor.X.name ) connection.send_command.assert_any_call(manage_measurement) - connection.send_command.assert_any_call(get_measurement) + connection.send_command.assert_any_call(get_measurement, retries=0) + connection.reset_mock() + + # Test resend mechanism + get_measurement = ( + types.GCODE.GET_TOF_MEASUREMENT.build_command() + .add_element(types.TOFSensor.X.name) + .add_element("R") + ) + payload = [p for p in get_histogram_payload(30)] + connection.send_command.side_effect = [ + "M215 X:1 T:2 M:3", + "M225 X K:0 C:1 L:3840", + payload[0], + payload[1], + # We raise NoResponse on frame 3 to simulate a timeout and force a resend + NoResponse("", "Timeout"), + # After the timeout we expect the same packet to be resent + payload[2] + # Then the rest of the packets + ] + payload[3:] + + response = await subject.get_tof_histogram(types.TOFSensor.X) + + connection.send_command.assert_any_call(manage_measurement) + # Assert that the M226 GCODE with `R` (resend) element was sent + connection.send_command.assert_any_call(get_measurement, retries=0) connection.reset_mock() diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py index fc53235bb69..32340727a46 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py @@ -258,7 +258,7 @@ def _subsystems_entry(info: DeviceInfoCache) -> Tuple[SubSystem, SubSystemState] current_fw_version=info.version, next_fw_version=2, current_fw_sha=info.shortsha, - pcba_revision="A1", + pcba_revision="A1.0", update_state=None, fw_update_needed=False, ) @@ -1292,6 +1292,7 @@ async def test_engage_motors( (80, 79, 0, 0, 1, 92, 60, False), (80, 45, 40, 0, 1, 92, 60, True), (80, 100, 0, 40, 0, 92, 60, True), + (95.5, 84, 0, 5, 5, 94, 60, True), ], ) def test_grip_error_detection( @@ -1316,8 +1317,44 @@ def test_grip_error_detection( narrower, actual_grip_width, allowed_error, + hard_min, hard_max, + ) + + +@pytest.mark.parametrize( + "expected_grip_width,actual_grip_width,wider,narrower,allowed_error,hard_max,hard_min,raise_error", + [ + (95.5, 84, 0, 5, 5, 94, 60, False), + (95.5, 60, 0, 5, 5, 94, 60, True), + (95.5, 94, 0, 5, 5, 94, 60, True), + ], +) +def test_grip_error_detection_disable_geometry( + controller: OT3Controller, + expected_grip_width: float, + actual_grip_width: float, + wider: float, + narrower: float, + allowed_error: float, + hard_max: float, + hard_min: float, + raise_error: bool, +) -> None: + context = cast( + AbstractContextManager[None], + pytest.raises(FailedGripperPickupError) if raise_error else does_not_raise(), + ) + with context: + controller.check_gripper_position_within_bounds( + expected_grip_width, + wider, + narrower, + actual_grip_width, + allowed_error, hard_min, + hard_max, + True, ) diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_subsystem_manager.py b/api/tests/opentrons/hardware_control/backends/test_ot3_subsystem_manager.py index fa00bbc414d..f040c13f1b1 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_subsystem_manager.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_subsystem_manager.py @@ -92,7 +92,7 @@ def default_fw_version() -> int: def default_revision() -> types.PCBARevision: - return types.PCBARevision("A1") + return types.PCBARevision("A1.0") def device_info_for( diff --git a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py index fa62314d44e..996659ce1d4 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py @@ -749,6 +749,19 @@ def test_maps_prevents_stacker_non_labware_double_occupancy( ), 170, ), + ( + ( + Point(x=150, y=250, z=40), + Point(x=250, y=201, z=40), + Point(x=150, y=201, z=40), + Point(x=250, y=250, z=40), + ), + pytest.raises( + pipette_movement_conflict.PartialTipMovementNotAllowedError, + match="result in collision with items on flexStackerModuleV1 mounted in B3.", + ), + 170, + ), ], ) def test_deck_conflict_raises_for_bad_pipette_move( @@ -766,6 +779,8 @@ def test_deck_conflict_raises_for_bad_pipette_move( - we are checking for conflicts when moving to a labware in C2. For each test case, we are moving to a different point in the destination labware, with the same pipette and tip + - we are checking for conflicts when moving to a point that would collide with a + flex stacker in column 4 at position B4 but nothing in the ancestor slot of B3 Note: this test does not stub out the slot overlap checker function in order to preserve readability of the test. That means the test does @@ -842,6 +857,24 @@ def test_deck_conflict_raises_for_bad_pipette_move( ) ).then_return(pipette_bounds) + stacker = LoadedModule( + id="fake-stacker-id", + model=ModuleModel.FLEX_STACKER_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_B3), + serialNumber="serial-number", + ) + decoy.when(mock_state_view.modules.get_by_slot(DeckSlotName.SLOT_B3)).then_return( + stacker + ) + decoy.when(mock_state_view.modules.is_column_4_module(stacker.model)).then_return( + True + ) + decoy.when( + mock_state_view.modules.ensure_and_convert_module_fixture_location( + DeckSlotName.SLOT_B3, stacker.model + ) + ).then_return("flexStackerModuleV1B4") + decoy.when( adjacent_slots_getters.get_surrounding_slots(5, robot_type="OT-3 Standard") ).then_return( @@ -850,6 +883,7 @@ def test_deck_conflict_raises_for_bad_pipette_move( DeckSlotName.SLOT_D1, DeckSlotName.SLOT_D2, DeckSlotName.SLOT_C1, + DeckSlotName.SLOT_B3, ], staging_slots=[StagingSlotName.SLOT_C4], ) @@ -888,6 +922,38 @@ def test_deck_conflict_raises_for_bad_pipette_move( StagingSlotLocation(slotName=StagingSlotName.SLOT_C4) ) ).then_return(50) + decoy.when( + mock_state_view.addressable_areas.get_addressable_area_position( + addressable_area_name="B3", do_compatibility_check=False + ) + ).then_return(Point(150, 200, 0)) + decoy.when( + mock_state_view.addressable_areas.get_addressable_area_bounding_box( + addressable_area_name="B3", do_compatibility_check=False + ) + ).then_return(Dimensions(90, 90, 0)) + + # Ensure slot B3 is empty so we can test the stacker + decoy.when( + mock_state_view.geometry.get_highest_z_in_slot( + DeckSlotLocation(slotName=DeckSlotName.SLOT_B3) + ) + ).then_return(0) + + decoy.when( + mock_state_view.addressable_areas.get_addressable_area_position( + addressable_area_name="flexStackerModuleV1B4", do_compatibility_check=False + ) + ).then_return(Point(200, 200, 0)) + decoy.when( + mock_state_view.addressable_areas.get_addressable_area_bounding_box( + addressable_area_name="flexStackerModuleV1B4", do_compatibility_check=False + ) + ).then_return(Dimensions(90, 90, 0)) + + decoy.when( + mock_state_view.geometry.get_highest_z_of_column_4_module(stacker) + ).then_return(50) for slot_name in [DeckSlotName.SLOT_C1, DeckSlotName.SLOT_D1, DeckSlotName.SLOT_D2]: decoy.when( mock_state_view.geometry.get_highest_z_in_slot( diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index c20f508caa0..8f1e7676a54 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -175,6 +175,8 @@ def subject( LoadedPipette.model_construct(mount=MountType.LEFT) # type: ignore[call-arg] ) + decoy.when(mock_protocol_core.api_version).then_return(MAX_SUPPORTED_VERSION) + decoy.when(mock_engine_client.state.pipettes.get_flow_rates("abc123")).then_return( FlowRates( default_aspirate={"1.2": 2.3}, @@ -2900,3 +2902,131 @@ def test_get_next_tip_raises_for_starting_tip_with_partial_config( tip_racks=tip_racks, starting_well=mock_starting_well, ) + + +@pytest.mark.parametrize("version", versions_at_or_above(APIVersion(2, 26))) +def test_flow_rates_use_defaults_on_newer_api_versions( + decoy: Decoy, + mock_engine_client: EngineClient, + mock_sync_hardware: SyncHardwareAPI, + mock_protocol_core: ProtocolCore, + version: APIVersion, +) -> None: + """It should default to getting the flow rates from the engine when not set by a user.""" + decoy.when(mock_engine_client.state.pipettes.get("abc123")).then_return( + LoadedPipette.model_construct(mount=MountType.LEFT) # type: ignore[call-arg] + ) + + decoy.when(mock_protocol_core.api_version).then_return(version) + + decoy.when(mock_engine_client.state.pipettes.get_flow_rates("abc123")).then_return( + FlowRates( + default_aspirate={"1.2": 2.3}, + default_dispense={"3.4": 4.5}, + default_blow_out={"5.6": 6.7}, + ), + ) + + subject = InstrumentCore( + pipette_id="abc123", + engine_client=mock_engine_client, + sync_hardware_api=mock_sync_hardware, + protocol_core=mock_protocol_core, + default_movement_speed=1337, + ) + + # Flow rates should default to the engine core + assert subject.get_aspirate_flow_rate() == 2.3 + assert subject.get_dispense_flow_rate() == 4.5 + assert subject.get_blow_out_flow_rate() == 6.7 + + decoy.when(mock_engine_client.state.pipettes.get_flow_rates("abc123")).then_return( + FlowRates( + default_aspirate={"1.2": 9.8}, + default_dispense={"3.4": 7.6}, + default_blow_out={"5.6": 5.4}, + ), + ) + + # Now that the flow rates from the engine have "changed" these calls should reflect that + assert subject.get_aspirate_flow_rate() == 9.8 + assert subject.get_dispense_flow_rate() == 7.6 + assert subject.get_blow_out_flow_rate() == 5.4 + + # Flow rates should use user set ones + subject.set_flow_rate(aspirate=99.9, dispense=66.6, blow_out=33.3) + + assert subject.get_aspirate_flow_rate() == 99.9 + assert subject.get_dispense_flow_rate() == 66.6 + assert subject.get_blow_out_flow_rate() == 33.3 + + # Resetting them via configure for volume should go back to engine defaults + subject.configure_for_volume(1337) + + assert subject.get_aspirate_flow_rate() == 9.8 + assert subject.get_dispense_flow_rate() == 7.6 + assert subject.get_blow_out_flow_rate() == 5.4 + + +@pytest.mark.parametrize("version", versions_below(APIVersion(2, 26), flex_only=True)) +def test_flow_rates_use_maintains_buggy_defaults_on_older_versions( + decoy: Decoy, + mock_engine_client: EngineClient, + mock_sync_hardware: SyncHardwareAPI, + mock_protocol_core: ProtocolCore, + version: APIVersion, +) -> None: + """It should only use the initial defaults on pre 2.26 API versions.""" + decoy.when(mock_engine_client.state.pipettes.get("abc123")).then_return( + LoadedPipette.model_construct(mount=MountType.LEFT) # type: ignore[call-arg] + ) + + decoy.when(mock_protocol_core.api_version).then_return(version) + + decoy.when(mock_engine_client.state.pipettes.get_flow_rates("abc123")).then_return( + FlowRates( + default_aspirate={"1.2": 2.3}, + default_dispense={"3.4": 4.5}, + default_blow_out={"5.6": 6.7}, + ), + ) + + subject = InstrumentCore( + pipette_id="abc123", + engine_client=mock_engine_client, + sync_hardware_api=mock_sync_hardware, + protocol_core=mock_protocol_core, + default_movement_speed=1337, + ) + + # Flow rates should default to the engine core + assert subject.get_aspirate_flow_rate() == 2.3 + assert subject.get_dispense_flow_rate() == 4.5 + assert subject.get_blow_out_flow_rate() == 6.7 + + decoy.when(mock_engine_client.state.pipettes.get_flow_rates("abc123")).then_return( + FlowRates( + default_aspirate={"1.2": 9.8}, + default_dispense={"3.4": 7.6}, + default_blow_out={"5.6": 5.4}, + ), + ) + + # Flow rates should stay with their initial default + assert subject.get_aspirate_flow_rate() == 2.3 + assert subject.get_dispense_flow_rate() == 4.5 + assert subject.get_blow_out_flow_rate() == 6.7 + + # Flow rates should use user set ones + subject.set_flow_rate(aspirate=99.9, dispense=66.6, blow_out=33.3) + + assert subject.get_aspirate_flow_rate() == 99.9 + assert subject.get_dispense_flow_rate() == 66.6 + assert subject.get_blow_out_flow_rate() == 33.3 + + # Resetting them via configure for volume should NOT reset the user set ones for older buggy versions + subject.configure_for_volume(1337) + + assert subject.get_aspirate_flow_rate() == 99.9 + assert subject.get_dispense_flow_rate() == 66.6 + assert subject.get_blow_out_flow_rate() == 33.3 diff --git a/api/tests/opentrons/protocol_api/core/engine/test_load_labware_params.py b/api/tests/opentrons/protocol_api/core/engine/test_load_labware_params.py index af52a9f3385..76d07569b7a 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_load_labware_params.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_load_labware_params.py @@ -8,10 +8,7 @@ from opentrons.protocols.api_support.constants import OPENTRONS_NAMESPACE from opentrons.protocols.api_support.types import APIVersion -from tests.opentrons.protocol_api import ( - versions_at_or_above, - versions_between, -) +from tests.opentrons.protocol_api import versions_between @pytest.mark.parametrize( @@ -95,7 +92,13 @@ def test_resolve_load_labware_params( high_inclusive_bound=APIVersion(2, 22), ) ], - *[(api_version, 4) for api_version in versions_at_or_above(APIVersion(2, 25))], + *[ + (api_version, 4) + for api_version in versions_between( + low_inclusive_bound=APIVersion(2, 25), + high_exclusive_bound=(APIVersion(2, 26)), + ) + ], ], ) def test_default_labware_version_dependent_on_api_version( diff --git a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py index e8cf79c4d5e..4f963cd07dc 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py @@ -75,6 +75,7 @@ LabwareCore, ModuleCore, load_labware_params, + _default_liquid_class_versions, ) from opentrons.protocol_api._liquid import Liquid, LiquidClass from opentrons.protocol_api.disposal_locations import TrashBin, WasteChute @@ -118,6 +119,17 @@ def patch_mock_load_labware_params( monkeypatch.setattr(load_labware_params, name, decoy.mock(func=func)) +@pytest.fixture(autouse=True) +def patch_default_liquid_class_versions( + decoy: Decoy, monkeypatch: pytest.MonkeyPatch +) -> None: + """Mock out _default_liquid_class_versions.py functions.""" + for name, func in inspect.getmembers( + _default_liquid_class_versions, inspect.isfunction + ): + monkeypatch.setattr(_default_liquid_class_versions, name, decoy.mock(func=func)) + + @pytest.fixture(autouse=True) def patch_mock_load_liquid_class_def( decoy: Decoy, monkeypatch: pytest.MonkeyPatch @@ -1886,6 +1898,42 @@ def test_define_liquid_class( assert subject.get_liquid_class("water", 123) == expected_liquid_class +def test_define_liquid_class_without_version_provided( + decoy: Decoy, + subject: ProtocolCore, + minimal_liquid_class_def1: LiquidClassSchemaV1, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should create a LiquidClass with the most recent version and cache the definition.""" + expected_liquid_class = LiquidClass( + _name="water1", _display_name="water 1", _by_pipette_setting={} + ) + decoy.when( + _default_liquid_class_versions.get_liquid_class_version( + subject.api_version, "water" + ) + ).then_return(987) + decoy.when(liquid_classes.load_definition("water", version=987)).then_return( + minimal_liquid_class_def1 + ) + + assert subject.get_liquid_class("water", version=None) == expected_liquid_class + + # Test that specified version number works too + decoy.when(liquid_classes.load_definition("water", version=654)).then_return( + minimal_liquid_class_def2 + ) + different_liquid_class = subject.get_liquid_class("water", 654) + assert different_liquid_class.name == "water2" + assert different_liquid_class.display_name == "water 2" + + # Test that definition caching works + decoy.when(liquid_classes.load_definition("water", version=987)).then_return( + minimal_liquid_class_def2 + ) + assert subject.get_liquid_class("water", version=None) == expected_liquid_class + + def test_get_labware_location_deck_slot( decoy: Decoy, mock_engine_client: EngineClient, diff --git a/api/tests/opentrons/protocol_api/test_flex_stacker_context.py b/api/tests/opentrons/protocol_api/test_flex_stacker_context.py index 8eefed291ca..906fdfcf999 100644 --- a/api/tests/opentrons/protocol_api/test_flex_stacker_context.py +++ b/api/tests/opentrons/protocol_api/test_flex_stacker_context.py @@ -1,8 +1,11 @@ """Tests for Protocol API Flex Stacker contexts.""" +from unittest.mock import sentinel + import pytest from decoy import Decoy, matchers +from opentrons.protocols.api_support.util import APIVersionError from opentrons.types import DeckSlotName from opentrons.legacy_broker import LegacyBroker from opentrons.protocols.api_support.types import APIVersion @@ -14,7 +17,7 @@ ) from opentrons.protocol_api.core.core_map import LoadedCoreMap -from . import versions_at_or_above +from . import versions_at_or_above, versions_between @pytest.fixture @@ -105,28 +108,161 @@ def test_empty( @pytest.mark.parametrize( - "api_version", versions_at_or_above(from_version=APIVersion(2, 25)) + "api_version", versions_at_or_above(from_version=APIVersion(2, 26)) ) def test_set_stored_labware( decoy: Decoy, mock_core: FlexStackerCore, subject: FlexStackerContext ) -> None: """It should route arguments appropriately.""" subject.set_stored_labware( - "load_name", "namespace", 1, "adapter", "lid", 2, stacking_offset_z=1.0 + load_name=sentinel.main_load_name, + namespace=sentinel.main_namespace, + version=sentinel.main_version, + adapter=sentinel.adapter_load_name, + lid=sentinel.lid_load_name, + count=sentinel.count, + stacking_offset_z=sentinel.stacking_offset_z, + ) + decoy.verify( + mock_core.set_stored_labware( + main_load_name=sentinel.main_load_name, + main_namespace=sentinel.main_namespace, + main_version=sentinel.main_version, + lid_load_name=sentinel.lid_load_name, + lid_namespace=None, + lid_version=None, + adapter_load_name=sentinel.adapter_load_name, + adapter_namespace=None, + adapter_version=None, + count=sentinel.count, + stacking_offset_z=sentinel.stacking_offset_z, + ) + ) + + subject.set_stored_labware( + load_name=sentinel.main_load_name, + namespace=sentinel.main_namespace, + version=sentinel.main_version, + adapter=sentinel.adapter_load_name, + lid=sentinel.lid_load_name, + count=sentinel.count, + stacking_offset_z=sentinel.stacking_offset_z, + adapter_namespace=sentinel.adapter_namespace, + adapter_version=sentinel.adapter_version, + lid_namespace=sentinel.lid_namespace, + lid_version=sentinel.lid_version, ) decoy.verify( mock_core.set_stored_labware( - main_load_name="load_name", - main_namespace="namespace", - main_version=1, - lid_load_name="lid", - lid_namespace="namespace", - lid_version=1, - adapter_load_name="adapter", - adapter_namespace="namespace", - adapter_version=1, - count=2, + main_load_name=sentinel.main_load_name, + main_namespace=sentinel.main_namespace, + main_version=sentinel.main_version, + lid_load_name=sentinel.lid_load_name, + lid_namespace=sentinel.lid_namespace, + lid_version=sentinel.lid_version, + adapter_load_name=sentinel.adapter_load_name, + adapter_namespace=sentinel.adapter_namespace, + adapter_version=sentinel.adapter_version, + count=sentinel.count, + stacking_offset_z=sentinel.stacking_offset_z, + ) + ) + + +@pytest.mark.parametrize( + "api_version", + versions_between( + low_inclusive_bound=APIVersion(2, 25), high_exclusive_bound=APIVersion(2, 26) + ), +) +def test_set_stored_labware_namespace_version_params_minimum_version( + subject: FlexStackerContext, +) -> None: + """{adapter,lid}_{namespace,version} params were unavailable before version 2.26.""" + with pytest.raises(APIVersionError): + subject.set_stored_labware( + "load_name", + "namespace", + 1, + "adapter", + "lid", + 2, + stacking_offset_z=1.0, + adapter_namespace="foo", + ) + with pytest.raises(APIVersionError): + subject.set_stored_labware( + "load_name", + "namespace", + 1, + "adapter", + "lid", + 2, stacking_offset_z=1.0, + adapter_version=123, + ) + with pytest.raises(APIVersionError): + subject.set_stored_labware( + "load_name", + "namespace", + 1, + "adapter", + "lid", + 2, + stacking_offset_z=1.0, + lid_namespace="foo", + ) + with pytest.raises(APIVersionError): + subject.set_stored_labware( + "load_name", + "namespace", + 1, + "adapter", + "lid", + 2, + stacking_offset_z=1.0, + lid_version=123, + ) + + +@pytest.mark.parametrize( + "api_version", + versions_between( + low_inclusive_bound=APIVersion(2, 25), high_exclusive_bound=APIVersion(2, 26) + ), +) +def test_set_stored_labware_namespace_version_params_legacy_behavior( + decoy: Decoy, + mock_core: FlexStackerCore, + subject: FlexStackerContext, +) -> None: + """Test legacy (version <2.26) behavior when the {adapter,lid}_{namespace,version} params were omitted. + + The adapter and lid namespace and version were taken from the main labware's namespace and version. + """ + subject.set_stored_labware( + load_name=sentinel.main_load_name, + namespace=sentinel.main_namespace, + version=sentinel.main_version, + adapter=sentinel.adapter_load_name, + lid=sentinel.lid_load_name, + count=sentinel.count, + stacking_offset_z=sentinel.stacking_offset_z, + ) + + decoy.verify( + mock_core.set_stored_labware( + main_load_name=sentinel.main_load_name, + main_namespace=sentinel.main_namespace, + main_version=sentinel.main_version, + lid_load_name=sentinel.lid_load_name, + lid_namespace=sentinel.main_namespace, + lid_version=sentinel.main_version, + adapter_load_name=sentinel.adapter_load_name, + adapter_namespace=sentinel.main_namespace, + adapter_version=sentinel.main_version, + count=sentinel.count, + stacking_offset_z=sentinel.stacking_offset_z, ) ) diff --git a/api/tests/opentrons/protocol_api/test_module_context.py b/api/tests/opentrons/protocol_api/test_module_context.py index 02d91e6b939..9a8cce9d2a8 100644 --- a/api/tests/opentrons/protocol_api/test_module_context.py +++ b/api/tests/opentrons/protocol_api/test_module_context.py @@ -294,7 +294,7 @@ def test_load_labware_with_adapter( decoy.when( mock_protocol_core.load_adapter( load_name="adaptation", - namespace="ideal", + namespace=None, version=None, location=mock_core, ) diff --git a/api/tests/opentrons/protocol_api/test_protocol_context.py b/api/tests/opentrons/protocol_api/test_protocol_context.py index 676c03a82c6..f9b2e5186b1 100644 --- a/api/tests/opentrons/protocol_api/test_protocol_context.py +++ b/api/tests/opentrons/protocol_api/test_protocol_context.py @@ -2,6 +2,7 @@ import inspect from typing import cast, Dict +from unittest.mock import sentinel import pytest from decoy import Decoy, matchers @@ -62,6 +63,10 @@ ) from opentrons.protocol_engine.errors import LabwareMovementNotAllowedError from opentrons.protocol_engine.clients import SyncClient as EngineClient +from tests.opentrons.protocol_api import ( + versions_at_or_above, + versions_between, +) @pytest.fixture(autouse=True) @@ -743,10 +748,80 @@ def test_load_adapter_on_staging_slot( decoy.verify(mock_core_map.add(mock_labware_core, result), times=1) +@pytest.mark.parametrize("api_version", [APIVersion(2, 25)]) +def test_load_labware_lid_adapter_namespace_version_requires_new_api_version( + subject: ProtocolContext, +) -> None: + """Make sure parameters new to apiLevel 2.26 raise, if given in older apiLevels.""" + with pytest.raises(APIVersionError, match="adapter_namespace"): + subject.load_labware("load_name", "A1", adapter_namespace="foo") + + with pytest.raises(APIVersionError, match="adapter_version"): + subject.load_labware("load_name", "A1", adapter_version=123) + + with pytest.raises(APIVersionError, match="lid_namespace"): + subject.load_labware("load_name", "A1", lid_namespace="foo") + + with pytest.raises(APIVersionError, match="lid_version"): + subject.load_labware("load_name", "A1", lid_version=123) + + +@pytest.mark.parametrize( + ( + "api_version", + "input_adapter_namespace", + "input_adapter_version", + "expected_adapter_namespace", + "expected_adapter_version", + ), + [ + *[ + # Old APIVersion: Adapter namespace and version cannot be specified explicitly. + # Adapter namespace always follows main labware, and adapter version is always None. + ( + v, + None, + None, + sentinel.input_namespace, + None, + ) + for v in versions_between( + low_inclusive_bound=APIVersion(2, 15), + high_exclusive_bound=APIVersion(2, 26), + ) + ], + *[ + # New APIVersion: Adapter namespace and version are used as-is if specified explicitly. + ( + v, + sentinel.input_adapter_namespace, + sentinel.input_adapter_version, + sentinel.input_adapter_namespace, + sentinel.input_adapter_version, + ) + for v in versions_at_or_above(APIVersion(2, 26)) + ], + *[ + # New APIVersion: Adapter namespace and version default to None if not provided. + ( + v, + None, + None, + None, + None, + ) + for v in versions_at_or_above(APIVersion(2, 26)) + ], + ], +) def test_load_labware_on_adapter( decoy: Decoy, mock_core: ProtocolCore, mock_core_map: LoadedCoreMap, + input_adapter_namespace: str | None, + input_adapter_version: int | None, + expected_adapter_namespace: str | None, + expected_adapter_version: int | None, api_version: APIVersion, subject: ProtocolContext, ) -> None: @@ -754,23 +829,25 @@ def test_load_labware_on_adapter( mock_labware_core = decoy.mock(cls=LabwareCore) mock_adapter_core = decoy.mock(cls=LabwareCore) - decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LABWARE")).then_return( - "lowercase_labware" - ) + decoy.when( + mock_validation.ensure_lowercase_name(sentinel.input_load_name) + ).then_return(sentinel.lowercase_input_load_name) - decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_ADAPTER")).then_return( - "lowercase_adapter" - ) + decoy.when( + mock_validation.ensure_lowercase_name(sentinel.input_adapter) + ).then_return(sentinel.lowercase_input_adapter) decoy.when(mock_core.robot_type).then_return("OT-3 Standard") decoy.when( - mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") - ).then_return(DeckSlotName.SLOT_5) + mock_validation.ensure_and_convert_deck_slot( + sentinel.input_location, api_version, "OT-3 Standard" + ) + ).then_return(sentinel.validated_location) decoy.when( mock_core.load_adapter( - load_name="lowercase_adapter", - location=DeckSlotName.SLOT_5, - namespace="some_namespace", - version=None, + load_name=sentinel.lowercase_input_adapter, + location=sentinel.validated_location, + namespace=expected_adapter_namespace, + version=expected_adapter_version, ) ).then_return(mock_adapter_core) @@ -778,11 +855,11 @@ def test_load_labware_on_adapter( decoy.when( mock_core.load_labware( - load_name="lowercase_labware", + load_name=sentinel.lowercase_input_load_name, location=mock_adapter_core, - label="some_display_name", - namespace="some_namespace", - version=1337, + label="input_label", + namespace=sentinel.input_namespace, + version=sentinel.input_version, ) ).then_return(mock_labware_core) @@ -791,12 +868,14 @@ def test_load_labware_on_adapter( decoy.when(mock_labware_core.get_well_columns()).then_return([]) result = subject.load_labware( - load_name="UPPERCASE_LABWARE", - location=42, - label="some_display_name", - namespace="some_namespace", - version=1337, - adapter="UPPERCASE_ADAPTER", + load_name=sentinel.input_load_name, + location=sentinel.input_location, + label="input_label", + namespace=sentinel.input_namespace, + version=sentinel.input_version, + adapter=sentinel.input_adapter, + adapter_namespace=input_adapter_namespace, + adapter_version=input_adapter_version, ) assert isinstance(result, Labware) @@ -805,70 +884,118 @@ def test_load_labware_on_adapter( decoy.verify(mock_core_map.add(mock_labware_core, result), times=1) -@pytest.mark.parametrize("api_version", [APIVersion(2, 23)]) +@pytest.mark.parametrize( + ( + "api_version", + "input_lid_namespace", + "input_lid_version", + "expected_lid_namespace", + "expected_lid_version", + ), + [ + *[ + # Old APIVersion: Lid namespace and version cannot be specified explicitly. + # Lid namespace and version always follow main labware. + ( + v, + None, + None, + sentinel.input_namespace, + sentinel.input_version, + ) + for v in versions_between( + low_inclusive_bound=APIVersion(2, 23), + high_exclusive_bound=APIVersion(2, 26), + ) + ], + *[ + # New APIVersion: Lid namespace and version are used as-is if specified explicitly. + ( + v, + sentinel.input_lid_namespace, + sentinel.input_lid_version, + sentinel.input_lid_namespace, + sentinel.input_lid_version, + ) + for v in versions_at_or_above(APIVersion(2, 26)) + ], + *[ + # New APIVersion: Lid namespace and version default to None if not provided. + ( + v, + None, + None, + None, + None, + ) + for v in versions_at_or_above(APIVersion(2, 26)) + ], + ], +) def test_load_labware_with_lid( decoy: Decoy, mock_core: ProtocolCore, mock_core_map: LoadedCoreMap, + input_lid_namespace: str | None, + input_lid_version: int | None, + expected_lid_namespace: str | None, + expected_lid_version: int | None, api_version: APIVersion, subject: ProtocolContext, ) -> None: """It should create a labware with a lid on it using its execution core.""" mock_labware_core = decoy.mock(cls=LabwareCore) - mock_lid_core = decoy.mock(cls=LabwareCore) - decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LABWARE")).then_return( - "lowercase_labware" - ) + decoy.when( + mock_validation.ensure_lowercase_name(sentinel.input_load_name) + ).then_return(sentinel.lowercase_input_load_name) - decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LID")).then_return( - "lowercase_lid" - ) decoy.when(mock_core.robot_type).then_return("OT-3 Standard") decoy.when( - mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") - ).then_return(DeckSlotName.SLOT_C1) + mock_validation.ensure_and_convert_deck_slot( + sentinel.input_location, api_version, "OT-3 Standard" + ) + ).then_return(sentinel.validated_location) decoy.when( mock_core.load_labware( - load_name="lowercase_labware", - location=DeckSlotName.SLOT_C1, - label="some_display_name", - namespace="some_namespace", - version=1337, + load_name=sentinel.lowercase_input_load_name, + location=sentinel.validated_location, + label="input_label", + namespace=sentinel.input_namespace, + version=sentinel.input_version, ) ).then_return(mock_labware_core) - decoy.when(mock_lid_core.get_well_columns()).then_return([]) - - decoy.when( - mock_core.load_lid( - load_name="lowercase_lid", - location=mock_labware_core, - namespace="some_namespace", - version=1337, - ) - ).then_return(mock_lid_core) decoy.when(mock_labware_core.get_name()).then_return("Full Name") decoy.when(mock_labware_core.get_display_name()).then_return("Display Name") decoy.when(mock_labware_core.get_well_columns()).then_return([]) - decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LABWARE")).then_return( - "lowercase_labware" - ) - result = subject.load_labware( - load_name="UPPERCASE_LABWARE", - location=42, - label="some_display_name", - namespace="some_namespace", - version=1337, - lid="lowercase_lid", + load_name=sentinel.input_load_name, + location=sentinel.input_location, + label="input_label", + namespace=sentinel.input_namespace, + version=sentinel.input_version, + lid=sentinel.input_lid, + lid_namespace=input_lid_namespace, + lid_version=input_lid_version, ) assert isinstance(result, Labware) assert result.name == "Full Name" + decoy.verify( + mock_core.load_lid( + # todo(mm, 2025-08-26): We're passing load_name=input_lid directly without lowercasing it, + # unlike how we lowercase adapter names. Is this a bug? + load_name=sentinel.input_lid, + location=mock_labware_core, + namespace=expected_lid_namespace, + version=expected_lid_version, + ) + ) + decoy.verify(mock_core_map.add(mock_labware_core, result), times=1) @@ -995,6 +1122,112 @@ def test_move_lids_from_stack( assert isinstance(result, Labware) +@pytest.mark.parametrize("api_version", [APIVersion(2, 23)]) +def test_move_lids_from_stack_via_stack_parent( + decoy: Decoy, + mock_core: ProtocolCore, + mock_core_map: LoadedCoreMap, + api_version: APIVersion, + subject: ProtocolContext, +) -> None: + """It should move a lid onto an empty riser, create a stack, and then move the lid back off by referencing the riser.""" + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_C1) + mock_riser_core = decoy.mock(cls=LabwareCore) + decoy.when(mock_validation.ensure_lowercase_name("RISER_LABWARE")).then_return( + "riser_labware" + ) + decoy.when( + mock_core.load_labware( + load_name="riser_labware", + location=DeckSlotName.SLOT_C1, + label=None, + namespace="some_namespace", + version=1337, + ) + ).then_return(mock_riser_core) + + decoy.when(mock_riser_core.get_name()).then_return("RISER_LABWARE") + decoy.when(mock_riser_core.get_display_name()).then_return("") + decoy.when(mock_riser_core.get_well_columns()).then_return([]) + + riser_lw = subject.load_labware( + load_name="RISER_LABWARE", + location=42, + label=None, + namespace="some_namespace", + version=1337, + ) + assert isinstance(riser_lw, Labware) + + # Load the lid stack on top of the riser + mock_lid_core = decoy.mock(cls=LabwareCore) + decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LID")).then_return( + "lowercase_lid" + ) + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_core.load_lid_stack( + load_name="lowercase_lid", + location=riser_lw._core, + quantity=1, + namespace="some_namespace", + version=1337, + ) + ).then_return(mock_lid_core) + + decoy.when(mock_lid_core.get_name()).then_return("STACK_OBJECT") + decoy.when(mock_lid_core.get_display_name()).then_return("") + decoy.when(mock_lid_core.get_well_columns()).then_return([]) + + result = subject.load_lid_stack( + load_name="UPPERCASE_LID", + location=riser_lw, + quantity=1, + namespace="some_namespace", + version=1337, + ) + + assert isinstance(result, Labware) + assert result.name == "STACK_OBJECT" + + # Move the lid, by referencing only the riser itself + subject.move_lid(riser_lw, "D3") + + # Load another lid stack where the lidstack once was, verifying its engine object is gone + mock_lid_core_2 = decoy.mock(cls=LabwareCore) + decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LID_2")).then_return( + "lowercase_lid_2" + ) + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_core.load_lid_stack( + load_name="lowercase_lid_2", + location=riser_lw._core, + quantity=1, + namespace="some_namespace", + version=1337, + ) + ).then_return(mock_lid_core_2) + + decoy.when(mock_lid_core_2.get_name()).then_return("STACK_OBJECT_2") + decoy.when(mock_lid_core_2.get_display_name()).then_return("") + decoy.when(mock_lid_core_2.get_well_columns()).then_return([]) + + result_2 = subject.load_lid_stack( + load_name="UPPERCASE_LID_2", + location=riser_lw, + quantity=1, + namespace="some_namespace", + version=1337, + ) + + assert isinstance(result_2, Labware) + assert result_2.name == "STACK_OBJECT_2" + + @pytest.mark.parametrize("api_version", [APIVersion(2, 22)]) def test_move_labware_lids_old( decoy: Decoy, @@ -1416,7 +1649,7 @@ def test_load_trash_bin_raises_for_staging_slot( subject.load_trash_bin("bleh") -def test_load_wast_chute( +def test_load_waste_chute( decoy: Decoy, mock_core: ProtocolCore, api_version: APIVersion, @@ -1849,11 +2082,11 @@ def test_define_liquid_class( expected_liquid_class = LiquidClass( _name="volatile_100", _display_name="volatile 100%", _by_pipette_setting={} ) - decoy.when(mock_core.get_liquid_class("volatile_90", 1)).then_return( + decoy.when(mock_core.get_liquid_class("volatile_90", 123)).then_return( expected_liquid_class ) decoy.when(mock_core.robot_type).then_return(robot_type) - assert subject.get_liquid_class("volatile_90") == expected_liquid_class + assert subject.get_liquid_class("volatile_90", 123) == expected_liquid_class @pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) diff --git a/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_store.py b/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_store.py index d3a77bb71b3..098e78eaa3f 100644 --- a/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_store.py +++ b/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_store.py @@ -12,6 +12,7 @@ from opentrons.protocol_engine.commands.flex_stacker.common import ( FlexStackerStallOrCollisionError, FlexStackerShuttleError, + FlexStackerLabwareStoreError, ) from opentrons.protocol_engine.resources import ModelUtils @@ -49,6 +50,7 @@ from opentrons_shared_data.errors.exceptions import ( FlexStackerStallError, FlexStackerShuttleMissingError, + FlexStackerShuttleLabwareError, ) @@ -200,6 +202,14 @@ async def test_store_raises_if_not_configured( ), FlexStackerShuttleError, ), + ( + FlexStackerShuttleLabwareError( + serial="123", + shuttle_state=PlatformState.UNKNOWN, + labware_expected=True, + ), + FlexStackerLabwareStoreError, + ), ], ) async def test_store_raises_if_stall( diff --git a/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py b/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py index dd40867cba3..982f7ec825f 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py +++ b/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py @@ -18,7 +18,10 @@ ConfigureForVolumeResult, ConfigureForVolumeImplementation, ) -from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette.types import ( + PipetteNameType, + LiquidClasses as VolumeModes, +) from opentrons_shared_data.pipette.pipette_definition import AvailableSensorDefinition from ..pipette_fixtures import get_default_nozzle_map from opentrons.types import Point @@ -78,6 +81,7 @@ async def test_configure_for_volume_implementation( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.lowVolumeDefault, ) decoy.when( diff --git a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py index 8c78fd23d7d..e903c7a7463 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py +++ b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py @@ -21,7 +21,10 @@ SupportedTipsDefinition, ) -from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette.types import ( + PipetteNameType, + LiquidClasses as VolumeModes, +) from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError from opentrons.protocol_engine.state.state import StateView @@ -234,6 +237,7 @@ async def test_liquid_probe_implementation( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) ) @@ -329,6 +333,7 @@ async def test_liquid_not_found_error( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) ) decoy.when( @@ -456,6 +461,7 @@ async def test_liquid_probe_tip_checking( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) ) with pytest.raises(TipNotAttachedError): @@ -519,6 +525,7 @@ async def test_liquid_probe_plunger_preparedness_checking( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) ) decoy.when(state_view.pipettes.get_aspirated_volume(pipette_id)).then_return(None) @@ -584,6 +591,7 @@ async def test_liquid_probe_volume_checking( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) ) decoy.when( @@ -657,6 +665,7 @@ async def test_liquid_probe_location_checking( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) ) decoy.when( @@ -727,6 +736,7 @@ async def test_liquid_probe_stall( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) ) decoy.when( diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py index 759497ba24c..a5f20cc8d90 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py @@ -9,7 +9,10 @@ import pytest from decoy import Decoy -from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette.types import ( + PipetteNameType, + LiquidClasses as VolumeModes, +) from opentrons_shared_data.robot.types import RobotType from opentrons_shared_data.pipette.pipette_definition import AvailableSensorDefinition from opentrons.types import MountType, Point @@ -86,6 +89,7 @@ async def test_load_pipette_implementation( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) decoy.when( @@ -166,6 +170,7 @@ async def test_load_pipette_implementation_96_channel( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.lowVolumeDefault, ) decoy.when( diff --git a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py index 6ea32923c02..0f7857e0b1f 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py @@ -9,7 +9,10 @@ import pytest from _pytest.fixtures import SubRequest -from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette.types import ( + PipetteNameType, + LiquidClasses as VolumeModes, +) from opentrons_shared_data.pipette import pipette_definition from opentrons_shared_data.labware.types import LabwareUri @@ -178,6 +181,7 @@ def loaded_static_pipette_data( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) diff --git a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py index 7cef81a75ca..d3f1d11095b 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py @@ -1,13 +1,15 @@ """Test labware movement command execution side effects.""" + from __future__ import annotations from datetime import datetime from typing import TYPE_CHECKING, Union, Optional -from unittest.mock import sentinel from decoy import Decoy, matchers import pytest +from opentrons_shared_data.labware.labware_definition import LabwareDefinition2 + from opentrons.protocol_engine.execution import EquipmentHandler, MovementHandler from opentrons.hardware_control import HardwareControlAPI from opentrons.types import DeckSlotName, Point @@ -24,6 +26,7 @@ NonStackedLocation, LabwareMovementOffsetData, Dimensions, + GripSpecs, ) from opentrons.protocol_engine.execution.thermocycler_plate_lifter import ( ThermocyclerPlateLifter, @@ -87,8 +90,17 @@ def heater_shaker_movement_flagger(decoy: Decoy) -> HeaterShakerMovementFlagger: return decoy.mock(cls=HeaterShakerMovementFlagger) +@pytest.fixture +def labware_def(decoy: Decoy) -> LabwareDefinition2: + """Get a mocked out LabwareDefinition2 instance.""" + return decoy.mock(cls=LabwareDefinition2) + + async def set_up_decoy_hardware_gripper( - decoy: Decoy, ot3_hardware_api: OT3API, state_store: StateStore + decoy: Decoy, + ot3_hardware_api: OT3API, + state_store: StateStore, + labware_def: LabwareDefinition2, ) -> None: """Shared hardware gripper decoy setup.""" decoy.when(state_store.config.use_virtual_gripper).then_return(False) @@ -109,9 +121,7 @@ async def set_up_decoy_hardware_gripper( decoy.when(ot3_hardware_api.hardware_gripper.jaw_width).then_return(89) - decoy.when( - state_store.labware.get_grip_force(sentinel.my_teleporting_labware_def) - ).then_return(100) + decoy.when(state_store.labware.get_grip_force(labware_def)).then_return(100) decoy.when(state_store.labware.get_labware_offset("new-offset-id")).then_return( LabwareOffset( @@ -154,13 +164,16 @@ async def test_raise_error_if_gripper_pickup_failed( state_store: StateStore, thermocycler_plate_lifter: ThermocyclerPlateLifter, ot3_hardware_api: OT3API, + labware_def: LabwareDefinition2, subject: LabwareMovementHandler, ) -> None: """Test that the gripper position check is called at the right time.""" # This function should only be called when after the gripper opens, # and then closes again. This is when we expect the labware to be # in the gripper jaws. - await set_up_decoy_hardware_gripper(decoy, ot3_hardware_api, state_store) + await set_up_decoy_hardware_gripper( + decoy, ot3_hardware_api, state_store, labware_def + ) assert ot3_hardware_api.hardware_gripper user_pick_up_offset = Point(x=123, y=234, z=345) @@ -173,9 +186,11 @@ async def test_raise_error_if_gripper_pickup_failed( starting_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_1) to_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_2) + labware_def.parameters.quirks = [] + decoy.when( state_store.labware.get_definition("my-teleporting-labware") - ).then_return(sentinel.my_teleporting_labware_def) + ).then_return(labware_def) mock_tc_context_manager = decoy.mock(name="mock_tc_context_manager") decoy.when( @@ -200,28 +215,20 @@ async def test_raise_error_if_gripper_pickup_failed( decoy.when( state_store.geometry.get_labware_grip_point( - labware_definition=sentinel.my_teleporting_labware_def, + labware_definition=labware_def, location=starting_location, ) ).then_return(Point(101, 102, 119.5)) decoy.when( state_store.geometry.get_labware_grip_point( - labware_definition=sentinel.my_teleporting_labware_def, location=to_location + labware_definition=labware_def, location=to_location ) ).then_return(Point(201, 202, 219.5)) decoy.when( - state_store.labware.get_dimensions( - labware_definition=sentinel.my_teleporting_labware_def - ) - ).then_return(Dimensions(x=100, y=85, z=0)) - - decoy.when( - state_store.labware.get_well_bbox( - labware_definition=sentinel.my_teleporting_labware_def - ) - ).then_return(Dimensions(x=99, y=80, z=1)) + state_store.labware.get_gripper_width_specs(labware_definition=labware_def) + ).then_return(GripSpecs(targetY=100, uncertaintyNarrower=5, uncertaintyWider=10)) await subject.move_labware_with_gripper( labware_id="my-teleporting-labware", @@ -239,21 +246,24 @@ async def test_raise_error_if_gripper_pickup_failed( await ot3_hardware_api.ungrip(), await ot3_hardware_api.grip(force_newtons=100), ot3_hardware_api.raise_error_if_gripper_pickup_failed( - expected_grip_width=85, - grip_width_uncertainty_wider=0, + expected_grip_width=100, + grip_width_uncertainty_wider=10, grip_width_uncertainty_narrower=5, + disable_geometry_grip_check=False, ), await ot3_hardware_api.grip(force_newtons=100), ot3_hardware_api.raise_error_if_gripper_pickup_failed( - expected_grip_width=85, - grip_width_uncertainty_wider=0, + expected_grip_width=100, + grip_width_uncertainty_wider=10, grip_width_uncertainty_narrower=5, + disable_geometry_grip_check=False, ), await ot3_hardware_api.grip(force_newtons=100), ot3_hardware_api.raise_error_if_gripper_pickup_failed( - expected_grip_width=85, - grip_width_uncertainty_wider=0, + expected_grip_width=100, + grip_width_uncertainty_wider=10, grip_width_uncertainty_narrower=5, + disable_geometry_grip_check=False, ), await ot3_hardware_api.disengage_axes([Axis.Z_G]), await ot3_hardware_api.ungrip(), @@ -296,6 +306,7 @@ async def test_move_labware_with_gripper( thermocycler_plate_lifter: ThermocyclerPlateLifter, ot3_hardware_api: OT3API, subject: LabwareMovementHandler, + labware_def: LabwareDefinition2, from_location: Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation], to_location: Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation], slide_offset: Optional[Point], @@ -304,11 +315,14 @@ async def test_move_labware_with_gripper( # TODO (spp, 2023-07-26): this test does NOT stub out movement waypoints in order to # keep this as the semi-smoke test that it previously was. We should add a proper # smoke test for gripper labware movement with actual labware and make this a unit test. - await set_up_decoy_hardware_gripper(decoy, ot3_hardware_api, state_store) + labware_def.parameters.quirks = [] + await set_up_decoy_hardware_gripper( + decoy, ot3_hardware_api, state_store, labware_def + ) decoy.when( state_store.labware.get_definition("my-teleporting-labware") - ).then_return(sentinel.my_teleporting_labware_def) + ).then_return(labware_def) user_pick_up_offset = Point(x=123, y=234, z=345) user_drop_offset = Point(x=111, y=222, z=333) @@ -333,26 +347,22 @@ async def test_move_labware_with_gripper( ) # TODO: Is this used for anything? Could this have been a sentinel? Are sentinels appropriate here? decoy.when( - state_store.labware.get_dimensions( - labware_definition=sentinel.my_teleporting_labware_def - ) + state_store.labware.get_dimensions(labware_definition=labware_def) ).then_return(Dimensions(x=100, y=85, z=0)) decoy.when( - state_store.labware.get_well_bbox( - labware_definition=sentinel.my_teleporting_labware_def - ) + state_store.labware.get_well_bbox(labware_definition=labware_def) ).then_return(Dimensions(x=99, y=80, z=1)) decoy.when( state_store.geometry.get_labware_grip_point( - labware_definition=sentinel.my_teleporting_labware_def, + labware_definition=labware_def, location=from_location, ) ).then_return(Point(101, 102, 119.5)) decoy.when( state_store.geometry.get_labware_grip_point( - labware_definition=sentinel.my_teleporting_labware_def, location=to_location + labware_definition=labware_def, location=to_location ) ).then_return(Point(201, 202, 219.5)) mock_tc_context_manager = decoy.mock(name="mock_tc_context_manager") @@ -362,6 +372,10 @@ async def test_move_labware_with_gripper( ) ).then_return(mock_tc_context_manager) + decoy.when( + state_store.labware.get_gripper_width_specs(labware_definition=labware_def) + ).then_return(GripSpecs(targetY=100, uncertaintyNarrower=5, uncertaintyWider=10)) + expected_waypoints = [ Point(100, 100, 999), # move to above slot 1 Point(100, 100, 116.5), # move to labware on slot 1 @@ -515,6 +529,7 @@ async def test_labware_movement_raises_without_gripper( """It should raise an error when attempting a gripper movement without a gripper.""" decoy.when(state_store.config.use_virtual_gripper).then_return(False) decoy.when(ot3_hardware_api.has_gripper()).then_return(False) + with pytest.raises(GripperNotAttachedError): await subject.move_labware_with_gripper( labware_id="labware-id", diff --git a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py index c909953ddf1..ce72222215f 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py @@ -2,7 +2,11 @@ from typing import Dict from sys import maxsize import pytest -from opentrons_shared_data.pipette.types import PipetteNameType, PipetteModel +from opentrons_shared_data.pipette.types import ( + PipetteNameType, + PipetteModel, + LiquidClasses as VolumeModes, +) from opentrons_shared_data.pipette import pipette_definition, types as pip_types from opentrons_shared_data.pipette.pipette_definition import ( PipetteBoundingBoxOffsetDefinition, @@ -81,6 +85,7 @@ def test_get_virtual_pipette_static_config( }, shaft_ul_per_mm=0.785, available_sensors=AvailableSensorDefinition(sensors=[]), + volume_mode=VolumeModes.default, ) @@ -122,6 +127,7 @@ def test_configure_virtual_pipette_for_volume( }, shaft_ul_per_mm=0.785, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) subject_instance.configure_virtual_pipette_for_volume( "my-pipette", 1, result1.model @@ -159,6 +165,7 @@ def test_configure_virtual_pipette_for_volume( }, shaft_ul_per_mm=0.785, available_sensors=available_sensors, + volume_mode=VolumeModes.lowVolumeDefault, ) @@ -197,6 +204,7 @@ def test_load_virtual_pipette_by_model_string( }, shaft_ul_per_mm=9.621, available_sensors=AvailableSensorDefinition(sensors=[]), + volume_mode=VolumeModes.default, ) @@ -298,6 +306,7 @@ def pipette_dict( "plunger_positions": {"top": 100, "bottom": 20, "blow_out": 10, "drop_tip": 0}, "shaft_ul_per_mm": 5.0, "available_sensors": available_sensors, + "volume_mode": VolumeModes.lowVolumeDefault, } @@ -348,6 +357,7 @@ def test_get_pipette_static_config( plunger_positions={"top": 100, "bottom": 20, "blow_out": 10, "drop_tip": 0}, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.lowVolumeDefault, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 7f6d29afe32..3271d003d85 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -30,7 +30,10 @@ StagingSlotName, MeniscusTrackingTarget, ) -from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette.types import ( + PipetteNameType, + LiquidClasses as VolumeModes, +) from opentrons_shared_data.labware.labware_definition import ( CuboidalFrustum, InnerWellGeometry, @@ -1131,6 +1134,9 @@ def test_get_highest_z_in_slot_with_single_module( addressable_areas=mock_addressable_area_view, ) ).then_return(12345) + decoy.when(mock_module_view.is_column_4_module(module_in_slot.model)).then_return( + False + ) assert ( subject.get_highest_z_in_slot(DeckSlotLocation(slotName=DeckSlotName.SLOT_3)) @@ -1293,6 +1299,9 @@ def test_get_highest_z_in_slot_with_labware_stack_on_module( "magneticModuleV2Slot3" ) ).then_return(Point(11, 22, 33)) + decoy.when(mock_module_view.is_column_4_module(module_on_slot.model)).then_return( + False + ) expected_highest_z = ( 33 @@ -3347,6 +3356,7 @@ def test_get_next_drop_tip_location( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) ) decoy.when(mock_pipette_view.get_mount("pip-123")).then_return(pipette_mount) diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py index 84dcfbf5dbf..b28a4b000c5 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py @@ -5,11 +5,13 @@ tested together, treating LabwareState as a private implementation detail. """ -import pytest from datetime import datetime from typing import Dict, Optional, cast, ContextManager, Any, Union, List from contextlib import nullcontext as does_not_raise +import pytest +from numpy import isclose + from opentrons_shared_data.deck.types import DeckDefinitionV5 from opentrons_shared_data.pipette.types import LabwareUri from opentrons_shared_data.labware import load_definition @@ -45,6 +47,7 @@ LabwareMovementOffsetData, OnAddressableAreaOffsetLocationSequenceComponent, OnModuleOffsetLocationSequenceComponent, + GripSpecs, ) from opentrons.protocol_engine.state._move_types import EdgePathType from opentrons.protocol_engine.state.labware import ( @@ -1855,3 +1858,55 @@ def test_calculates_well_bounding_box( assert subject.get_well_bbox(definition).x == pytest.approx(well_bbox.x) assert subject.get_well_bbox(definition).y == pytest.approx(well_bbox.y) assert subject.get_well_bbox(definition).z == pytest.approx(well_bbox.z) + + +@pytest.mark.parametrize( + "labware_to_check,gripper_specs", + [ + ( + "opentrons_universal_flat_adapter", + GripSpecs(targetY=75, uncertaintyNarrower=5, uncertaintyWider=0), + ), + # well min: 7.81 + # well max: 77.67 + # well bbox: 69.86 + ( + "corning_96_wellplate_360ul_flat", + GripSpecs(targetY=85.47, uncertaintyNarrower=15.61, uncertaintyWider=0), + ), + # well min 7.18 + # well max 78.38 + # well bbox 71.2 + ( + "nest_12_reservoir_15ml", + GripSpecs(targetY=85.48, uncertaintyNarrower=14.28, uncertaintyWider=0), + ), + ( + "opentrons_tough_universal_lid", + GripSpecs(targetY=85.48, uncertaintyNarrower=5, uncertaintyWider=0), + ), + ( + "opentrons_flex_tiprack_lid", + GripSpecs(targetY=78.75, uncertaintyNarrower=5, uncertaintyWider=0), + ), + # well min 7.175 + # well max 78.305 + # well bbox 71.13 + ( + "corning_384_wellplate_112ul_flat", + GripSpecs(targetY=85.47, uncertaintyNarrower=14.34, uncertaintyWider=0), + ), + ], +) +def test_calculates_gripper_positions( + labware_to_check: str, gripper_specs: GripSpecs +) -> None: + """It should calculate gripper positions.""" + definition = labware_definition_type_adapter.validate_python( + load_definition(labware_to_check, 1) + ) + subject = get_labware_view() + specs = subject.get_gripper_width_specs(definition) + assert isclose(specs.targetY, gripper_specs.targetY) + assert isclose(specs.uncertaintyNarrower, gripper_specs.uncertaintyNarrower) + assert isclose(specs.uncertaintyWider, gripper_specs.uncertaintyWider) diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_state.py b/api/tests/opentrons/protocol_engine/state/test_pipette_state.py index 039683f28f0..cac108b512e 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_state.py @@ -9,7 +9,10 @@ import pytest from opentrons_shared_data.pipette import pipette_definition -from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette.types import ( + PipetteNameType, + LiquidClasses as VolumeModes, +) from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons.protocol_engine import actions, commands @@ -84,6 +87,7 @@ def test_handle_pipette_config_action( available_sensors=pipette_definition.AvailableSensorDefinition( sensors=["pressure", "capacitive", "environment"] ), + volume_mode=VolumeModes.default, ), ) subject.handle_action( @@ -199,6 +203,7 @@ def test_active_channels( available_sensors=pipette_definition.AvailableSensorDefinition( sensors=["pressure", "capacitive", "environment"] ), + volume_mode=VolumeModes.default, ), ) subject.handle_action( diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_store_old.py b/api/tests/opentrons/protocol_engine/state/test_pipette_store_old.py index 3bc217f198b..43ba0a2f417 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_store_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_store_old.py @@ -6,7 +6,10 @@ """ import pytest -from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette.types import ( + PipetteNameType, + LiquidClasses as VolumeModes, +) from opentrons_shared_data.pipette import pipette_definition from opentrons.protocol_engine.state import update_types @@ -223,6 +226,7 @@ def test_handles_load_pipette( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) config_update = update_types.PipetteConfigUpdate( pipette_id="pipette-id", @@ -664,6 +668,7 @@ def test_add_pipette_config( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) subject.handle_action( @@ -710,6 +715,7 @@ def test_add_pipette_config( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) assert subject.state.flow_rates_by_id["pipette-id"].default_aspirate == {"a": 1.0} assert subject.state.flow_rates_by_id["pipette-id"].default_dispense == {"b": 2.0} diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_view_old.py b/api/tests/opentrons/protocol_engine/state/test_pipette_view_old.py index 0ae74472c27..1d48dd9a9ca 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_view_old.py @@ -12,7 +12,10 @@ from decoy import Decoy -from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette.types import ( + PipetteNameType, + LiquidClasses as VolumeModes, +) from opentrons_shared_data.pipette import pipette_definition from opentrons_shared_data.pipette.pipette_definition import ( ValidNozzleMaps, @@ -321,6 +324,7 @@ def test_get_pipette_working_volume( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) }, ) @@ -361,6 +365,7 @@ def test_get_pipette_working_volume_raises_if_tip_volume_is_none( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) }, ) @@ -413,6 +418,7 @@ def test_get_pipette_available_volume( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ), "pipette-id-none": StaticPipetteConfig( min_volume=1, @@ -437,6 +443,7 @@ def test_get_pipette_available_volume( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ), }, ) @@ -558,6 +565,7 @@ def test_get_static_config( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) subject = get_pipette_view( @@ -618,6 +626,7 @@ def test_get_nominal_tip_overlap( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) subject = get_pipette_view(static_config_by_id={"pipette-id": config}) @@ -1053,6 +1062,7 @@ def test_get_pipette_bounds_at_location( }, shaft_ul_per_mm=5.0, available_sensors=available_sensors, + volume_mode=VolumeModes.default, ) }, ) diff --git a/app-shell/build/release-notes.md b/app-shell/build/release-notes.md index c977ed986c5..ba4e9cab191 100644 --- a/app-shell/build/release-notes.md +++ b/app-shell/build/release-notes.md @@ -8,6 +8,28 @@ By installing and using Opentrons software, you agree to the Opentrons End-User --- +## Opentrons App Changes in 8.7.0 + +Welcome to the v8.7.0 release of the Opentrons App! This release adds support for Opentrons Tough Universal Lids, improves error recovery on the Opentrons App and Flex touchscreen, and addresses several bugs. + +### New Features + +Use Opentrons Tough Universal Lids on compatible well plates and reservoirs. + +### Improvements + +- Recover from Flex Stacker errors to resume your protocol: + - If you try to store labware in the Stacker, but the shuttle is empty. + - If the Stacker stalls when storing or retrieving labware. + +### Bug Fixes + +- Liquid colors now match across deck views on the Opentrons App and Flex touchscreen. +- The run log now properly shows robot motor control actions for Flex 96-channel pipettes. +- The API raises an error when the Flex Gripper fails to pick up a lid. +- Deck views in error recovery now include labware loaded in a Flex Stacker. +- Quick transfers no longer crash when adding an air gap or blow out after dispensing. + ## Opentrons App Changes in 8.6.0 Welcome to the v8.6.0 release of the Opentrons App! This release adds support for the Flex Stacker Module, as well as other improvements. diff --git a/app/src/App/hooks.ts b/app/src/App/hooks.ts index 4e808436b93..47cfd89eca8 100644 --- a/app/src/App/hooks.ts +++ b/app/src/App/hooks.ts @@ -159,9 +159,10 @@ export function useGetModulesNeedingSetup(): AttachedModule[] { .map(m => m.opentronsModuleSerialNumber) return attachedModules.filter( m => - !modulesInDeckConfig.includes(m.serialNumber) || - (!MODULES_NOT_REQUIRING_CALIBRATION.includes(m.moduleType) && - m.moduleOffset === undefined) + m.compatibleWithRobot && + (!modulesInDeckConfig.includes(m.serialNumber) || + (!MODULES_NOT_REQUIRING_CALIBRATION.includes(m.moduleType) && + m.moduleOffset === undefined)) ) } return [] diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index ee3d49dbf68..c6bcfbf1493 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -6,9 +6,10 @@ "__dev_internal__lpcRedesign": "LPC Redesign", "__dev_internal__protocolStats": "Protocol Stats", "__dev_internal__protocolTimeline": "Protocol Timeline", - "__dev_internal__quickTransferExportPython": "Enable Python Export for Quick Transfer", + "__dev_internal__quickTransferExportJSON": "Enable JSON Export for Quick Transfer", "__dev_internal__reactQueryDevtools": "Enable React Query Devtools", "__dev_internal__reactScan": "Enable React Scan", + "__dev_internal__quickTransferProtocolContentsLog": "Enable QT Protocol Contents Logging", "add_folder_button": "Add labware source folder", "add_ip_button": "Add", "add_ip_error": "Enter an IP Address or Hostname", diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json index ec9ddd19a7f..30708036ca9 100644 --- a/app/src/assets/localization/en/error_recovery.json +++ b/app/src/assets/localization/en/error_recovery.json @@ -32,6 +32,7 @@ "door_open_robot_home": "The robot needs to safely move to its home location before you manually move the labware.", "droplets_or_liquid_cause_failure": "Droplets or liquid in the tips may cause liquid level detection to fail", "empty_shuttle_to_retry_retrieve": "Empty the labware shuttle so that the stacker is able to retry the retrieve command.", + "empty_shuttle_to_retry_store": "Empty the labware shuttle so that the stacker is able to retry the store command.", "empty_stacker_of_all_labware": "Empty the stacker of all labware above the latch.", "empty_stacker_of_labware_above_latch": "Empty stacker of labware above latch", "ensure_lw_is_accurately_placed": "Ensure labware is accurately placed in the slot to prevent further errors.", @@ -72,7 +73,9 @@ "load_labware_into_labware_shuttle": "Load labware onto labware shuttle", "load_labware_into_stacker_and_retry_step": "Load labware into stacker and retry step", "load_labware_shuttle_and_retry_step": "Load labware shuttle and retry step", + "manually_load_labware_into_stacker_and_skip": "Manually load labware into Stacker and skip step", "load_labware_shuttle_onto_track": "Load labware shuttle onto track", + "load_labware_onto_shuttle_and_retry": "Load labware onto shuttle and retry step", "load_stacker_shuttle_to_proceed": "Load the stacker shuttle onto the track to proceed", "load_stacker_with_correct_labware": "Load the stacker with the correct labware to complete the stacker retrieve step", "make_sure_loaded_correct_number_of_labware_stacker": "Make sure you load the correct number of labware into the stacker.", @@ -149,9 +152,10 @@ "stacker_latch_is_jammed": "Stacker latch is jammed", "stacker_latch_jammed_errors_occur_when": "Stacker latch jammed errors occur when labware gets stuck in between the stacker latch. This is usually caused by improperly placed labware or inaccurate labware definitions", "stacker_latch_will_reengage": "The stacker latch will re-engage so that you can reload the stacker. Make sure that any obstructions have been cleared.", - "stacker_shuttle_full": "Stacker shuttle full", + "stacker_shuttle_full": "Stacker shuttle in use", "stacker_shuttle_missing_error_occurs_when": "Stacker shuttle missing errors occur when the shuttle is not placed correctly on the track", "stacker_stall_or_collision_error": "Stacker stalled", + "stacker_shuttle_store_empty": "Shuttle empty", "stacker_what_is_wrong": "What is wrong with the Stacker?", "stall_or_collision_detected_when": "A stall or collision is detected when the robot's motors are blocked", "stall_or_collision_error": "Stall or collision", @@ -176,5 +180,11 @@ "use_dry_unused_tips": "Use dry, unused tips for best results", "view_error_details": "View error details", "view_recovery_options": "View recovery options", - "you_can_still_drop_tips": "You can still drop the attached tips before proceeding to tip selection." + "you_can_still_drop_tips": "You can still drop the attached tips before proceeding to tip selection.", + "stacker_shuttle_store_empty_error_occurs_when": "Shuttle empty errors occur when the robot tries to store labware into a stacker from an empty shuttle", + "load_labware_shuttle_to_proceed": "Load the shuttle with the correct labware to complete the stacker store step", + "stacker_shuttle_occupied_error_occurs_when": "The shuttle has labware when it should be empty", + "remove_labware_from_shuttle_to_proceed": "Remove the labware from the shuttle to complete the stacker retrieve step", + "stacker_hopper_or_shuttle_empty_error_occurs_when": "Stacker errors occur when the stacker is empty when the robot expects the stacker to be filled, or when labware is stuck on the labware latch", + "troubleshoot_issue_complete_retrieve_step": "Troubleshoot the issue to complete the stacker retrieve step" } diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index 3efea4f1ca1..3da08ea33cf 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -70,6 +70,7 @@ "move_to_slot": "Moving to Slot {{slot_name}}", "move_to_well": "Moving to X {{xOffset}} Y {{yOffset}} Z {{zOffset}} relative to {{positionRelative}} of well {{wellName}} of {{labware}} in {{displayLocation}}", "multiple": "multiple", + "ninety_six_channel_cam": "pipette tip attach cam", "notes": "notes", "off_deck": "off deck", "offdeck": "offdeck", diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json index f04bd734b39..ff3d9b6e67a 100644 --- a/app/src/assets/localization/en/quick_transfer.json +++ b/app/src/assets/localization/en/quick_transfer.json @@ -1,9 +1,8 @@ { "a_way_to_move_liquid": "A way to move a single liquid from one labware to another.", - "add_or_remove": "add or remove", "add_or_remove_columns": "add or remove columns", + "add_or_remove": "add or remove", "advanced_settings": "Advanced settings", - "air_gap": "Air gap", "air_gap_after_aspirating": "Air gap after aspirating", "air_gap_after_dispensing": "Air gap after dispensing", "air_gap_capacity_error": "The tip is too full to add an air gap.", @@ -11,19 +10,19 @@ "air_gap_description_dispense": "Draw in air before moving to trash to dispose of tip", "air_gap_value": "{{volume}} µL", "air_gap_volume_µL": "Air gap volume (µL)", + "air_gap": "Air gap", "all": "All labware", "always": "Always", "apply_predefined_settings": "Apply predefined settings for the type of liquid used in your transfer", - "aspirate": "Aspirate", - "aspirate_flow_rate": "Aspirate flow rate", "aspirate_flow_rate_µL": "Aspirate flow rate (µL/s)", + "aspirate_flow_rate": "Aspirate flow rate", "aspirate_setting_disabled": "Aspirate setting disabled for this transfer", "aspirate_settings": "Aspirate Settings", "aspirate_tip_position": "Aspirate tip position", - "aspirate_volume": "Aspirate volume per well", "aspirate_volume_µL": "Aspirate volume per well (µL)", + "aspirate_volume": "Aspirate volume per well", + "aspirate": "Aspirate", "attach_pipette": "Attach pipette", - "blow_out": "Blowout", "blow_out_after_dispensing": "Blowout after dispensing", "blow_out_description": "Blow extra air through the tip", "blow_out_destination_well": "Destination well", @@ -35,54 +34,57 @@ "blow_out_speed": "Blowout speed (µL/second)", "blow_out_trash_bin": "Trash bin", "blow_out_waste_chute": "Waste chute", + "blow_out": "Blowout", "blowout_flow_rate_µL": "Blowout flow rate (µL/second)", "both_mounts": "Left + Right Mount", + "bottom": "bottom", "change_tip": "Change tip", "character_limit_error": "Character limit exceeded", "column": "column", "columns": "columns", "compatibility_error": "The {{pipetteOrLabware}} is incompatible with this liquid class", - "condition": "Condition", "condition_before_aspirating": "Condition before aspirating", "condition_description": "First aspirate and dispense liquid into the source well to ensure a more accurate first multi-dispense", "condition_max_volume": "Max {{max}} µL", "condition_volume": "Conditioning volume (µL)", - "consolidate": "Consolidate", + "condition": "Condition", "consolidate_volume_error": "The selected destination well is too small to consolidate into. Try consolidating from fewer wells.", + "consolidate": "Consolidate", "create_new_to_edit": "Create a new quick transfer to edit", "create_new_transfer": "Create new quick transfer", "create_to_get_started": "Create a new quick transfer to get started.", "create_transfer": "Create transfer", "default": "Default", - "delay": "Delay", "delay_after_aspirating": "Delay after aspirating", "delay_before_dispensing": "Delay before dispensing", "delay_description_aspirate": "Delay after each aspiration and air gap", "delay_description_dispense": "Delay after each dispense", "delay_duration_s": "Delay duration (seconds)", "delay_value": "{{delay}} s", + "delay": "Delay", "delete_this_transfer": "Delete this quick transfer?", "delete_transfer": "Delete quick transfer", "deleted_transfer": "Deleted quick transfer", - "destination": "Destination", "destination_labware": "Destination labware", + "destination": "Destination", "disabled": "Disabled", - "dispense": "Dispense", - "dispense_flow_rate": "Dispense flow rate", "dispense_flow_rate_µL": "Dispense flow rate (µL/s)", + "dispense_flow_rate": "Dispense flow rate", "dispense_setting_disabled": "Dispense setting disabled for this transfer", "dispense_settings": "Dispense Settings", "dispense_tip_position": "Dispense tip position", - "dispense_volume": "Dispense volume per well", "dispense_volume_µL": "Dispense volume per well (µL)", - "disposal_volume": "Disposal volume", + "dispense_volume": "Dispense volume per well", + "dispense": "Dispense", "disposal_volume_flow_rate": "Between {{min}} and {{max}}", "disposal_volume_label": "{{volume}}, {{location}}, {{flowRate}} µL/s", "disposal_volume_µL": "Disposal volume (µL)", + "disposal_volume": "Disposal volume", "distance_bottom_of_well_mm": "Distance from bottom of well (mm)", "distance_from_bottom": "Distance from bottom of well (mm)", - "distribute": "Distribute", + "distance_top_of_well_mm": "Distance from top of well (mm)", "distribute_volume_error": "The selected source well is too small to distribute from. Try distributing to fewer wells.", + "distribute": "Distribute", "do_not_use_liquid_class": "Don't use liquid class settings", "enter_characters": "Enter up to 60 characters", "error_analyzing": "An error occurred while attempting to analyze {{transferName}}.", @@ -90,6 +92,7 @@ "failed_analysis": "failed analysis", "flow_rate_value": "{{flow_rate}} µL/s", "from_bottom": "Between 0 and {{max}} mm", + "from_top": "Between {{min}} and 2 mm", "got_it": "Got it", "grid": "grid", "grids": "grids", @@ -98,17 +101,17 @@ "left_mount": "Left Mount", "liquid_class": "Liquid class", "lose_all_progress": "You will lose all progress on this quick transfer.", - "mix": "Mix", "mix_after_dispensing": "Mix after dispensing", "mix_before_aspirating": "Mix before aspirating", "mix_description": "Aspirate and dispense repeatedly before main aspiration", "mix_repetitions": "Mix repetitions", "mix_value": "{{volume}} µL, {{reps}} times", "mix_volume_µL": "Mix volume (µL)", + "mix": "Mix", "name_your_transfer": "Name your quick transfer", "never": "Never", - "none": "None", "none_to_show": "No quick transfers to show!", + "none": "None", "number_wells_selected_error_learn_more": "Quick transfers with multiple source {{selectionUnits}} can either be one-to-one (select {{wellCount}} destination {{selectionUnits}} for this transfer) or consolidate (select 1 destination {{selectionUnit}}).", "number_wells_selected_error_message": "Select 1 or {{wellCount}} {{selectionUnits}} to make this transfer.", "once": "Once", @@ -120,39 +123,39 @@ "pin_transfer": "Pin quick transfer", "pinned_transfer": "Pinned quick transfer", "pinned_transfers": "Pinned Quick Transfers", - "pipette": "Pipette", "pipette_currently_attached": "Quick transfer options depend on the pipettes currently attached to your robot.", - "pipette_path": "Pipette path", "pipette_path_multi_aspirate": "Multi-aspirate", - "pipette_path_multi_dispense": "Multi-dispense", "pipette_path_multi_dispense_volume_blowout": "Multi-dispense, {{volume}} µL disposal volume, blowout {{blowOutLocation}}", + "pipette_path_multi_dispense": "Multi-dispense", "pipette_path_single": "Single transfers", - "pre_wet_tip": "Pre-wet tip", + "pipette_path": "Pipette path", + "pipette": "Pipette", "pre_wet_tip_before_aspirating": "Pre-wet tip before aspirating", "pre_wet_tip_description": "Pre-wet by aspirating and dispensing the total aspiration volume", - "push_out": "Push out", + "pre_wet_tip": "Pre-wet tip", "push_out_after_dispensing": "Push out after dispensing", "push_out_description": "Helps ensure all liquid leaves the tip", "push_out_value": "{{volume}} µL", "push_out_volume": "Push out volume (µL)", - "quick_transfer": "Quick transfer", + "push_out": "Push out", "quick_transfer_volume": "Quick Transfer {{volume}}µL", + "quick_transfer": "Quick transfer", "reservoir": "Reservoirs", "reset_kind_settings": "Reset {{transferName}} settings?", - "reset_settings": "Reset {{transferName}} settings", "reset_settings_description": "Continuing will undo any changes and restore the {{transferName}} settings back to the default values.", "reset_settings_with_liquid_class_description": "Continuing will undo any changes and restore the {{transferName}} settings to the values associated with the {{liquidClassName}} liquid class.", - "retract": "Retract", + "reset_settings": "Reset {{transferName}} settings", "retract_after_aspirating": "Retract after aspirating", "retract_after_dispensing": "Retract after dispensing", - "retract_value": "{{speed}} mm/s, {{delayDuration}} s, {{position}} mm from bottom", + "retract_value": "{{speed}} mm/s, {{delayDuration}} s, {{position}} mm from {{positionReference}}", + "retract": "Retract", "right_mount": "Right Mount", "run_now": "Run now", "run_quick_transfer_now": "Do you want to run your quick transfer now?", "run_transfer": "Run quick transfer", - "save": "Save", "save_for_later": "Save for later", "save_to_run_later": "Save your quick transfer to run it in the future.", + "save": "Save", "select_attached_pipette": "Select attached pipette", "select_blow_out_location": "Select blowout location", "select_by": "select by", @@ -169,27 +172,27 @@ "set_dispense_volume": "Set dispense volume", "set_transfer_volume": "Set transfer volume", "single": "Single transfer", - "source": "Source", - "source_labware": "Source labware", "source_labware_c2": "Source labware in C2", + "source_labware": "Source labware", + "source": "Source", "speed": "Speed (mm/second)", "starting_well": "starting well", "storage_limit_reached": "Storage limit reached", - "submerge": "Submerge", "submerge_aspirate_description": "Lower the tip into the liquid before aspirating", "submerge_before_aspirating": "Submerge before aspirating", "submerge_before_dispensing": " Submerge before dispensing", "submerge_dispense_description": "Lower the tip into the liquid before dispensing", - "submerge_value": "{{speed}} mm/s, {{delayDuration}} s, {{position}} mm", + "submerge_value": "{{speed}} mm/s, {{delayDuration}} s, {{position}} mm from {{positionReference}}", + "submerge": "Submerge", "tip_change_frequency": "Tip change frequency", "tip_drop_location": "Tip drop location", "tip_management": "Tip management", - "tip_position": "Tip position", "tip_position_value": "{{position}} mm from the bottom", + "tip_position": "Tip position", "tip_rack": "Tip rack", "too_many_pins_body": "Remove a quick transfer in order to add more transfers to your pinned list.", "too_many_pins_header": "You've hit your max!", - "touch_tip": "Touch tip", + "top": "top", "touch_tip_after_aspirating": "Touch tip after aspirating", "touch_tip_after_dispensing": "Touch tip after dispensing", "touch_tip_description_aspirating": "Touch tip to each side of the well after aspirating", @@ -197,27 +200,28 @@ "touch_tip_from_top": "Between {{min}} and {{max}}", "touch_tip_position_mm": "Touch tip position from top of well (mm)", "touch_tip_value": "{{speed}}mm/s, {{position}} mm from bottom", + "touch_tip": "Touch tip", "transfer_analysis_failed": "quick transfer analysis failed", "transfer_name": "Transfer Name", "transfer_pipette_path_incompatible": "The selected pipette path is incompatible with this liquid class", "transfer_volumes_incompatible": "Transfer volumes of 10 µL or less are incompatible with liquid classes", - "trashBin": "Trash bin", "trashBin_location": "Trash bin in {{slotName}}", + "trashBin": "Trash bin", "tubeRack": "Tube racks", "unpin_transfer": "Unpin quick transfer", "unpinned_transfer": "Unpinned quick transfer", "use_deck_slots": "Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.Make sure that your deck configuration is up to date to avoid collisions.", "value_out_of_range": "Value must be between {{min}} to {{max}}", - "volume": "{{volume}} µL", - "volume_per_well": "Volume per well", "volume_per_well_µL": "Volume per well (µL)", - "wasteChute": "Waste chute", + "volume_per_well": "Volume per well", + "volume": "{{volume}} µL", "wasteChute_location": "Waste chute in {{slotName}}", + "wasteChute": "Waste chute", "welcome_to_quick_transfer": "Welcome to quick transfer!", - "well": "well", - "wellPlate": "Well plates", "well_ratio": "Quick transfers with multiple source wells can either be one-to-one (select {{wells}} for this transfer) or consolidate (select 1 destination well).", "well_selection": "Well selection", + "well": "well", + "wellPlate": "Well plates", "wells": "wells", "will_be_deleted": "{{transferName}} will be permanently deleted.", "withdraw_tip_from_liquid_aspirate": "Withdraw the tip from the liquid after aspirating", diff --git a/app/src/molecules/InProgressModal/InProgressModal.tsx b/app/src/molecules/InProgressModal/InProgressModal.tsx index e030746810f..3c1e01833ab 100644 --- a/app/src/molecules/InProgressModal/InProgressModal.tsx +++ b/app/src/molecules/InProgressModal/InProgressModal.tsx @@ -27,6 +27,7 @@ const StyledDescription = styled(LegacyStyledText)` ${TYPOGRAPHY.h1Default} margin-top: ${SPACING.spacing24}; margin-bottom: ${SPACING.spacing8}; + text-align: ${TYPOGRAPHY.textAlignCenter}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { font-weight: ${TYPOGRAPHY.fontWeightBold}; @@ -35,7 +36,6 @@ const StyledDescription = styled(LegacyStyledText)` margin-bottom: ${SPACING.spacing4}; margin-left: 4.5rem; margin-right: 4.5rem; - text-align: ${TYPOGRAPHY.textAlignCenter}; line-height: ${TYPOGRAPHY.lineHeight42}; } ` diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx index e0b14083f78..f81a7184239 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx @@ -89,7 +89,8 @@ export function SetupLabwareMap({ ? getWellFillFromLabwareId( topLabwareInfo.labwareId, protocolAnalysis.liquids, - labwareByLiquidId + labwareByLiquidId, + protocolAnalysis.commands ) : undefined const moduleType = getModuleType(module.moduleModel) @@ -161,7 +162,8 @@ export function SetupLabwareMap({ const wellFill = getWellFillFromLabwareId( topLabwareInfo.labwareId, protocolAnalysis.liquids, - labwareByLiquidId + labwareByLiquidId, + protocolAnalysis.commands ) return { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/SlotDetailModal.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/SlotDetailModal.tsx index 8c4b4a5e60c..bbf577ea538 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/SlotDetailModal.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/SlotDetailModal.tsx @@ -82,8 +82,9 @@ export const SlotDetailModal = ( const [selectedLabware, setSelectedLabware] = useState(labwareInStack[0]) const wellFill = getWellFillFromLabwareId( selectedLabware.labwareId, - protocolData?.liquids ?? [], - labwareByLiquidId + protocolData.liquids, + labwareByLiquidId, + protocolData.commands ) const labwareDefinition = definitionsByURI[selectedLabware.definitionUri] diff --git a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx index f7a80f4e8b0..1f8b76c2275 100644 --- a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx @@ -28,10 +28,16 @@ import { StackerSelectErrorFlow, StackerShuttleEmptyRetry, StackerShuttleEmptySkip, + StackerShuttleEmptyStoreRetry, + StackerShuttleEmptyStoreSkip, StackerShuttleMissing, StackerStalledRetry, StackerStalledSkip, + StackerStalledStoreRetry, + StackerStalledStoreSkip, } from './RecoveryOptions' +import { ShuttleFullRetry } from './RecoveryOptions/ShuttleFullRetry' +import { ShuttleFullSkip } from './RecoveryOptions/ShuttleFullSkip' import { ErrorDetailsModal, RecoveryDoorOpenSpecial, @@ -249,6 +255,12 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { const buildStackerHopperEmptySkip = (): JSX.Element => { return } + const buildStackerShuttleEmptyStoreRetry = (): JSX.Element => { + return + } + const buildStackerShuttleEmptyStoreSkip = (): JSX.Element => { + return + } const buildStackerShuttleEmptyRetry = (): JSX.Element => { return } @@ -258,6 +270,12 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { const buildStackerShuttleMissing = (): JSX.Element => { return } + const buildShuttleFullRetry = (): JSX.Element => { + return + } + const buildShuttleFullSkip = (): JSX.Element => { + return + } const buildStackerStalledRetry = (): JSX.Element => { return } @@ -267,6 +285,12 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { const buildStackerSelectErrorFlow = (): JSX.Element => { return } + const buildStackerStalledStoreRetry = (): JSX.Element => { + return + } + const buildStackerStalledStoreSkip = (): JSX.Element => { + return + } switch (props.recoveryMap.route) { case RECOVERY_MAP.OPTION_SELECTION.ROUTE: @@ -303,16 +327,28 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { return buildStackerHopperEmptyRetry() case RECOVERY_MAP.STACKER_HOPPER_EMPTY_SKIP.ROUTE: return buildStackerHopperEmptySkip() + case RECOVERY_MAP.SHUTTLE_FULL_RETRY.ROUTE: + return buildShuttleFullRetry() + case RECOVERY_MAP.SHUTTLE_FULL_SKIP.ROUTE: + return buildShuttleFullSkip() case RECOVERY_MAP.STACKER_STALLED_RETRY.ROUTE: return buildStackerStalledRetry() case RECOVERY_MAP.STACKER_STALLED_SKIP.ROUTE: return buildStackerStalledSkip() + case RECOVERY_MAP.STACKER_STALLED_STORE_RETRY.ROUTE: + return buildStackerStalledStoreRetry() + case RECOVERY_MAP.STACKER_STALLED_STORE_SKIP.ROUTE: + return buildStackerStalledStoreSkip() case RECOVERY_MAP.STACKER_SHUTTLE_MISSING_RETRY.ROUTE: return buildStackerShuttleMissing() case RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.ROUTE: return buildStackerShuttleEmptyRetry() case RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.ROUTE: return buildStackerShuttleEmptySkip() + case RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.ROUTE: + return buildStackerShuttleEmptyStoreRetry() + case RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.ROUTE: + return buildStackerShuttleEmptyStoreSkip() case RECOVERY_MAP.ROBOT_DOOR_OPEN_SPECIAL.ROUTE: return buildRecoveryDoorOpenSpecial() case RECOVERY_MAP.ROBOT_IN_MOTION.ROUTE: diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx index 4f3a4377efd..56ebf6c0b3c 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx @@ -49,12 +49,16 @@ export function SelectRecoveryOptionHome({ getRecoveryOptionCopy, analytics, isOnDevice, + failedCommand, }: RecoveryContentProps): JSX.Element | null { const { t } = useTranslation('error_recovery') const { proceedToRouteAndStep } = routeUpdateActions const { determineTipStatus } = tipStatusUtils const { setSelectedRecoveryOption } = currentRecoveryOptionUtils - const validRecoveryOptions = getRecoveryOptions(errorKind) + const validRecoveryOptions = getRecoveryOptions( + errorKind, + failedCommand?.byRunRecord.commandType + ) const [selectedRoute, setSelectedRoute] = useState( head(validRecoveryOptions) as RecoveryRoute ) @@ -176,7 +180,10 @@ export function useCurrentTipStatus( }, []) } -export function getRecoveryOptions(errorKind: ErrorKind): RecoveryRoute[] { +export function getRecoveryOptions( + errorKind: ErrorKind, + commandType?: string +): RecoveryRoute[] { switch (errorKind) { case ERROR_KINDS.NO_LIQUID_DETECTED: return NO_LIQUID_DETECTED_OPTIONS @@ -197,13 +204,17 @@ export function getRecoveryOptions(errorKind: ErrorKind): RecoveryRoute[] { case ERROR_KINDS.STALL_OR_COLLISION: return STALL_OR_COLLISION_OPTIONS case ERROR_KINDS.STACKER_STALLED: - return STACKER_STALLED_OPTIONS + return commandType === 'flexStacker/store' + ? STACKER_STALLED_STORE_OPTIONS + : STACKER_STALLED_RETRIEVE_OPTIONS case ERROR_KINDS.STACKER_HOPPER_EMPTY: return STACKER_HOPPER_EMPTY_OPTIONS case ERROR_KINDS.STACKER_SHUTTLE_MISSING: return STACKER_SHUTTLE_MISSING_OPTIONS case ERROR_KINDS.STACKER_SHUTTLE_EMPTY: return STACKER_SHUTTLE_EMPTY_OPTIONS + case ERROR_KINDS.STACKER_SHUTTLE_STORE_EMPTY: + return STACKER_SHUTTLE_EMPTY_STORE_OPTIONS case ERROR_KINDS.STACKER_SHUTTLE_OCCUPIED: return STACKER_SHUTTLE_OCCUPIED_OPTIONS case ERROR_KINDS.STACKER_HOPPER_OR_SHUTTLE_EMPTY: @@ -212,7 +223,8 @@ export function getRecoveryOptions(errorKind: ErrorKind): RecoveryRoute[] { } export const STACKER_SHUTTLE_OCCUPIED_OPTIONS: RecoveryRoute[] = [ - RECOVERY_MAP.STACKER_STALLED_RETRY.ROUTE, + RECOVERY_MAP.SHUTTLE_FULL_RETRY.ROUTE, + RECOVERY_MAP.SHUTTLE_FULL_SKIP.ROUTE, RECOVERY_MAP.CANCEL_RUN.ROUTE, ] @@ -222,6 +234,12 @@ export const STACKER_SHUTTLE_EMPTY_OPTIONS: RecoveryRoute[] = [ RECOVERY_MAP.CANCEL_RUN.ROUTE, ] +export const STACKER_SHUTTLE_EMPTY_STORE_OPTIONS: RecoveryRoute[] = [ + RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.ROUTE, + RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.ROUTE, + RECOVERY_MAP.CANCEL_RUN.ROUTE, +] + export const STACKER_SHUTTLE_MISSING_OPTIONS: RecoveryRoute[] = [ RECOVERY_MAP.STACKER_SHUTTLE_MISSING_RETRY.ROUTE, RECOVERY_MAP.CANCEL_RUN.ROUTE, @@ -233,12 +251,18 @@ export const STACKER_HOPPER_EMPTY_OPTIONS: RecoveryRoute[] = [ RECOVERY_MAP.CANCEL_RUN.ROUTE, ] -export const STACKER_STALLED_OPTIONS: RecoveryRoute[] = [ +export const STACKER_STALLED_RETRIEVE_OPTIONS: RecoveryRoute[] = [ RECOVERY_MAP.STACKER_STALLED_RETRY.ROUTE, RECOVERY_MAP.STACKER_STALLED_SKIP.ROUTE, RECOVERY_MAP.CANCEL_RUN.ROUTE, ] +export const STACKER_STALLED_STORE_OPTIONS: RecoveryRoute[] = [ + RECOVERY_MAP.STACKER_STALLED_STORE_RETRY.ROUTE, + RECOVERY_MAP.STACKER_STALLED_STORE_SKIP.ROUTE, + RECOVERY_MAP.CANCEL_RUN.ROUTE, +] + export const STALL_OR_COLLISION_OPTIONS: RecoveryRoute[] = [ RECOVERY_MAP.HOME_AND_RETRY.ROUTE, RECOVERY_MAP.CANCEL_RUN.ROUTE, diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ShuttleFullRetry.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ShuttleFullRetry.tsx new file mode 100644 index 00000000000..36ea11cebc2 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ShuttleFullRetry.tsx @@ -0,0 +1,35 @@ +import { RECOVERY_MAP } from '../constants' +import { + RetryStepInfo, + StackerEmptyHopper, + StackerEnsureShuttleEmpty, + StackerHomeShuttle, + StackerHopperLwInfo, +} from '../shared' +import { SelectRecoveryOption } from './SelectRecoveryOption' + +import type { RecoveryContentProps } from '../types' + +export function ShuttleFullRetry(props: RecoveryContentProps): JSX.Element { + const { recoveryMap } = props + const { step, route } = recoveryMap + const { SHUTTLE_FULL_RETRY } = RECOVERY_MAP + + switch (step) { + case SHUTTLE_FULL_RETRY.STEPS.EMPTY_STACKER: + return + case SHUTTLE_FULL_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS: + return + case SHUTTLE_FULL_RETRY.STEPS.CHECK_HOPPER: + return + case SHUTTLE_FULL_RETRY.STEPS.RETRY: + return + case SHUTTLE_FULL_RETRY.STEPS.ENSURE_SHUTTLE_EMPTY: + return + default: + console.warn( + `ShuttleFullRetry: ${step} in ${route} not explicitly handled. Rerouting.` + ) + return + } +} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ShuttleFullSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ShuttleFullSkip.tsx new file mode 100644 index 00000000000..c33c316e18b --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ShuttleFullSkip.tsx @@ -0,0 +1,35 @@ +import { RECOVERY_MAP } from '../constants' +import { + SkipStepInfo, + StackerEmptyHopper, + StackerHomeShuttle, + StackerHopperLwInfo, + StackerShuttleLwInfo, +} from '../shared' +import { SelectRecoveryOption } from './SelectRecoveryOption' + +import type { RecoveryContentProps } from '../types' + +export function ShuttleFullSkip(props: RecoveryContentProps): JSX.Element { + const { recoveryMap } = props + const { step, route } = recoveryMap + const { SHUTTLE_FULL_SKIP } = RECOVERY_MAP + + switch (step) { + case SHUTTLE_FULL_SKIP.STEPS.EMPTY_STACKER: + return + case SHUTTLE_FULL_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS: + return + case SHUTTLE_FULL_SKIP.STEPS.PLACE_LABWARE_ON_SHUTTLE: + return + case SHUTTLE_FULL_SKIP.STEPS.CHECK_HOPPER: + return + case SHUTTLE_FULL_SKIP.STEPS.SKIP: + return + default: + console.warn( + `StackerStalledSkip: ${step} in ${route} not explicitly handled. Rerouting.` + ) + return + } +} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptyRetry.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptyRetry.tsx index d48725f66f5..dff6293f39b 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptyRetry.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptyRetry.tsx @@ -5,7 +5,6 @@ import { RetryStepInfo, StackerEmptyHopper, StackerEnsureShuttleEmpty, - StackerHomeShuttle, StackerHopperLwInfo, StackerReengageLatch, } from '../shared' @@ -23,8 +22,6 @@ export function StackerShuttleEmptyRetry( switch (step) { case STACKER_SHUTTLE_EMPTY_RETRY.STEPS.EMPTY_STACKER: return - case STACKER_SHUTTLE_EMPTY_RETRY.STEPS.PREPARE_TRACK_FOR_HOMING: - return case STACKER_SHUTTLE_EMPTY_RETRY.STEPS.CONFIRM_LABWARE_IN_LATCH: return case STACKER_SHUTTLE_EMPTY_RETRY.STEPS.RELEASE_FROM_LATCH: diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptyRetryStore.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptyRetryStore.tsx new file mode 100644 index 00000000000..e25d149568d --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptyRetryStore.tsx @@ -0,0 +1,34 @@ +import { RECOVERY_MAP } from '../constants' +import { + RetryStepInfo, + StackerHomeShuttle, + StackerHopperLwInfo, + StackerShuttleLwInfo, +} from '../shared' +import { SelectRecoveryOption } from './SelectRecoveryOption' + +import type { RecoveryContentProps } from '../types' + +export function StackerShuttleEmptyStoreRetry( + props: RecoveryContentProps +): JSX.Element { + const { recoveryMap } = props + const { step, route } = recoveryMap + const { STACKER_SHUTTLE_EMPTY_STORE_RETRY } = RECOVERY_MAP + + switch (step) { + case STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS: + return + case STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.CHECK_HOPPER: + return + case STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.PLACE_LABWARE_ON_SHUTTLE: + return + case STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.RETRY: + return + default: + console.warn( + `StackerShuttleEmptyStoreRetry: ${step} in ${route} not explicitly handled. Rerouting.` + ) + return + } +} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptySkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptySkip.tsx index 8157980c75a..c3f1cbfde71 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptySkip.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptySkip.tsx @@ -4,7 +4,6 @@ import { ReleaseLabware, SkipStepInfo, StackerEmptyHopper, - StackerHomeShuttle, StackerHopperLwInfo, StackerReengageLatch, StackerShuttleLwInfo, @@ -23,8 +22,6 @@ export function StackerShuttleEmptySkip( switch (step) { case STACKER_SHUTTLE_EMPTY_SKIP.STEPS.EMPTY_STACKER: return - case STACKER_SHUTTLE_EMPTY_SKIP.STEPS.PREPARE_TRACK_FOR_HOMING: - return case STACKER_SHUTTLE_EMPTY_SKIP.STEPS.CONFIRM_LABWARE_IN_LATCH: return case STACKER_SHUTTLE_EMPTY_SKIP.STEPS.RELEASE_FROM_LATCH: diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptyStoreSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptyStoreSkip.tsx new file mode 100644 index 00000000000..cbddf15e0a1 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerShuttleEmptyStoreSkip.tsx @@ -0,0 +1,34 @@ +import { RECOVERY_MAP } from '../constants' +import { + SkipStepInfo, + StackerEnsureShuttleEmpty, + StackerHomeShuttle, + StackerHopperLwInfo, +} from '../shared' +import { SelectRecoveryOption } from './SelectRecoveryOption' + +import type { RecoveryContentProps } from '../types' + +export function StackerShuttleEmptyStoreSkip( + props: RecoveryContentProps +): JSX.Element { + const { recoveryMap } = props + const { step, route } = recoveryMap + const { STACKER_SHUTTLE_EMPTY_STORE_SKIP } = RECOVERY_MAP + + switch (step) { + case STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS: + return + case STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.CHECK_HOPPER: + return + case STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.ENSURE_SHUTTLE_EMPTY: + return + case STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.SKIP: + return + default: + console.warn( + `StackerShuttleEmptyStoreSkip: ${step} in ${route} not explicitly handled. Rerouting.` + ) + return + } +} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledRetry.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledRetry.tsx index f31ecc9729e..2cb5da0c364 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledRetry.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledRetry.tsx @@ -18,7 +18,6 @@ export function StackerStalledRetry(props: RecoveryContentProps): JSX.Element { switch (step) { case STACKER_STALLED_RETRY.STEPS.EMPTY_STACKER: return - case STACKER_STALLED_RETRY.STEPS.PREPARE_TRACK_FOR_HOMING: case STACKER_STALLED_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS: return case STACKER_STALLED_RETRY.STEPS.CHECK_HOPPER: diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledSkip.tsx index a062eb50783..a0565874eb8 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledSkip.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledSkip.tsx @@ -18,7 +18,6 @@ export function StackerStalledSkip(props: RecoveryContentProps): JSX.Element { switch (step) { case STACKER_STALLED_SKIP.STEPS.EMPTY_STACKER: return - case STACKER_STALLED_SKIP.STEPS.PREPARE_TRACK_FOR_HOMING: case STACKER_STALLED_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS: return case STACKER_STALLED_SKIP.STEPS.PLACE_LABWARE_ON_SHUTTLE: diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledStoreRetry.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledStoreRetry.tsx new file mode 100644 index 00000000000..48c5ccf3e29 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledStoreRetry.tsx @@ -0,0 +1,37 @@ +import { RECOVERY_MAP } from '../constants' +import { + RetryStepInfo, + StackerEmptyHopper, + StackerHomeShuttle, + StackerHopperLwInfo, + StackerShuttleLwInfo, +} from '../shared' +import { SelectRecoveryOption } from './SelectRecoveryOption' + +import type { RecoveryContentProps } from '../types' + +export function StackerStalledStoreRetry( + props: RecoveryContentProps +): JSX.Element { + const { recoveryMap } = props + const { step, route } = recoveryMap + const { STACKER_STALLED_STORE_RETRY } = RECOVERY_MAP + + switch (step) { + case STACKER_STALLED_STORE_RETRY.STEPS.EMPTY_STACKER: + return + case STACKER_STALLED_STORE_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS: + return + case STACKER_STALLED_STORE_RETRY.STEPS.CHECK_HOPPER: + return + case STACKER_STALLED_STORE_RETRY.STEPS.RETRY: + return + case STACKER_STALLED_STORE_RETRY.STEPS.PLACE_LABWARE_ON_SHUTTLE: + return + default: + console.warn( + `StackerStalledStoreRetry: ${step} in ${route} not explicitly handled. Rerouting.` + ) + return + } +} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledStoreSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledStoreSkip.tsx new file mode 100644 index 00000000000..e18e24a4694 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/StackerStalledStoreSkip.tsx @@ -0,0 +1,37 @@ +import { RECOVERY_MAP } from '../constants' +import { + SkipStepInfo, + StackerEmptyHopper, + StackerEnsureShuttleEmpty, + StackerHomeShuttle, + StackerHopperLwInfo, +} from '../shared' +import { SelectRecoveryOption } from './SelectRecoveryOption' + +import type { RecoveryContentProps } from '../types' + +export function StackerStalledStoreSkip( + props: RecoveryContentProps +): JSX.Element { + const { recoveryMap } = props + const { step, route } = recoveryMap + const { STACKER_STALLED_STORE_SKIP } = RECOVERY_MAP + + switch (step) { + case STACKER_STALLED_STORE_SKIP.STEPS.EMPTY_STACKER: + return + case STACKER_STALLED_STORE_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS: + return + case STACKER_STALLED_STORE_SKIP.STEPS.ENSURE_SHUTTLE_EMPTY: + return + case STACKER_STALLED_STORE_SKIP.STEPS.CHECK_HOPPER: + return + case STACKER_STALLED_STORE_SKIP.STEPS.SKIP: + return + default: + console.warn( + `StackerStalledStoreSkip: ${step} in ${route} not explicitly handled. Rerouting.` + ) + return + } +} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx index 659b41db07d..dbd0f8f77c1 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx @@ -19,6 +19,8 @@ import { RecoveryOptions, SelectRecoveryOption, STACKER_SHUTTLE_EMPTY_OPTIONS, + STACKER_STALLED_RETRIEVE_OPTIONS, + STACKER_STALLED_STORE_OPTIONS, STALL_OR_COLLISION_OPTIONS, TIP_DROP_FAILED_OPTIONS, TIP_NOT_DETECTED_OPTIONS, @@ -558,4 +560,24 @@ describe('getRecoveryOptions', () => { ) expect(labwareMissingInShuttleOptions).toBe(STACKER_SHUTTLE_EMPTY_OPTIONS) }) + + it(`returns valid options when the errorKind is ${ + ERROR_KINDS.STACKER_STALLED + } and the commandType is ${'flexStacker/store'}`, () => { + const stackerStalledOptions = getRecoveryOptions( + ERROR_KINDS.STACKER_STALLED, + 'flexStacker/store' + ) + expect(stackerStalledOptions).toBe(STACKER_STALLED_STORE_OPTIONS) + }) + + it(`returns valid options when the errorKind is ${ + ERROR_KINDS.STACKER_STALLED + } and the commandType is ${'flexStacker/retrieve'}`, () => { + const stackerStalledOptions = getRecoveryOptions( + ERROR_KINDS.STACKER_STALLED, + 'flexStacker/retrieve' + ) + expect(stackerStalledOptions).toBe(STACKER_STALLED_RETRIEVE_OPTIONS) + }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ShuttleFullRetry.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ShuttleFullRetry.test.tsx new file mode 100644 index 00000000000..9f59e930869 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ShuttleFullRetry.test.tsx @@ -0,0 +1,86 @@ +import { screen } from '@testing-library/react' +import { beforeEach, describe, it, vi } from 'vitest' + +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' + +import { mockRecoveryContentProps } from '../../__fixtures__' +import { RECOVERY_MAP } from '../../constants' +import { + RetryStepInfo, + StackerEnsureShuttleEmpty, + StackerHomeShuttle, + StackerHopperLwInfo, +} from '../../shared' +import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { ShuttleFullRetry } from '../ShuttleFullRetry' + +import type { ComponentProps } from 'react' + +vi.mock('../SelectRecoveryOption') +vi.mock('../../shared/') + +const render = (props: ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('StackerStalledRetry', () => { + let props: ComponentProps + beforeEach(() => { + props = { + ...mockRecoveryContentProps, + currentRecoveryOptionUtils: { + ...mockRecoveryContentProps.currentRecoveryOptionUtils, + selectedRecoveryOption: RECOVERY_MAP.SHUTTLE_FULL_RETRY.ROUTE, + }, + } + vi.mocked(SelectRecoveryOption).mockReturnValue( +
MOCK_SELECT_RECOVERY_OPTION
+ ) + vi.mocked(StackerHomeShuttle).mockReturnValue( +
MOCK_STACKER_HOME_SHUTTLE
+ ) + vi.mocked(StackerHopperLwInfo).mockReturnValue( +
MOCK_STACKER_HOPPER_LW_INFO
+ ) + vi.mocked(RetryStepInfo).mockReturnValue(
MOCK_RETRY_STEP_INFO
) + vi.mocked(StackerEnsureShuttleEmpty).mockReturnValue( +
MOCK_TWO_COLUMN_AND_IMAGE
+ ) + }) + + it(`renders StackerHomeShuttle when step is ${RECOVERY_MAP.SHUTTLE_FULL_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS}`, () => { + props.recoveryMap.step = + RECOVERY_MAP.SHUTTLE_FULL_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS + render(props) + screen.getByText('MOCK_STACKER_HOME_SHUTTLE') + }) + + it(`renders StackerHopperLwInfo when step is ${RECOVERY_MAP.SHUTTLE_FULL_RETRY.STEPS.CHECK_HOPPER}`, () => { + props.recoveryMap.step = RECOVERY_MAP.SHUTTLE_FULL_RETRY.STEPS.CHECK_HOPPER + render(props) + screen.getByText('MOCK_STACKER_HOPPER_LW_INFO') + }) + + it(`renders twoColumnAndImage when step is ${RECOVERY_MAP.SHUTTLE_FULL_RETRY.STEPS.ENSURE_SHUTTLE_EMPTY}`, () => { + props.recoveryMap.step = + RECOVERY_MAP.SHUTTLE_FULL_RETRY.STEPS.ENSURE_SHUTTLE_EMPTY + render(props) + screen.getByText('MOCK_TWO_COLUMN_AND_IMAGE') + }) + + it(`renders RetryStepInfo when step is ${RECOVERY_MAP.SHUTTLE_FULL_RETRY.STEPS.RETRY}`, () => { + props.recoveryMap.step = RECOVERY_MAP.SHUTTLE_FULL_RETRY.STEPS.RETRY + render(props) + screen.getByText('MOCK_RETRY_STEP_INFO') + }) + + it(`renders SelectRecoveryOption for unknown step`, () => { + props.recoveryMap.step = + RECOVERY_MAP.SKIP_STEP_WITH_NEW_TIPS.STEPS.REPLACE_TIPS + render(props) + screen.getByText('MOCK_SELECT_RECOVERY_OPTION') + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ShuttleFullSkip.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ShuttleFullSkip.test.tsx new file mode 100644 index 00000000000..07bad81328a --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ShuttleFullSkip.test.tsx @@ -0,0 +1,98 @@ +import { screen } from '@testing-library/react' +import { beforeEach, describe, it, vi } from 'vitest' + +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' + +import { mockRecoveryContentProps } from '../../__fixtures__' +import { RECOVERY_MAP } from '../../constants' +import { + HoldingLabware, + ReleaseLabware, + SkipStepInfo, + StackerEmptyHopper, + StackerHomeShuttle, + StackerHopperLwInfo, + StackerReengageLatch, + StackerShuttleLwInfo, +} from '../../shared' +import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { ShuttleFullSkip } from '../ShuttleFullSkip' + +import type { ComponentProps } from 'react' + +vi.mock('../SelectRecoveryOption') +vi.mock('../../shared/') + +const render = (props: ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('ShuttleFullSkip', () => { + let props: ComponentProps + beforeEach(() => { + props = { + ...mockRecoveryContentProps, + currentRecoveryOptionUtils: { + ...mockRecoveryContentProps.currentRecoveryOptionUtils, + selectedRecoveryOption: RECOVERY_MAP.STACKER_STALLED_SKIP.ROUTE, + }, + } + vi.mocked(SelectRecoveryOption).mockReturnValue( +
MOCK_SELECT_RECOVERY_OPTION
+ ) + vi.mocked(StackerEmptyHopper).mockReturnValue( +
MOCK_STACKER_EMPTY_HOPPER
+ ) + vi.mocked(StackerHomeShuttle).mockReturnValue( +
MOCK_STACKER_HOME_SHUTTLE
+ ) + vi.mocked(HoldingLabware).mockReturnValue(
MOCK_HOLDING_LABWARE
) + vi.mocked(ReleaseLabware).mockReturnValue(
MOCK_RELEASE_LABWARE
) + vi.mocked(StackerReengageLatch).mockReturnValue( +
MOCK_STACKER_REENGAGE_LATCH
+ ) + vi.mocked(StackerShuttleLwInfo).mockReturnValue( +
MOCK_STACKER_SHUTTLE_LW_INFO
+ ) + vi.mocked(StackerHopperLwInfo).mockReturnValue( +
MOCK_STACKER_HOPPER_LW_INFO
+ ) + vi.mocked(SkipStepInfo).mockReturnValue(
MOCK_SKIP_STEP_INFO
) + }) + + it(`renders StackerHomeShuttle when step is ${RECOVERY_MAP.STACKER_STALLED_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS}`, () => { + props.recoveryMap.step = + RECOVERY_MAP.SHUTTLE_FULL_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS + render(props) + screen.getByText('MOCK_STACKER_HOME_SHUTTLE') + }) + + it(`renders StackerReengageLatch when step is ${RECOVERY_MAP.STACKER_STALLED_SKIP.STEPS.PLACE_LABWARE_ON_SHUTTLE}`, () => { + props.recoveryMap.step = + RECOVERY_MAP.SHUTTLE_FULL_SKIP.STEPS.PLACE_LABWARE_ON_SHUTTLE + render(props) + screen.getByText('MOCK_STACKER_SHUTTLE_LW_INFO') + }) + + it(`renders StackerHopperLwInfo when step is ${RECOVERY_MAP.STACKER_STALLED_SKIP.STEPS.CHECK_HOPPER}`, () => { + props.recoveryMap.step = RECOVERY_MAP.SHUTTLE_FULL_SKIP.STEPS.CHECK_HOPPER + render(props) + screen.getByText('MOCK_STACKER_HOPPER_LW_INFO') + }) + + it(`renders SkipStepInfo when step is ${RECOVERY_MAP.STACKER_STALLED_SKIP.STEPS.SKIP}`, () => { + props.recoveryMap.step = RECOVERY_MAP.STACKER_STALLED_SKIP.STEPS.SKIP + render(props) + screen.getByText('MOCK_SKIP_STEP_INFO') + }) + + it(`renders SelectRecoveryOption for unknown step`, () => { + props.recoveryMap.step = + RECOVERY_MAP.SKIP_STEP_WITH_NEW_TIPS.STEPS.REPLACE_TIPS + render(props) + screen.getByText('MOCK_SELECT_RECOVERY_OPTION') + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptyRetry.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptyRetry.test.tsx index 5cf91c256b7..b2b97038e53 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptyRetry.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptyRetry.test.tsx @@ -74,13 +74,6 @@ describe('StackerShuttleEmptyRetry', () => { screen.getByText('MOCK_STACKER_EMPTY_HOPPER') }) - it(`renders StackerHomeShuttle when step is ${RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.STEPS.PREPARE_TRACK_FOR_HOMING}`, () => { - props.recoveryMap.step = - RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.STEPS.PREPARE_TRACK_FOR_HOMING - render(props) - screen.getByText('MOCK_STACKER_HOME_SHUTTLE') - }) - it(`renders HoldingLabware when step is ${RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.STEPS.CONFIRM_LABWARE_IN_LATCH}`, () => { props.recoveryMap.step = RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.STEPS.CONFIRM_LABWARE_IN_LATCH diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptySkip.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptySkip.test.tsx index 63b81d0b03d..698618cdccc 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptySkip.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptySkip.test.tsx @@ -74,13 +74,6 @@ describe('StackerShuttleEmptySkip', () => { screen.getByText('MOCK_STACKER_EMPTY_HOPPER') }) - it(`renders StackerHomeShuttle when step is ${RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.STEPS.PREPARE_TRACK_FOR_HOMING}`, () => { - props.recoveryMap.step = - RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.STEPS.PREPARE_TRACK_FOR_HOMING - render(props) - screen.getByText('MOCK_STACKER_HOME_SHUTTLE') - }) - it(`renders HoldingLabware when step is ${RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.STEPS.CONFIRM_LABWARE_IN_LATCH}`, () => { props.recoveryMap.step = RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.STEPS.CONFIRM_LABWARE_IN_LATCH diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptyStoreRetry.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptyStoreRetry.test.tsx new file mode 100644 index 00000000000..440fe002258 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptyStoreRetry.test.tsx @@ -0,0 +1,92 @@ +import { screen } from '@testing-library/react' +import { afterEach, beforeEach, describe, it, vi } from 'vitest' + +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' + +import { mockRecoveryContentProps } from '../../__fixtures__' +import { RECOVERY_MAP } from '../../constants' +import { + RetryStepInfo, + StackerHomeShuttle, + StackerHopperLwInfo, + StackerShuttleLwInfo, +} from '../../shared' +import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { StackerShuttleEmptyStoreRetry } from '../StackerShuttleEmptyRetryStore' + +import type { ComponentProps } from 'react' + +vi.mock('../SelectRecoveryOption') +vi.mock('../../shared/') + +const render = ( + props: ComponentProps +) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('StackerShuttleEmptyStoreRetry', () => { + let props: ComponentProps + beforeEach(() => { + props = { + ...mockRecoveryContentProps, + currentRecoveryOptionUtils: { + ...mockRecoveryContentProps.currentRecoveryOptionUtils, + selectedRecoveryOption: RECOVERY_MAP.STACKER_HOPPER_EMPTY_RETRY.ROUTE, + }, + } + vi.mocked(SelectRecoveryOption).mockReturnValue( +
MOCK_SELECT_RECOVERY_OPTION
+ ) + vi.mocked(StackerHopperLwInfo).mockReturnValue( +
MOCK_STACKER_HOPPER_LW_INFO
+ ) + vi.mocked(RetryStepInfo).mockReturnValue(
MOCK_RETRY_STEP_INFO
) + vi.mocked(StackerShuttleLwInfo).mockReturnValue( +
MOCK_SHUTTLE_LABWARE_INFO
+ ) + vi.mocked(StackerHomeShuttle).mockReturnValue(
MOCK_HOME_SHUTTLE
) + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + it(`renders StackerHopperLwInfo when step is ${RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.CHECK_HOPPER}`, () => { + props.recoveryMap.step = + RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.CHECK_HOPPER + render(props) + screen.getByText('MOCK_STACKER_HOPPER_LW_INFO') + }) + + it(`renders twoColumnAndImage when step is ${RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.PLACE_LABWARE_ON_SHUTTLE}`, () => { + props.recoveryMap.step = + RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.PLACE_LABWARE_ON_SHUTTLE + render(props) + screen.getByText('MOCK_SHUTTLE_LABWARE_INFO') + }) + + it(`renders RetryStepInfo when step is ${RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.RETRY}`, () => { + props.recoveryMap.step = + RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.RETRY + render(props) + screen.getByText('MOCK_RETRY_STEP_INFO') + }) + + it(`renders SelectRecoveryOption for unknown step`, () => { + props.recoveryMap.step = + RECOVERY_MAP.SKIP_STEP_WITH_NEW_TIPS.STEPS.REPLACE_TIPS + render(props) + screen.getByText('MOCK_SELECT_RECOVERY_OPTION') + }) + + it(`renders StackerHomeShuttle when step is ${RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS}`, () => { + props.recoveryMap.step = + RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS + render(props) + screen.getByText('MOCK_HOME_SHUTTLE') + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptyStoreSkip.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptyStoreSkip.test.tsx new file mode 100644 index 00000000000..53f284cd0c5 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerShuttleEmptyStoreSkip.test.tsx @@ -0,0 +1,90 @@ +import { screen } from '@testing-library/react' +import { afterEach, beforeEach, describe, it, vi } from 'vitest' + +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' + +import { mockRecoveryContentProps } from '../../__fixtures__' +import { RECOVERY_MAP } from '../../constants' +import { + SkipStepInfo, + StackerEnsureShuttleEmpty, + StackerHomeShuttle, + StackerHopperLwInfo, +} from '../../shared' +import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { StackerShuttleEmptyStoreSkip } from '../StackerShuttleEmptyStoreSkip' + +import type { ComponentProps } from 'react' + +vi.mock('../SelectRecoveryOption') +vi.mock('../../shared/') + +const render = (props: ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('StackerShuttleEmptyStoreSkip', () => { + let props: ComponentProps + beforeEach(() => { + props = { + ...mockRecoveryContentProps, + currentRecoveryOptionUtils: { + ...mockRecoveryContentProps.currentRecoveryOptionUtils, + selectedRecoveryOption: RECOVERY_MAP.STACKER_HOPPER_EMPTY_SKIP.ROUTE, + }, + } + vi.mocked(SelectRecoveryOption).mockReturnValue( +
MOCK_SELECT_RECOVERY_OPTION
+ ) + vi.mocked(StackerEnsureShuttleEmpty).mockReturnValue( +
MOCK_ENSURE_SHUTTLE_EMPTY
+ ) + vi.mocked(StackerHopperLwInfo).mockReturnValue( +
MOCK_STACKER_HOPPER_LW_INFO
+ ) + vi.mocked(SkipStepInfo).mockReturnValue(
MOCK_SKIP_STEP_INFO
) + vi.mocked(StackerHomeShuttle).mockReturnValue(
MOCK_HOME_SHUTTLE
) + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + it(`renders StackerEnsureShuttleEmpty when step is ${RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.ENSURE_SHUTTLE_EMPTY}`, () => { + props.recoveryMap.step = + RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.ENSURE_SHUTTLE_EMPTY + render(props) + screen.getByText('MOCK_ENSURE_SHUTTLE_EMPTY') + }) + + it(`renders StackerHopperLwInfo when step is ${RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.CHECK_HOPPER}`, () => { + props.recoveryMap.step = + RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.CHECK_HOPPER + render(props) + screen.getByText('MOCK_STACKER_HOPPER_LW_INFO') + }) + + it(`renders SkipStepInfo when step is ${RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.SKIP}`, () => { + props.recoveryMap.step = + RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.SKIP + render(props) + screen.getByText('MOCK_SKIP_STEP_INFO') + }) + + it(`renders SelectRecoveryOption for unknown step`, () => { + props.recoveryMap.step = + RECOVERY_MAP.SKIP_STEP_WITH_NEW_TIPS.STEPS.REPLACE_TIPS + render(props) + screen.getByText('MOCK_SELECT_RECOVERY_OPTION') + }) + + it(`renders StackerHomeShuttle when step is ${RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS}`, () => { + props.recoveryMap.step = + RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS + render(props) + screen.getByText('MOCK_HOME_SHUTTLE') + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerStalledRetry.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerStalledRetry.test.tsx index 0b696a97869..5c9d52f6d67 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerStalledRetry.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerStalledRetry.test.tsx @@ -55,13 +55,6 @@ describe('StackerStalledRetry', () => { vi.resetAllMocks() }) - it(`renders StackerHomeShuttle when step is ${RECOVERY_MAP.STACKER_STALLED_RETRY.STEPS.PREPARE_TRACK_FOR_HOMING}`, () => { - props.recoveryMap.step = - RECOVERY_MAP.STACKER_STALLED_RETRY.STEPS.PREPARE_TRACK_FOR_HOMING - render(props) - screen.getByText('MOCK_STACKER_HOME_SHUTTLE') - }) - it(`renders StackerHomeShuttle when step is ${RECOVERY_MAP.STACKER_STALLED_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS}`, () => { props.recoveryMap.step = RECOVERY_MAP.STACKER_STALLED_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerStalledSkip.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerStalledSkip.test.tsx index 8ab4e0cba58..71cb9bb1ac0 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerStalledSkip.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/StackerStalledSkip.test.tsx @@ -67,13 +67,6 @@ describe('StackerStalledSkip', () => { vi.resetAllMocks() }) - it(`renders StackerHomeShuttle when step is ${RECOVERY_MAP.STACKER_STALLED_SKIP.STEPS.PREPARE_TRACK_FOR_HOMING}`, () => { - props.recoveryMap.step = - RECOVERY_MAP.STACKER_STALLED_SKIP.STEPS.PREPARE_TRACK_FOR_HOMING - render(props) - screen.getByText('MOCK_STACKER_HOME_SHUTTLE') - }) - it(`renders StackerHomeShuttle when step is ${RECOVERY_MAP.STACKER_STALLED_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS}`, () => { props.recoveryMap.step = RECOVERY_MAP.STACKER_STALLED_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts index e216559735b..c5fe3893d6d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts @@ -20,3 +20,7 @@ export { StackerShuttleMissing } from './StackerShuttleMissing' export { StackerStalledRetry } from './StackerStalledRetry' export { StackerStalledSkip } from './StackerStalledSkip' export { StackerSelectErrorFlow } from './StackerSelectErrorFlow' +export { StackerStalledStoreSkip } from './StackerStalledStoreSkip' +export { StackerStalledStoreRetry } from './StackerStalledStoreRetry' +export { StackerShuttleEmptyStoreSkip } from './StackerShuttleEmptyStoreSkip' +export { StackerShuttleEmptyStoreRetry } from './StackerShuttleEmptyRetryStore' diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx index 15b7014c247..5006e264686 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx @@ -33,9 +33,13 @@ import { StackerHopperEmptySkip, StackerShuttleEmptyRetry, StackerShuttleEmptySkip, + StackerShuttleEmptyStoreRetry, + StackerShuttleEmptyStoreSkip, StackerShuttleMissing, StackerStalledRetry, StackerStalledSkip, + StackerStalledStoreRetry, + StackerStalledStoreSkip, } from '../RecoveryOptions' import { ErrorDetailsModal, @@ -208,6 +212,10 @@ describe('ErrorRecoveryContent', () => { STACKER_SHUTTLE_MISSING_RETRY, STACKER_SHUTTLE_EMPTY_RETRY, STACKER_SHUTTLE_EMPTY_SKIP, + STACKER_STALLED_STORE_SKIP, + STACKER_STALLED_STORE_RETRY, + STACKER_SHUTTLE_EMPTY_STORE_SKIP, + STACKER_SHUTTLE_EMPTY_STORE_RETRY, } = RECOVERY_MAP let props: ComponentProps @@ -270,6 +278,18 @@ describe('ErrorRecoveryContent', () => { vi.mocked(StackerStalledSkip).mockReturnValue(
MOCK_STACKER_STALLED_SKIP
) + vi.mocked(StackerStalledStoreSkip).mockReturnValue( +
MOCK_STACKER_STALLED_STORE_SKIP
+ ) + vi.mocked(StackerStalledStoreRetry).mockReturnValue( +
MOCK_STACKER_STALLED_STORE_RETRY
+ ) + vi.mocked(StackerShuttleEmptyStoreSkip).mockReturnValue( +
MOCK_STACKER_SHUTTLE_EMPTY_STORE_SKIP
+ ) + vi.mocked(StackerShuttleEmptyStoreRetry).mockReturnValue( +
MOCK_STACKER_SHUTTLE_EMPTY_STORE_RETRY
+ ) }) it(`returns SelectRecoveryOption when the route is ${OPTION_SELECTION.ROUTE}`, () => { @@ -499,6 +519,58 @@ describe('ErrorRecoveryContent', () => { screen.getByText('MOCK_STACKER_STALLED_SKIP') }) + it(`returns appropriate view when the route is ${STACKER_STALLED_STORE_SKIP.ROUTE}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + route: STACKER_STALLED_STORE_SKIP.ROUTE, + }, + } + renderRecoveryContent(props) + + screen.getByText('MOCK_STACKER_STALLED_STORE_SKIP') + }) + + it(`returns appropriate view when the route is ${STACKER_STALLED_STORE_RETRY.ROUTE}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + route: STACKER_STALLED_STORE_RETRY.ROUTE, + }, + } + renderRecoveryContent(props) + + screen.getByText('MOCK_STACKER_STALLED_STORE_RETRY') + }) + + it(`returns appropriate view when the route is ${STACKER_SHUTTLE_EMPTY_STORE_SKIP.ROUTE}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + route: STACKER_SHUTTLE_EMPTY_STORE_SKIP.ROUTE, + }, + } + renderRecoveryContent(props) + + screen.getByText('MOCK_STACKER_SHUTTLE_EMPTY_STORE_SKIP') + }) + + it(`returns appropriate view when the route is ${STACKER_SHUTTLE_EMPTY_STORE_RETRY.ROUTE}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + route: STACKER_SHUTTLE_EMPTY_STORE_RETRY.ROUTE, + }, + } + renderRecoveryContent(props) + + screen.getByText('MOCK_STACKER_SHUTTLE_EMPTY_STORE_RETRY') + }) + it(`returns appropriate view when the route is ${STACKER_STALLED_RETRY.ROUTE}`, () => { props = { ...props, diff --git a/app/src/organisms/ErrorRecoveryFlows/constants.ts b/app/src/organisms/ErrorRecoveryFlows/constants.ts index 281a04f6ed2..4e28765d2f9 100644 --- a/app/src/organisms/ErrorRecoveryFlows/constants.ts +++ b/app/src/organisms/ErrorRecoveryFlows/constants.ts @@ -30,6 +30,7 @@ export const DEFINED_ERROR_TYPES = { HOPPER_LABWARE_MISSING: 'flexStackerHopperLabwareFailed', STACKER_SHUTTLE_MISSING: 'flexStackerShuttleMissing', STACKER_SHUTTLE_EMPTY: 'flexStackerLabwareRetrieveFailed', + STACKER_SHUTTLE_STORE_EMPTY: 'flexStackerLabwareStoreFailed', STACKER_SHUTTLE_OCCUPIED: 'flexStackerShuttleOccupied', } as const @@ -48,6 +49,7 @@ export const ERROR_KINDS = { STACKER_HOPPER_EMPTY: 'STACKER_HOPPER_EMPTY', STACKER_SHUTTLE_MISSING: 'STACKER_SHUTTLE_MISSING', STACKER_SHUTTLE_EMPTY: 'STACKER_SHUTTLE_EMPTY', + STACKER_SHUTTLE_STORE_EMPTY: 'STACKER_SHUTTLE_STORE_EMPTY', STACKER_SHUTTLE_OCCUPIED: 'STACKER_SHUTTLE_OCCUPIED', STACKER_HOPPER_OR_SHUTTLE_EMPTY: 'STACKER_HOPPER_OR_SHUTTLE_EMPTY', } as const @@ -205,11 +207,30 @@ export const RECOVERY_MAP = { RETRY: 'retry', }, }, + SHUTTLE_FULL_RETRY: { + ROUTE: 'shuttle-full-retry', + STEPS: { + EMPTY_STACKER: 'empty-stacker', + CLEAR_TRACK_OF_OBSTRUCTIONS: 'clear-track-of-obstructions', + CHECK_HOPPER: 'check-hopper', + ENSURE_SHUTTLE_EMPTY: 'ensure-shuttle-empty', + RETRY: 'retry', + }, + }, + SHUTTLE_FULL_SKIP: { + ROUTE: 'shuttle-full-skip', + STEPS: { + EMPTY_STACKER: 'empty-stacker', + CLEAR_TRACK_OF_OBSTRUCTIONS: 'clear-track-of-obstructions', + PLACE_LABWARE_ON_SHUTTLE: 'place-labware-on-shuttle', + CHECK_HOPPER: 'check-hopper', + SKIP: 'skip', + }, + }, STACKER_STALLED_RETRY: { ROUTE: 'stacker-stalled-retry', STEPS: { EMPTY_STACKER: 'empty-stacker', - PREPARE_TRACK_FOR_HOMING: 'prepare-track-for-homing', CLEAR_TRACK_OF_OBSTRUCTIONS: 'clear-track-of-obstructions', CHECK_HOPPER: 'check-hopper', ENSURE_SHUTTLE_EMPTY: 'ensure-shuttle-empty', @@ -220,13 +241,32 @@ export const RECOVERY_MAP = { ROUTE: 'stacker-stalled-skip', STEPS: { EMPTY_STACKER: 'empty-stacker', - PREPARE_TRACK_FOR_HOMING: 'prepare-track-for-homing', CLEAR_TRACK_OF_OBSTRUCTIONS: 'clear-track-of-obstructions', PLACE_LABWARE_ON_SHUTTLE: 'place-labware-on-shuttle', CHECK_HOPPER: 'check-hopper', SKIP: 'skip', }, }, + STACKER_STALLED_STORE_RETRY: { + ROUTE: 'stacker-stalled-store-retry', + STEPS: { + EMPTY_STACKER: 'empty-stacker', + CLEAR_TRACK_OF_OBSTRUCTIONS: 'clear-track-of-obstructions', + PLACE_LABWARE_ON_SHUTTLE: 'place-labware-on-shuttle', + CHECK_HOPPER: 'check-hopper', + RETRY: 'retry', + }, + }, + STACKER_STALLED_STORE_SKIP: { + ROUTE: 'stacker-stalled-store-skip', + STEPS: { + EMPTY_STACKER: 'empty-stacker', + CLEAR_TRACK_OF_OBSTRUCTIONS: 'clear-track-of-obstructions', + ENSURE_SHUTTLE_EMPTY: 'ensure-shuttle-empty', + CHECK_HOPPER: 'check-hopper', + SKIP: 'skip', + }, + }, STACKER_HOPPER_OR_SHUTTLE_EMPTY: { ROUTE: 'stacker-hopper-or-shuttle-empty', STEPS: { @@ -253,7 +293,6 @@ export const RECOVERY_MAP = { ROUTE: 'stacker-shuttle-empty-retry', STEPS: { EMPTY_STACKER: 'empty-stacker', - PREPARE_TRACK_FOR_HOMING: 'prepare-track-for-homing', CONFIRM_LABWARE_IN_LATCH: 'confirm-labware-in-latch', RELEASE_FROM_LATCH: 'release-labware-from-latch', REENGAGE_LATCH: 're-engage-latch', @@ -266,7 +305,6 @@ export const RECOVERY_MAP = { ROUTE: 'stacker-shuttle-empty-skip', STEPS: { EMPTY_STACKER: 'empty-stacker', - PREPARE_TRACK_FOR_HOMING: 'prepare-track-for-homing', CONFIRM_LABWARE_IN_LATCH: 'confirm-labware-in-latch', RELEASE_FROM_LATCH: 'release-labware-from-latch', REENGAGE_LATCH: 're-engage-latch', @@ -275,6 +313,24 @@ export const RECOVERY_MAP = { SKIP: 'skip', }, }, + STACKER_SHUTTLE_EMPTY_STORE_RETRY: { + ROUTE: 'stacker-shuttle-empty-store-retry', + STEPS: { + CLEAR_TRACK_OF_OBSTRUCTIONS: 'clear-track-of-obstructions', + PLACE_LABWARE_ON_SHUTTLE: 'place-labware-on-shuttle', + CHECK_HOPPER: 'check-hopper', + RETRY: 'retry', + }, + }, + STACKER_SHUTTLE_EMPTY_STORE_SKIP: { + ROUTE: 'stacker-shuttle-empty-store-skip', + STEPS: { + CLEAR_TRACK_OF_OBSTRUCTIONS: 'clear-track-of-obstructions', + ENSURE_SHUTTLE_EMPTY: 'ensure-shuttle-empty', + CHECK_HOPPER: 'check-hopper', + SKIP: 'skip', + }, + }, STACKER_SHUTTLE_MISSING_RETRY: { ROUTE: 'load-shuttle-and-retry', STEPS: { @@ -344,8 +400,12 @@ const { MANUAL_FILL_AND_RETRY_NEW_TIPS, MANUAL_MOVE_AND_SKIP, MANUAL_REPLACE_AND_RETRY, + SHUTTLE_FULL_RETRY, + SHUTTLE_FULL_SKIP, STACKER_STALLED_RETRY, STACKER_STALLED_SKIP, + STACKER_STALLED_STORE_RETRY, + STACKER_STALLED_STORE_SKIP, SKIP_STEP_WITH_NEW_TIPS, SKIP_STEP_WITH_SAME_TIPS, HOME_AND_RETRY, @@ -354,6 +414,8 @@ const { STACKER_HOPPER_EMPTY_SKIP, STACKER_SHUTTLE_EMPTY_RETRY, STACKER_SHUTTLE_EMPTY_SKIP, + STACKER_SHUTTLE_EMPTY_STORE_RETRY, + STACKER_SHUTTLE_EMPTY_STORE_SKIP, STACKER_HOPPER_OR_SHUTTLE_EMPTY, } = RECOVERY_MAP @@ -426,9 +488,22 @@ export const STEP_ORDER: StepOrder = { MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE, MANUAL_REPLACE_AND_RETRY.STEPS.RETRY, ], + [SHUTTLE_FULL_RETRY.ROUTE]: [ + SHUTTLE_FULL_RETRY.STEPS.EMPTY_STACKER, + SHUTTLE_FULL_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS, + SHUTTLE_FULL_RETRY.STEPS.CHECK_HOPPER, + SHUTTLE_FULL_RETRY.STEPS.ENSURE_SHUTTLE_EMPTY, + SHUTTLE_FULL_RETRY.STEPS.RETRY, + ], + [SHUTTLE_FULL_SKIP.ROUTE]: [ + SHUTTLE_FULL_SKIP.STEPS.EMPTY_STACKER, + SHUTTLE_FULL_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS, + SHUTTLE_FULL_SKIP.STEPS.PLACE_LABWARE_ON_SHUTTLE, + SHUTTLE_FULL_SKIP.STEPS.CHECK_HOPPER, + SHUTTLE_FULL_SKIP.STEPS.SKIP, + ], [STACKER_STALLED_RETRY.ROUTE]: [ STACKER_STALLED_RETRY.STEPS.EMPTY_STACKER, - STACKER_STALLED_RETRY.STEPS.PREPARE_TRACK_FOR_HOMING, STACKER_STALLED_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS, STACKER_STALLED_RETRY.STEPS.CHECK_HOPPER, STACKER_STALLED_RETRY.STEPS.ENSURE_SHUTTLE_EMPTY, @@ -436,12 +511,25 @@ export const STEP_ORDER: StepOrder = { ], [STACKER_STALLED_SKIP.ROUTE]: [ STACKER_STALLED_SKIP.STEPS.EMPTY_STACKER, - STACKER_STALLED_SKIP.STEPS.PREPARE_TRACK_FOR_HOMING, STACKER_STALLED_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS, STACKER_STALLED_SKIP.STEPS.PLACE_LABWARE_ON_SHUTTLE, STACKER_STALLED_SKIP.STEPS.CHECK_HOPPER, STACKER_STALLED_SKIP.STEPS.SKIP, ], + [STACKER_STALLED_STORE_RETRY.ROUTE]: [ + STACKER_STALLED_STORE_RETRY.STEPS.EMPTY_STACKER, + STACKER_STALLED_STORE_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS, + STACKER_STALLED_STORE_RETRY.STEPS.PLACE_LABWARE_ON_SHUTTLE, + STACKER_STALLED_STORE_RETRY.STEPS.CHECK_HOPPER, + STACKER_STALLED_STORE_RETRY.STEPS.RETRY, + ], + [STACKER_STALLED_STORE_SKIP.ROUTE]: [ + STACKER_STALLED_STORE_SKIP.STEPS.EMPTY_STACKER, + STACKER_STALLED_STORE_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS, + STACKER_STALLED_STORE_SKIP.STEPS.ENSURE_SHUTTLE_EMPTY, + STACKER_STALLED_STORE_SKIP.STEPS.CHECK_HOPPER, + STACKER_STALLED_STORE_SKIP.STEPS.SKIP, + ], [STACKER_SHUTTLE_MISSING_RETRY.ROUTE]: [ STACKER_SHUTTLE_MISSING_RETRY.STEPS.PREPARE_TRACK_FOR_HOMING, STACKER_SHUTTLE_MISSING_RETRY.STEPS.LOAD_SHUTTLE, @@ -464,7 +552,6 @@ export const STEP_ORDER: StepOrder = { ], [STACKER_SHUTTLE_EMPTY_RETRY.ROUTE]: [ STACKER_SHUTTLE_EMPTY_RETRY.STEPS.EMPTY_STACKER, - STACKER_SHUTTLE_EMPTY_RETRY.STEPS.PREPARE_TRACK_FOR_HOMING, STACKER_SHUTTLE_EMPTY_RETRY.STEPS.CONFIRM_LABWARE_IN_LATCH, STACKER_SHUTTLE_EMPTY_RETRY.STEPS.RELEASE_FROM_LATCH, STACKER_SHUTTLE_EMPTY_RETRY.STEPS.REENGAGE_LATCH, @@ -474,7 +561,6 @@ export const STEP_ORDER: StepOrder = { ], [STACKER_SHUTTLE_EMPTY_SKIP.ROUTE]: [ STACKER_SHUTTLE_EMPTY_SKIP.STEPS.EMPTY_STACKER, - STACKER_SHUTTLE_EMPTY_SKIP.STEPS.PREPARE_TRACK_FOR_HOMING, STACKER_SHUTTLE_EMPTY_SKIP.STEPS.CONFIRM_LABWARE_IN_LATCH, STACKER_SHUTTLE_EMPTY_SKIP.STEPS.RELEASE_FROM_LATCH, STACKER_SHUTTLE_EMPTY_SKIP.STEPS.REENGAGE_LATCH, @@ -482,6 +568,18 @@ export const STEP_ORDER: StepOrder = { STACKER_SHUTTLE_EMPTY_SKIP.STEPS.FILL_HOPPER, STACKER_SHUTTLE_EMPTY_SKIP.STEPS.SKIP, ], + [STACKER_SHUTTLE_EMPTY_STORE_RETRY.ROUTE]: [ + STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS, + STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.PLACE_LABWARE_ON_SHUTTLE, + STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.CHECK_HOPPER, + STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.RETRY, + ], + [STACKER_SHUTTLE_EMPTY_STORE_SKIP.ROUTE]: [ + STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS, + STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.CHECK_HOPPER, + STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.ENSURE_SHUTTLE_EMPTY, + STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.SKIP, + ], [ERROR_WHILE_RECOVERING.ROUTE]: [ ERROR_WHILE_RECOVERING.STEPS.RECOVERY_ACTION_FAILED, ERROR_WHILE_RECOVERING.STEPS.DROP_TIP_GENERAL_ERROR, @@ -636,13 +734,40 @@ export const RECOVERY_MAP_METADATA: RecoveryRouteStepMetadata = { [MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE]: { allowDoorOpen: true }, [MANUAL_REPLACE_AND_RETRY.STEPS.RETRY]: { allowDoorOpen: true }, }, + [SHUTTLE_FULL_RETRY.ROUTE]: { + [SHUTTLE_FULL_RETRY.STEPS.EMPTY_STACKER]: { + allowDoorOpen: true, + }, + [SHUTTLE_FULL_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS]: { + allowDoorOpen: true, + }, + [SHUTTLE_FULL_RETRY.STEPS.CHECK_HOPPER]: { + allowDoorOpen: true, + }, + [SHUTTLE_FULL_RETRY.STEPS.ENSURE_SHUTTLE_EMPTY]: { + allowDoorOpen: true, + }, + [SHUTTLE_FULL_RETRY.STEPS.RETRY]: { allowDoorOpen: false }, + }, + [SHUTTLE_FULL_SKIP.ROUTE]: { + [SHUTTLE_FULL_SKIP.STEPS.EMPTY_STACKER]: { + allowDoorOpen: true, + }, + [SHUTTLE_FULL_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS]: { + allowDoorOpen: true, + }, + [SHUTTLE_FULL_SKIP.STEPS.CHECK_HOPPER]: { + allowDoorOpen: true, + }, + [SHUTTLE_FULL_SKIP.STEPS.PLACE_LABWARE_ON_SHUTTLE]: { + allowDoorOpen: true, + }, + [SHUTTLE_FULL_SKIP.STEPS.SKIP]: { allowDoorOpen: false }, + }, [STACKER_STALLED_RETRY.ROUTE]: { [STACKER_STALLED_RETRY.STEPS.EMPTY_STACKER]: { allowDoorOpen: true, }, - [STACKER_STALLED_RETRY.STEPS.PREPARE_TRACK_FOR_HOMING]: { - allowDoorOpen: false, - }, [STACKER_STALLED_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS]: { allowDoorOpen: true, }, @@ -658,9 +783,6 @@ export const RECOVERY_MAP_METADATA: RecoveryRouteStepMetadata = { [STACKER_STALLED_SKIP.STEPS.EMPTY_STACKER]: { allowDoorOpen: true, }, - [STACKER_STALLED_SKIP.STEPS.PREPARE_TRACK_FOR_HOMING]: { - allowDoorOpen: false, - }, [STACKER_STALLED_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS]: { allowDoorOpen: true, }, @@ -672,6 +794,37 @@ export const RECOVERY_MAP_METADATA: RecoveryRouteStepMetadata = { }, [STACKER_STALLED_SKIP.STEPS.SKIP]: { allowDoorOpen: false }, }, + [STACKER_STALLED_STORE_RETRY.ROUTE]: { + [STACKER_STALLED_STORE_RETRY.STEPS.EMPTY_STACKER]: { + allowDoorOpen: true, + }, + [STACKER_STALLED_STORE_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS]: { + allowDoorOpen: true, + }, + [STACKER_STALLED_STORE_RETRY.STEPS.PLACE_LABWARE_ON_SHUTTLE]: { + allowDoorOpen: false, + }, + [STACKER_STALLED_STORE_RETRY.STEPS.CHECK_HOPPER]: { + allowDoorOpen: true, + }, + + [STACKER_STALLED_STORE_RETRY.STEPS.RETRY]: { allowDoorOpen: false }, + }, + [STACKER_STALLED_STORE_SKIP.ROUTE]: { + [STACKER_STALLED_STORE_SKIP.STEPS.EMPTY_STACKER]: { + allowDoorOpen: true, + }, + [STACKER_STALLED_STORE_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS]: { + allowDoorOpen: true, + }, + [STACKER_STALLED_STORE_SKIP.STEPS.ENSURE_SHUTTLE_EMPTY]: { + allowDoorOpen: true, + }, + [STACKER_STALLED_STORE_SKIP.STEPS.CHECK_HOPPER]: { + allowDoorOpen: true, + }, + [STACKER_STALLED_STORE_SKIP.STEPS.SKIP]: { allowDoorOpen: false }, + }, [STACKER_HOPPER_OR_SHUTTLE_EMPTY.ROUTE]: { [STACKER_HOPPER_OR_SHUTTLE_EMPTY.STEPS.SELECT_FLOW]: { allowDoorOpen: false, @@ -716,9 +869,6 @@ export const RECOVERY_MAP_METADATA: RecoveryRouteStepMetadata = { [STACKER_SHUTTLE_EMPTY_RETRY.STEPS.EMPTY_STACKER]: { allowDoorOpen: true, }, - [STACKER_SHUTTLE_EMPTY_RETRY.STEPS.PREPARE_TRACK_FOR_HOMING]: { - allowDoorOpen: false, - }, [STACKER_SHUTTLE_EMPTY_RETRY.STEPS.CONFIRM_LABWARE_IN_LATCH]: { allowDoorOpen: true, }, @@ -742,9 +892,6 @@ export const RECOVERY_MAP_METADATA: RecoveryRouteStepMetadata = { [STACKER_SHUTTLE_EMPTY_SKIP.STEPS.EMPTY_STACKER]: { allowDoorOpen: true, }, - [STACKER_SHUTTLE_EMPTY_SKIP.STEPS.PREPARE_TRACK_FOR_HOMING]: { - allowDoorOpen: false, - }, [STACKER_SHUTTLE_EMPTY_SKIP.STEPS.CONFIRM_LABWARE_IN_LATCH]: { allowDoorOpen: true, }, @@ -764,6 +911,34 @@ export const RECOVERY_MAP_METADATA: RecoveryRouteStepMetadata = { allowDoorOpen: false, }, }, + [STACKER_SHUTTLE_EMPTY_STORE_RETRY.ROUTE]: { + [STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS]: { + allowDoorOpen: true, + }, + [STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.PLACE_LABWARE_ON_SHUTTLE]: { + allowDoorOpen: true, + }, + [STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.CHECK_HOPPER]: { + allowDoorOpen: true, + }, + [STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.RETRY]: { + allowDoorOpen: false, + }, + }, + [STACKER_SHUTTLE_EMPTY_STORE_SKIP.ROUTE]: { + [STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.CLEAR_TRACK_OF_OBSTRUCTIONS]: { + allowDoorOpen: true, + }, + [STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.CHECK_HOPPER]: { + allowDoorOpen: true, + }, + [STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.ENSURE_SHUTTLE_EMPTY]: { + allowDoorOpen: true, + }, + [STACKER_SHUTTLE_EMPTY_STORE_SKIP.STEPS.SKIP]: { + allowDoorOpen: false, + }, + }, [RETRY_STEP.ROUTE]: { [RETRY_STEP.STEPS.CONFIRM_RETRY]: { allowDoorOpen: false, diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useDeckMapUtils.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useDeckMapUtils.test.ts index 317922e1042..daddc2731a5 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useDeckMapUtils.test.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useDeckMapUtils.test.ts @@ -361,7 +361,7 @@ describe('getRunCurrentLabwareInfo', () => { it('should return an empty array if runRecord is null', () => { const result = getRunCurrentLabwareInfo({ - runRecord: undefined, + runData: undefined, runLwDefsByUri: {} as any, }) @@ -370,7 +370,7 @@ describe('getRunCurrentLabwareInfo', () => { it('should return an empty array if protocolAnalysis is null', () => { const result = getRunCurrentLabwareInfo({ - runRecord: { data: { labware: [] } } as any, + runData: { labware: [] } as any, runLwDefsByUri: {}, }) @@ -384,7 +384,7 @@ describe('getRunCurrentLabwareInfo', () => { } const result = getRunCurrentLabwareInfo({ - runRecord: { data: { labware: [mockPickUpTipLwSlotName] } } as any, + runData: { labware: [mockPickUpTipLwSlotName] } as any, runLwDefsByUri: { [mockPickUpTipLabware.definitionUri]: mockLabwareDef, }, diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx index b3344133412..cf3430c214d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx @@ -122,6 +122,28 @@ describe('useRecoveryOptionCopy', () => { screen.getByText('Home gantry and retry step') }) + it(`renders the correct copy for ${RECOVERY_MAP.STACKER_STALLED_RETRY.ROUTE}`, () => { + render({ route: RECOVERY_MAP.STACKER_STALLED_RETRY.ROUTE }) + + screen.getByText('Clear obstruction in stacker and retry step') + }) + + it(`renders the correct copy for ${RECOVERY_MAP.STACKER_HOPPER_EMPTY_RETRY.ROUTE}`, () => { + render({ route: RECOVERY_MAP.STACKER_HOPPER_EMPTY_RETRY.ROUTE }) + }) + + it(`renders the correct copy for ${RECOVERY_MAP.STACKER_STALLED_SKIP.ROUTE}`, () => { + render({ route: RECOVERY_MAP.STACKER_STALLED_SKIP.ROUTE }) + + screen.getByText('Manually load labware onto labware shuttle and skip step') + }) + + it(`renders the correct copy for ${RECOVERY_MAP.STACKER_STALLED_STORE_SKIP.ROUTE}`, () => { + render({ route: RECOVERY_MAP.STACKER_STALLED_STORE_SKIP.ROUTE }) + + screen.getByText('Clear obstruction in stacker and skip to next step') + }) + it('renders "Unknown action" for an unknown recovery option', () => { render({ route: 'unknown_route' as RecoveryRoute }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryToasts.test.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryToasts.test.tsx index c3952227aab..f1fccff79a2 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryToasts.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryToasts.test.tsx @@ -303,8 +303,11 @@ describe('handleRecoveryOptionAction', () => { RECOVERY_MAP.IGNORE_AND_SKIP.ROUTE, RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, RECOVERY_MAP.STACKER_STALLED_SKIP.ROUTE, + RECOVERY_MAP.STACKER_STALLED_STORE_SKIP.ROUTE, RECOVERY_MAP.STACKER_HOPPER_EMPTY_SKIP.ROUTE, RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.ROUTE, + RECOVERY_MAP.SHUTTLE_FULL_SKIP.ROUTE, + RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.ROUTE, ] // Routes that should return the currentStepReturnVal toasts. @@ -318,10 +321,13 @@ describe('handleRecoveryOptionAction', () => { RECOVERY_MAP.MANUAL_FILL_AND_RETRY_NEW_TIPS.ROUTE, RECOVERY_MAP.MANUAL_FILL_AND_RETRY_SAME_TIPS.ROUTE, RECOVERY_MAP.STACKER_STALLED_RETRY.ROUTE, + RECOVERY_MAP.STACKER_STALLED_STORE_RETRY.ROUTE, RECOVERY_MAP.STACKER_HOPPER_EMPTY_RETRY.ROUTE, RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.ROUTE, RECOVERY_MAP.STACKER_SHUTTLE_MISSING_RETRY.ROUTE, RECOVERY_MAP.STACKER_HOPPER_OR_SHUTTLE_EMPTY.ROUTE, + RECOVERY_MAP.SHUTTLE_FULL_RETRY.ROUTE, + RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.ROUTE, ] // Routes that should return no toasts. diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts index f148447430a..95e5705b5ea 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts @@ -15,12 +15,9 @@ import { } from '@opentrons/shared-data' import { RECOVERY_MAP } from '/app/organisms/ErrorRecoveryFlows/constants' -import { - getRunLabwareRenderInfo, - getRunModuleRenderInfo, -} from '/app/organisms/InterventionModal/utils' +import { getRunModuleRenderInfo } from '/app/organisms/InterventionModal/utils' -import type { Run } from '@opentrons/api-client' +import type { Run, RunData } from '@opentrons/api-client' import type { CutoutConfigProtocolSpec, DeckDefinition, @@ -33,10 +30,7 @@ import type { ModuleModel, RobotType, } from '@opentrons/shared-data' -import type { - RunLabwareInfo, - RunModuleInfo, -} from '/app/organisms/InterventionModal/utils' +import type { RunModuleInfo } from '/app/organisms/InterventionModal/utils' import type { ErrorRecoveryFlowsProps } from '..' import type { ERUtilsProps, ERUtilsResults } from './useERUtils' import type { UseFailedLabwareUtilsResult } from './useFailedLabwareUtils' @@ -58,7 +52,6 @@ export interface UseDeckMapUtilsResult { loadedModules: LoadedModule[] movedLabwareDef: LabwareDefinition | null moduleRenderInfo: RunModuleInfo[] - labwareRenderInfo: RunLabwareInfo[] highlightLabwareEventuallyIn: string[] kind: 'intervention' robotType: RobotType @@ -99,7 +92,8 @@ export function useDeckMapUtils({ ) const currentLabwareInfo = useMemo( - () => getRunCurrentLabwareInfo({ runRecord, runLwDefsByUri }), + () => + getRunCurrentLabwareInfo({ runData: runRecord?.data, runLwDefsByUri }), [runRecord, runLwDefsByUri] ) @@ -132,14 +126,6 @@ export function useDeckMapUtils({ [deckDef, runLwDefsByUri, runRecord] ) - const labwareRenderInfo = useMemo( - () => - runRecord != null && runLwDefsByUri != null - ? getRunLabwareRenderInfo(runRecord.data, runLwDefsByUri, deckDef) - : [], - [deckDef, runLwDefsByUri, runRecord] - ) - return { deckConfig, modulesOnDeck: updatedModules.map( @@ -150,10 +136,13 @@ export function useDeckMapUtils({ nestedLabwareDef, }) ), - labwareOnDeck: runCurrentLabware.map(({ labwareLocation, definition }) => ({ - labwareLocation, - definition, - })), + labwareOnDeck: runCurrentLabware.map( + ({ labwareLocation, definition, labwareId }) => ({ + labwareLocation, + definition, + labwareId, + }) + ), highlightLabwareEventuallyIn: [...updatedModules, ...runCurrentLabware] .map(el => el.highlight) .filter(maybeSlot => maybeSlot != null) as string[], @@ -163,7 +152,6 @@ export function useDeckMapUtils({ loadedLabware: runRecord?.data.labware ?? [], movedLabwareDef, moduleRenderInfo, - labwareRenderInfo, } } @@ -218,6 +206,7 @@ export function getRunCurrentModulesOnDeck({ interface RunCurrentLabwareOnDeck { labwareLocation: LabwareLocation definition: LabwareDefinition + labwareId?: string } // Builds the necessary labware object expected by BaseDeck. // Note that while this highlights all labware in the failed labware slot, the result is later filtered to render @@ -248,12 +237,13 @@ export function getRunCurrentLabwareOnDeck({ } return currentLabwareInfo.map( - ({ slotName, labwareDef, labwareLocation }) => ({ + ({ slotName, labwareDef, labwareLocation, labwareId }) => ({ labwareLocation, definition: labwareDef, highlight: getIsLabwareMatch(slotName, runRecord, labwareToMatch()) ? slotName : null, + labwareId, }) ) } @@ -310,7 +300,7 @@ export const getRunCurrentModulesInfo = ({ const nestedLwLoc = nestedLabware?.location ?? null const [nestedLwSlotName] = getSlotNameAndLwLocFrom( nestedLwLoc, - runRecord, + runRecord.data, false ) @@ -343,21 +333,21 @@ interface RunCurrentLabwareInfo { // Derive the labware info necessary to render labware on the deck. export function getRunCurrentLabwareInfo({ - runRecord, + runData, runLwDefsByUri, }: { - runRecord: UseDeckMapUtilsProps['runRecord'] + runData: RunData | undefined runLwDefsByUri: UseDeckMapUtilsProps['runLwDefsByUri'] }): RunCurrentLabwareInfo[] { - if (runRecord == null) { + if (runData == null) { return [] } else { - const allLabware = runRecord.data.labware.reduce( + const allLabware = runData.labware.reduce( (acc: RunCurrentLabwareInfo[], lw) => { const loc = lw.location const [slotName, labwareLocation] = getSlotNameAndLwLocFrom( loc, - runRecord, + runData, true ) // Exclude modules since handled separately. const labwareDef = getLabwareDefinition(lw, runLwDefsByUri) @@ -430,14 +420,14 @@ const getLabwareDefinition = ( // Get the slotName for on deck labware. export function getSlotNameAndLwLocFrom( location: LabwareLocation | null, - runRecord: UseDeckMapUtilsProps['runRecord'], + runData: RunData, excludeModules: boolean ): [string | null, LabwareLocation | null] { const labwareLocationObject = getLabwareLocation({ location, detailLevel: 'slot-only', - loadedLabwares: runRecord?.data?.labware ?? [], - loadedModules: runRecord?.data?.modules ?? [], + loadedLabwares: runData?.labware ?? [], + loadedModules: runData?.modules ?? [], robotType: FLEX_ROBOT_TYPE, }) const onModuleModel = labwareLocationObject?.moduleModel ?? null @@ -455,13 +445,11 @@ export function getSlotNameAndLwLocFrom( location === 'systemLocation' ) { return [null, null] + } else if (excludeModules && onModuleModel != null) { + return [null, null] } else if ('moduleId' in location) { - if (excludeModules && onModuleModel != null) { - return [null, null] - } else { - const moduleId = location.moduleId - return [baseSlot, { moduleId }] - } + const moduleId = location.moduleId + return [baseSlot, { moduleId }] } else if ('labwareId' in location) { const labwareId = location.labwareId return [baseSlot, { labwareId }] diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts index c2d75ae2de7..1a3babe234a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts @@ -37,6 +37,8 @@ export function useErrorName(errorKind: ErrorKind): string { return t('stacker_shuttle_full') case ERROR_KINDS.STACKER_HOPPER_OR_SHUTTLE_EMPTY: return t('stacker_error') + case ERROR_KINDS.STACKER_SHUTTLE_STORE_EMPTY: + return t('stacker_shuttle_store_empty') default: return t('error') } diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts index fe4aa360dbf..8219ff84340 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts @@ -116,6 +116,7 @@ export function useFailedLabwareUtils({ }), [failedCommand, runCommands] ) + const relevantPickUpTipCommand = getRelevantPickUpTipCommand( failedCommandByRunRecord, runCommands @@ -230,6 +231,7 @@ export function getRelevantFailedLabwareCmdFrom({ case ERROR_KINDS.STACKER_SHUTTLE_EMPTY: case ERROR_KINDS.STACKER_SHUTTLE_OCCUPIED: case ERROR_KINDS.STACKER_HOPPER_OR_SHUTTLE_EMPTY: + case ERROR_KINDS.STACKER_SHUTTLE_STORE_EMPTY: return failedCommandByRunRecord as FlexStackerRetrieveRunTimeCommand default: console.error( @@ -378,6 +380,7 @@ export function getRelevantLabwareIdFromFailedCmd( 'flexStackerHopperLabwareFailed', 'flexStackerLabwareRetrieveFailed', 'flexStackerShuttleOccupied', + 'flexStackerLabwareStoreFailed', ].includes(error.errorType) if (recentRelevantFailedLabwareCmd == null) { return null diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx index 71245ac7db0..56434501de2 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx @@ -48,18 +48,27 @@ export function useRecoveryOptionCopy(): ( case RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE: return t('manually_replace_lw_and_retry') case RECOVERY_MAP.STACKER_STALLED_RETRY.ROUTE: + case RECOVERY_MAP.STACKER_STALLED_STORE_RETRY.ROUTE: + case RECOVERY_MAP.SHUTTLE_FULL_RETRY.ROUTE: return t('clear_obstruction_in_stacker_and_retry_step') case RECOVERY_MAP.STACKER_HOPPER_EMPTY_RETRY.ROUTE: return t('load_labware_into_stacker_and_retry_step') case RECOVERY_MAP.STACKER_STALLED_SKIP.ROUTE: case RECOVERY_MAP.STACKER_HOPPER_EMPTY_SKIP.ROUTE: return t('manually_load_labware_into_labware_shuttle_and_skip_step') + case RECOVERY_MAP.STACKER_STALLED_STORE_SKIP.ROUTE: + case RECOVERY_MAP.SHUTTLE_FULL_SKIP.ROUTE: + return t('clear_obstruction_in_stacker_and_skip_to_next_step') case RECOVERY_MAP.STACKER_SHUTTLE_MISSING_RETRY.ROUTE: return t('load_labware_shuttle_and_retry_step') case RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.ROUTE: return t('manually_load_labware_into_shuttle_and_skip') + case RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.ROUTE: + return t('manually_load_labware_into_stacker_and_skip') case RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.ROUTE: return t('replace_labware_in_stacker_and_retry') + case RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.ROUTE: + return t('load_labware_onto_shuttle_and_retry') default: return 'Unknown action' } diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts index f0ac0a9738d..305968fb90d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts @@ -171,7 +171,10 @@ export function handleRecoveryOptionAction( case RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE: case RECOVERY_MAP.STACKER_STALLED_SKIP.ROUTE: case RECOVERY_MAP.STACKER_HOPPER_EMPTY_SKIP.ROUTE: + case RECOVERY_MAP.STACKER_STALLED_STORE_SKIP.ROUTE: case RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.ROUTE: + case RECOVERY_MAP.SHUTTLE_FULL_SKIP.ROUTE: + case RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.ROUTE: return nextStepReturnVal case RECOVERY_MAP.CANCEL_RUN.ROUTE: case RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE: @@ -183,9 +186,12 @@ export function handleRecoveryOptionAction( case RECOVERY_MAP.MANUAL_FILL_AND_RETRY_SAME_TIPS.ROUTE: case RECOVERY_MAP.STACKER_STALLED_RETRY.ROUTE: case RECOVERY_MAP.STACKER_HOPPER_EMPTY_RETRY.ROUTE: + case RECOVERY_MAP.STACKER_STALLED_STORE_RETRY.ROUTE: case RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.ROUTE: case RECOVERY_MAP.STACKER_SHUTTLE_MISSING_RETRY.ROUTE: case RECOVERY_MAP.STACKER_HOPPER_OR_SHUTTLE_EMPTY.ROUTE: + case RECOVERY_MAP.SHUTTLE_FULL_RETRY.ROUTE: + case RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.ROUTE: return currentStepReturnVal default: { console.error('Unhandled recovery toast case. Handle explicitly.') diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx index 5c53371612d..596e35579d1 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx @@ -75,6 +75,9 @@ export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element { case ERROR_KINDS.STACKER_HOPPER_EMPTY: case ERROR_KINDS.STACKER_SHUTTLE_EMPTY: case ERROR_KINDS.STACKER_SHUTTLE_MISSING: + case ERROR_KINDS.STACKER_SHUTTLE_STORE_EMPTY: + case ERROR_KINDS.STACKER_SHUTTLE_OCCUPIED: + case ERROR_KINDS.STACKER_HOPPER_OR_SHUTTLE_EMPTY: return true default: return false @@ -233,6 +236,12 @@ export function NotificationBanner({ return case ERROR_KINDS.STACKER_SHUTTLE_MISSING: return + case ERROR_KINDS.STACKER_SHUTTLE_STORE_EMPTY: + return + case ERROR_KINDS.STACKER_SHUTTLE_OCCUPIED: + return + case ERROR_KINDS.STACKER_HOPPER_OR_SHUTTLE_EMPTY: + return default: console.error('Handle error kind notification banners explicitly.') return
@@ -326,6 +335,42 @@ export function StackerShuttleMissingErrorBanner(): JSX.Element { ) } +export function StackerShuttleStoreEmptyErrorBanner(): JSX.Element { + const { t } = useTranslation('error_recovery') + + return ( + + ) +} + +export function StackerShuttleOccupiedErrorBanner(): JSX.Element { + const { t } = useTranslation('error_recovery') + + return ( + + ) +} + +export function StackerHopperOrShuttleEmptyErrorBanner(): JSX.Element { + const { t } = useTranslation('error_recovery') + + return ( + + ) +} + export function LabwareMissingOnShuttleErrorBanner(): JSX.Element { const { t } = useTranslation('error_recovery') diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx index 47b915fca71..c490f828b9f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx @@ -39,8 +39,11 @@ export function LeftColumnLabwareInfo({ const { STACKER_STALLED_RETRY, STACKER_STALLED_SKIP, + STACKER_STALLED_STORE_SKIP, + STACKER_STALLED_STORE_RETRY, STACKER_HOPPER_EMPTY_SKIP, STACKER_SHUTTLE_EMPTY_SKIP, + STACKER_SHUTTLE_EMPTY_STORE_RETRY, } = RECOVERY_MAP const { t, i18n } = useTranslation(['error_recovery', 'shared']) @@ -72,6 +75,8 @@ export function LeftColumnLabwareInfo({ } else { switch (step) { case STACKER_STALLED_RETRY.STEPS.CHECK_HOPPER: + case STACKER_STALLED_STORE_RETRY.STEPS.CHECK_HOPPER: + case STACKER_STALLED_STORE_SKIP.STEPS.CHECK_HOPPER: case STACKER_STALLED_SKIP.STEPS.CHECK_HOPPER: case STACKER_SHUTTLE_EMPTY_SKIP.STEPS.FILL_HOPPER: return { @@ -85,8 +90,10 @@ export function LeftColumnLabwareInfo({ }, } case STACKER_STALLED_SKIP.STEPS.PLACE_LABWARE_ON_SHUTTLE: + case STACKER_STALLED_STORE_RETRY.STEPS.PLACE_LABWARE_ON_SHUTTLE: case STACKER_HOPPER_EMPTY_SKIP.STEPS.PLACE_LABWARE_ON_SHUTTLE: case STACKER_SHUTTLE_EMPTY_SKIP.STEPS.PLACE_LABWARE_ON_SHUTTLE: + case STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.PLACE_LABWARE_ON_SHUTTLE: return { labwareName: failedLabwareNames.name ?? '', labwareNickname: failedLabwareNames.nickName, @@ -112,34 +119,51 @@ export function LeftColumnLabwareInfo({ } const buildQuantity = (): number | null => { - if (!showQuantity) { + if (!showQuantity || labwareQuantity == null) { return null } - if ( - (route === RECOVERY_MAP.STACKER_HOPPER_EMPTY_SKIP.ROUTE && - step === RECOVERY_MAP.STACKER_HOPPER_EMPTY_SKIP.STEPS.FILL_HOPPER) || - (route === RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.ROUTE && - step === RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.STEPS.FILL_HOPPER) || - (route === RECOVERY_MAP.STACKER_STALLED_SKIP.ROUTE && - step === RECOVERY_MAP.STACKER_STALLED_SKIP.STEPS.CHECK_HOPPER) - ) { - return labwareQuantity != null && labwareQuantity > 0 - ? labwareQuantity - 1 // one has been moved manually onto the shuttle - : null - } else { - return labwareQuantity ?? null - } + // Define routes and steps that require quantity adjustment + const requiresQuantityAdjustment = [ + { + route: RECOVERY_MAP.STACKER_HOPPER_EMPTY_SKIP.ROUTE, + step: RECOVERY_MAP.STACKER_HOPPER_EMPTY_SKIP.STEPS.FILL_HOPPER, + }, + { + route: RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.ROUTE, + step: RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.STEPS.FILL_HOPPER, + }, + { + route: RECOVERY_MAP.STACKER_STALLED_SKIP.ROUTE, + step: RECOVERY_MAP.STACKER_STALLED_SKIP.STEPS.CHECK_HOPPER, + }, + { + route: RECOVERY_MAP.STACKER_STALLED_STORE_RETRY.ROUTE, + step: RECOVERY_MAP.STACKER_STALLED_STORE_RETRY.STEPS.CHECK_HOPPER, + }, + { + route: RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.ROUTE, + step: RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_RETRY.STEPS.CHECK_HOPPER, + }, + ] + + const needsAdjustment = requiresQuantityAdjustment.some( + ({ route: expectedRoute, step: expectedStep }) => + route === expectedRoute && step === expectedStep + ) + + return needsAdjustment && labwareQuantity > 0 + ? labwareQuantity - 1 // one has been moved manually onto the shuttle + : labwareQuantity } // build info props + const quantity = buildQuantity() return ( { + skipFailedCommand() + }) + break default: skipFailedCommand() } @@ -76,6 +86,9 @@ export function SkipStepInfo(props: RecoveryContentProps): JSX.Element { case STACKER_STALLED_SKIP.ROUTE: case STACKER_HOPPER_EMPTY_SKIP.ROUTE: case STACKER_SHUTTLE_EMPTY_SKIP.ROUTE: + case STACKER_STALLED_STORE_SKIP.ROUTE: + case SHUTTLE_FULL_SKIP.ROUTE: + case STACKER_SHUTTLE_EMPTY_STORE_SKIP.ROUTE: return t('skip_to_next_step') default: console.error( @@ -96,6 +109,9 @@ export function SkipStepInfo(props: RecoveryContentProps): JSX.Element { case STACKER_STALLED_SKIP.ROUTE: case STACKER_HOPPER_EMPTY_SKIP.ROUTE: case STACKER_SHUTTLE_EMPTY_SKIP.ROUTE: + case STACKER_STALLED_STORE_SKIP.ROUTE: + case SHUTTLE_FULL_SKIP.ROUTE: + case STACKER_SHUTTLE_EMPTY_STORE_SKIP.ROUTE: return 'robot_not_attempt_to_move_lw' default: console.error( diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/StackerEnsureShuttleEmpty.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/StackerEnsureShuttleEmpty.tsx index 6e36713b75d..d5885d4d36e 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/StackerEnsureShuttleEmpty.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/StackerEnsureShuttleEmpty.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import stackerImage from '/app/assets/images/stacker_shuttle_empty.png' import { DescriptionContent, TwoColumn } from '/app/molecules/InterventionModal' +import { RECOVERY_MAP } from '../constants' import { RecoverySingleColumnContentWrapper } from './RecoveryContentWrapper' import { RecoveryFooterButtons } from './RecoveryFooterButtons' @@ -11,16 +12,24 @@ import type { RecoveryContentProps } from '../types' export function StackerEnsureShuttleEmpty( props: RecoveryContentProps ): JSX.Element | null { - const { routeUpdateActions } = props + const { routeUpdateActions, recoveryMap } = props + const { route } = recoveryMap const { proceedNextStep, goBackPrevStep } = routeUpdateActions const { t } = useTranslation('error_recovery') + const storeSkip = + route === RECOVERY_MAP.STACKER_STALLED_STORE_SKIP.ROUTE || + route === RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.ROUTE return ( Stacker shuttle empty diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx index 83c91f56a1f..86662a00d7d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx @@ -1,11 +1,6 @@ import { useTranslation } from 'react-i18next' -import { - COLORS, - Flex, - LabwareRender, - MoveLabwareOnDeck, -} from '@opentrons/components' +import { COLORS, Flex, MoveLabwareOnDeck } from '@opentrons/components' import { DeckMapContent, TwoColumn } from '/app/molecules/InterventionModal' @@ -119,7 +114,7 @@ export function TwoColLwInfoAndDeck( const { movedLabwareDef, moduleRenderInfo, - labwareRenderInfo, + labwareOnDeck, ...restUtils } = deckMapUtils @@ -140,6 +135,9 @@ export function TwoColLwInfoAndDeck( : null, } }) + const labwareOnDeckFiltered = labwareOnDeck?.filter( + lw => lw.labwareId !== failedLwId + ) return isValidDeck ? ( - {labwareRenderInfo - .filter(l => l.labwareId !== failedLwId) - .map(({ labwareOrigin, labwareDef, labwareId }) => ( - - - - ))} - - } + labwareOnDeck={labwareOnDeckFiltered} /> ) : ( diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx index a92597f7b25..4613c115328 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx @@ -15,7 +15,10 @@ import { LabwareMissingOnShuttleErrorBanner, NoLiquidDetectedBanner, OverpressureBanner, + StackerHopperOrShuttleEmptyErrorBanner, StackerShuttleMissingErrorBanner, + StackerShuttleOccupiedErrorBanner, + StackerShuttleStoreEmptyErrorBanner, StackerStallErrorBanner, StallErrorBanner, TipNotDetectedBanner, @@ -242,6 +245,34 @@ describe('renders the InlineNotification', () => { {} ) }) + it('renders the InlineNotification for StackerShuttleOccupiedErrorBanner', () => { + renderWithProviders(, { + i18nInstance: i18n, + }) + expect(vi.mocked(InlineNotification)).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'alert', + heading: 'The shuttle has labware when it should be empty', + message: + 'Remove the labware from the shuttle to complete the stacker retrieve step', + }), + {} + ) + }) + it('renders the InlineNotification for StackerHopperOrShuttleEmptyErrorBanner', () => { + renderWithProviders(, { + i18nInstance: i18n, + }) + expect(vi.mocked(InlineNotification)).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'alert', + heading: + 'Stacker errors occur when the stacker is empty when the robot expects the stacker to be filled, or when labware is stuck on the labware latch', + message: 'Troubleshoot the issue to complete the stacker retrieve step', + }), + {} + ) + }) it('renders the InlineNotification for LabwareMissingErrorBanner', () => { renderWithProviders(, { @@ -289,4 +320,20 @@ describe('renders the InlineNotification', () => { {} ) }) + + it('renders the InlineNotification for StackerShuttleStoreEmptyErrorBanner', () => { + renderWithProviders(, { + i18nInstance: i18n, + }) + expect(vi.mocked(InlineNotification)).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'alert', + heading: + 'Shuttle empty errors occur when the robot tries to store labware into a stacker from an empty shuttle', + message: + 'Load the shuttle with the correct labware to complete the stacker store step', + }), + {} + ) + }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx index fe675770a2b..d27f7cabc9f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx @@ -121,6 +121,66 @@ describe('LeftColumnLabwareInfo', () => { ) }) + it(`renders with correct props for ${RECOVERY_MAP.STACKER_STALLED_STORE_RETRY.STEPS.CHECK_HOPPER} step`, () => { + props.recoveryMap = { + route: RECOVERY_MAP.STACKER_STALLED_STORE_RETRY.ROUTE, + step: RECOVERY_MAP.STACKER_STALLED_STORE_RETRY.STEPS.CHECK_HOPPER, + } + props.failedLabwareUtils.labwareQuantity = 5 + render(props) + + expect(vi.mocked(InterventionContent)).toHaveBeenCalledWith( + expect.objectContaining({ + headline: 'MOCK_TITLE', + infoProps: { + layout: 'default', + type: 'location', + labwareName: 'MOCK_LW_NAME', + labwareNickname: 'MOCK_LW_NICKNAME', + currentLocationProps: { deckLabel: 'STACKER s' }, + newLocationProps: { deckLabel: 'SLOT B2' }, + subText: undefined, + tagText: 'Quantity: 4', + }, + notificationProps: { + type: 'alert', + heading: 'MOCK_BANNER_TEXT', + }, + }), + {} + ) + }) + + it(`renders with correct props for ${RECOVERY_MAP.STACKER_STALLED_STORE_RETRY.STEPS.CHECK_HOPPER} step`, () => { + props.recoveryMap = { + route: RECOVERY_MAP.STACKER_STALLED_STORE_SKIP.ROUTE, + step: RECOVERY_MAP.STACKER_STALLED_STORE_SKIP.STEPS.CHECK_HOPPER, + } + props.failedLabwareUtils.labwareQuantity = 5 + render(props) + + expect(vi.mocked(InterventionContent)).toHaveBeenCalledWith( + expect.objectContaining({ + headline: 'MOCK_TITLE', + infoProps: { + layout: 'default', + type: 'location', + labwareName: 'MOCK_LW_NAME', + labwareNickname: 'MOCK_LW_NICKNAME', + currentLocationProps: { deckLabel: 'STACKER s' }, + newLocationProps: { deckLabel: 'SLOT B2' }, + subText: undefined, + tagText: 'Quantity: 5', + }, + notificationProps: { + type: 'alert', + heading: 'MOCK_BANNER_TEXT', + }, + }), + {} + ) + }) + it('does not include notificationProps when bannerText is not provided', () => { props.bannerText = undefined render(props) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SkipStepInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SkipStepInfo.test.tsx index e3af4075e02..480f817a101 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SkipStepInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SkipStepInfo.test.tsx @@ -126,6 +126,7 @@ describe('SkipStepInfo', () => { RECOVERY_MAP.STACKER_HOPPER_EMPTY_SKIP.ROUTE, RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.ROUTE, RECOVERY_MAP.STACKER_STALLED_SKIP.ROUTE, + RECOVERY_MAP.SHUTTLE_FULL_SKIP.ROUTE, ])('calls manualRetreive when the route is %s', async route => { props.currentRecoveryOptionUtils.selectedRecoveryOption = route props.failedCommand = { @@ -155,6 +156,8 @@ describe('SkipStepInfo', () => { RECOVERY_MAP.STACKER_HOPPER_EMPTY_SKIP.ROUTE, RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.ROUTE, RECOVERY_MAP.STACKER_STALLED_SKIP.ROUTE, + RECOVERY_MAP.STACKER_STALLED_STORE_SKIP.ROUTE, + RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_STORE_SKIP.ROUTE, ])('calls manualStore when the route is %s', async route => { props.currentRecoveryOptionUtils.selectedRecoveryOption = route props.failedCommand = { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StackerHomeShuttle.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StackerHomeShuttle.test.tsx index e68b2f23b29..67033a95243 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StackerHomeShuttle.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StackerHomeShuttle.test.tsx @@ -40,7 +40,7 @@ describe('Render StackerHomeShuttle', () => { recoveryMap: { route: RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.ROUTE, step: - RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.STEPS + RECOVERY_MAP.STACKER_SHUTTLE_MISSING_RETRY.STEPS .PREPARE_TRACK_FOR_HOMING, }, } as any @@ -82,30 +82,12 @@ describe('Render StackerHomeShuttle', () => { }) it.each([ - { - route: RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.ROUTE, - step: - RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.STEPS.PREPARE_TRACK_FOR_HOMING, - }, - { - route: RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.ROUTE, - step: - RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_SKIP.STEPS.PREPARE_TRACK_FOR_HOMING, - }, { route: RECOVERY_MAP.STACKER_SHUTTLE_MISSING_RETRY.ROUTE, step: RECOVERY_MAP.STACKER_SHUTTLE_MISSING_RETRY.STEPS .PREPARE_TRACK_FOR_HOMING, }, - { - route: RECOVERY_MAP.STACKER_STALLED_RETRY.ROUTE, - step: RECOVERY_MAP.STACKER_STALLED_RETRY.STEPS.PREPARE_TRACK_FOR_HOMING, - }, - { - route: RECOVERY_MAP.STACKER_STALLED_SKIP.ROUTE, - step: RECOVERY_MAP.STACKER_STALLED_SKIP.STEPS.PREPARE_TRACK_FOR_HOMING, - }, ])(`renders correct title for route $route step $step`, ({ route, step }) => { props.recoveryMap = { route: route, diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TwoColLwInfoAndDeck.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TwoColLwInfoAndDeck.test.tsx index 81506a36b41..710991183b8 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TwoColLwInfoAndDeck.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TwoColLwInfoAndDeck.test.tsx @@ -66,6 +66,8 @@ describe('TwoColLwInfoAndDeck', () => { movedLabwareDef: {}, moduleRenderInfo: [], labwareRenderInfo: [], + modulesOnDeck: [], + labwareOnDeck: [], }, currentRecoveryOptionUtils: { selectedRecoveryOption: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, diff --git a/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts b/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts index 95436d2de70..18e3610420e 100644 --- a/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts +++ b/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts @@ -87,6 +87,16 @@ describe('getErrorKind', () => { errorType: DEFINED_ERROR_TYPES.STACKER_STALL, expectedError: ERROR_KINDS.STACKER_STALLED, }, + { + commandType: 'flexStacker/store', + errorType: DEFINED_ERROR_TYPES.STACKER_SHUTTLE_STORE_EMPTY, + expectedError: ERROR_KINDS.STACKER_SHUTTLE_STORE_EMPTY, + }, + { + commandType: 'flexStacker/store', + errorType: DEFINED_ERROR_TYPES.STACKER_SHUTTLE_OCCUPIED, + expectedError: ERROR_KINDS.STACKER_SHUTTLE_OCCUPIED, + }, { commandType: 'aspirate', errorType: DEFINED_ERROR_TYPES.OVERPRESSURE, diff --git a/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts b/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts index 9330b49860c..b1e69f53051 100644 --- a/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts +++ b/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts @@ -57,6 +57,8 @@ export function getErrorKind( return ERROR_KINDS.STACKER_SHUTTLE_MISSING case DEFINED_ERROR_TYPES.STACKER_SHUTTLE_EMPTY: return ERROR_KINDS.STACKER_HOPPER_OR_SHUTTLE_EMPTY + case DEFINED_ERROR_TYPES.STACKER_SHUTTLE_STORE_EMPTY: + return ERROR_KINDS.STACKER_SHUTTLE_STORE_EMPTY case DEFINED_ERROR_TYPES.STACKER_SHUTTLE_OCCUPIED: return ERROR_KINDS.STACKER_SHUTTLE_OCCUPIED default: { diff --git a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx index 28afb7e2b44..2551975b6dc 100644 --- a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx +++ b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx @@ -7,7 +7,6 @@ import { Flex, getLabwareDisplayLocation, getLoadedLabware, - LabwareRender, MoveLabwareOnDeck, SPACING, } from '@opentrons/components' @@ -19,11 +18,8 @@ import { import { InterventionInfo } from '/app/molecules/InterventionModal/InterventionContent' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' -import { - getLabwareNameFromRunData, - getRunLabwareRenderInfo, - getRunModuleRenderInfo, -} from './utils' +import { getRunCurrentLabwareInfo } from '../ErrorRecoveryFlows/hooks/useDeckMapUtils' +import { getLabwareNameFromRunData, getRunModuleRenderInfo } from './utils' import type { RunData } from '@opentrons/api-client' import type { @@ -54,11 +50,19 @@ export function MoveLabwareInterventionContent({ const deckDef = getDeckDefFromRobotType(robotType) const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] - const labwareRenderInfo = getRunLabwareRenderInfo( - run, - labwareDefsByUri, - deckDef - ) + const runCurrentLabwareInfo = getRunCurrentLabwareInfo({ + runData: run, + runLwDefsByUri: labwareDefsByUri, + }) + + const labwareOnDeck = runCurrentLabwareInfo + .filter(lw => lw.labwareId !== command.params.labwareId) + .map(lw => ({ + labwareLocation: lw.labwareLocation, + definition: lw.labwareDef, + labwareId: lw.labwareId, + })) + const moduleRenderInfo = getRunModuleRenderInfo( run, deckDef, @@ -146,23 +150,7 @@ export function MoveLabwareInterventionContent({ loadedLabware={run.labware} deckConfig={deckConfig} modulesOnDeck={modulesOnDeck} - backgroundItems={ - <> - {labwareRenderInfo - .filter(l => l.labwareId !== command.params.labwareId) - .map(({ labwareOrigin, labwareDef, labwareId }) => ( - - - - ))} - - } + labwareOnDeck={labwareOnDeck} /> diff --git a/app/src/organisms/InterventionModal/__tests__/utils.test.ts b/app/src/organisms/InterventionModal/__tests__/utils.test.ts index 40a62f44504..4c6070de38d 100644 --- a/app/src/organisms/InterventionModal/__tests__/utils.test.ts +++ b/app/src/organisms/InterventionModal/__tests__/utils.test.ts @@ -1,14 +1,9 @@ import deepClone from 'lodash/cloneDeep' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' -import { - getSchema2Dimensions, - getSlotHasMatingSurfaceUnitVector, - ot2DeckDefV5, -} from '@opentrons/shared-data' +import { ot2DeckDefV5 } from '@opentrons/shared-data' import { - mockLabwareDefinition, mockLabwareDefinitionsByUri, mockLabwareOnSlot, mockModule, @@ -19,7 +14,6 @@ import { getLabwareNameFromRunData, getModuleDisplayLocationFromRunData, getModuleModelFromRunData, - getRunLabwareRenderInfo, getRunModuleRenderInfo, } from '../utils' @@ -124,82 +118,6 @@ describe('getModuleModelFromRunData', () => { }) }) -describe('getRunLabwareRenderInfo', () => { - beforeEach(() => { - vi.mocked(getSlotHasMatingSurfaceUnitVector).mockReturnValue(true) - }) - - it('returns an empty array if there is no loaded labware for the run', () => { - const res = getRunLabwareRenderInfo({ labware: [] } as any, {}, {} as any) - - expect(res).toBeInstanceOf(Array) - expect(res).toHaveLength(0) - }) - - it('returns run labware render info', () => { - const res = getRunLabwareRenderInfo( - mockRunData, - mockLabwareDefinitionsByUri, - ot2DeckDefV5 as any - ) - const labwareInfo = res[0] - expect(labwareInfo).toBeTruthy() - expect(labwareInfo.labwareOrigin).toStrictEqual({ x: 0, y: 0, z: 0 }) // taken from deckDef fixture - expect(labwareInfo.labwareDef.metadata.displayName).toEqual( - 'NEST 96 Well Plate 100 µL PCR Full Skirt' - ) - expect(labwareInfo.labwareId).toEqual('mockLabwareID2') - }) - - it('does not add labware to results array if the labware is on deck and the slot does not have a mating surface vector', () => { - vi.mocked(getSlotHasMatingSurfaceUnitVector).mockReturnValue(false) - const res = getRunLabwareRenderInfo( - mockRunData, - mockLabwareDefinitionsByUri, - ot2DeckDefV5 as any - ) - expect(res).toHaveLength(1) // the offdeck labware still gets added because the mating surface doesn't exist for offdeck labware - }) - - it('does add offdeck labware to the results array', () => { - const res = getRunLabwareRenderInfo( - mockRunData, - mockLabwareDefinitionsByUri, - ot2DeckDefV5 as any - ) - expect(res).toHaveLength(2) - const labwareInfo = res.find( - labware => labware.labwareId === 'mockLabwareID3' - ) - expect(labwareInfo).toBeTruthy() - expect(labwareInfo?.labwareOrigin).toStrictEqual({ - x: 0, - y: - ot2DeckDefV5.cornerOffsetFromOrigin[1] - - getSchema2Dimensions(mockLabwareDefinition).yDimension, - z: 0, - }) - }) - - it('omits labware if slot position not found in deck definition', () => { - const mockBadSlotLabware = { - id: 'mockBadLabwareID', - loadName: 'nest_96_wellplate_100ul_pcr_full_skirt', - definitionUri: 'opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1', - location: { - slotName: '0', - }, - } - const res = getRunLabwareRenderInfo( - { labware: [mockBadSlotLabware] } as any, - mockLabwareDefinitionsByUri, - ot2DeckDefV5 as any - ) - - expect(res).toHaveLength(0) - }) -}) - describe('getRunModuleRenderInfo', () => { it('returns an empty array if there is no loaded module for the run', () => { const res = getRunModuleRenderInfo({ modules: [] } as any, {} as any, {}) diff --git a/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts b/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts deleted file mode 100644 index a2b5d2effe1..00000000000 --- a/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - coordinateTupleToVector3D, - getAddressableAreaFromSlotId, - getDeckSlotOriginToLabwareOrigin, - getLabwareBackLeftBottomToOrigin, - getPositionFromSlotId, - getSlotHasMatingSurfaceUnitVector, - getVectorSum, -} from '@opentrons/shared-data' - -import type { RunData } from '@opentrons/api-client' -import type { - DeckDefinition, - LabwareDefinition, - LabwareDefinitionsByUri, - Vector3D, -} from '@opentrons/shared-data' - -export interface RunLabwareInfo { - /** - * The labware origin, in deck coordinates. - * Use this with ``. - */ - labwareOrigin: Vector3D - - labwareDef: LabwareDefinition - labwareId: string -} - -export function getRunLabwareRenderInfo( - runData: RunData, - labwareDefs: LabwareDefinitionsByUri, - deckDef: DeckDefinition -): RunLabwareInfo[] { - if (runData.labware.length > 0) { - return runData.labware.reduce((acc, labware) => { - const location = labware.location - if ( - (typeof location === 'object' && 'moduleId' in location) || - (typeof location === 'object' && 'labwareId' in location) || - labware.id === 'fixedTrash' - ) { - return acc - } - - const labwareDef = labwareDefs[labware.definitionUri] - if (labwareDef == null) { - return acc - } - - if (location !== 'offDeck' && location !== 'systemLocation') { - const slotName = - 'addressableAreaName' in location - ? location.addressableAreaName - : location.slotName - - const slotAddressableArea = getAddressableAreaFromSlotId( - slotName, - deckDef - ) - if (slotAddressableArea == null) { - return acc - } - - const slotHasMatingSurfaceVector = getSlotHasMatingSurfaceUnitVector( - deckDef, - slotName - ) - if (!slotHasMatingSurfaceVector) { - // todo(mm, 2025-06-25): Are we using slotHasMatingSurfaceVector as an - // approximation for "is a deck slot"? Should we use - // `slotAddressableArea.areaType === "slot"`` instead? - return acc - } - - // 0,0,0 default inherited from prior code. I don't think we ever reach it in - // practice, just keeping it to be safe. - const slotOriginTuple = getPositionFromSlotId(slotName, deckDef) ?? [ - 0, - 0, - 0, - ] - const slotOrigin = coordinateTupleToVector3D(slotOriginTuple) - - const slotOriginToLabwareOrigin = getDeckSlotOriginToLabwareOrigin( - slotAddressableArea, - labwareDef - ) - const labwareOrigin = getVectorSum( - slotOrigin, - slotOriginToLabwareOrigin - ) - - return [ - ...acc, - { - labwareOrigin, - labwareId: labware.id, - labwareDef, - }, - ] - } else { - // Place off-deck labware literally beyond the bounds of the deck. - // Calling code can change the position for animated fly-ins/fly-outs. - // It's unclear to me whether it matters exactly what position we choose here; - // this roughly maintains prior behavior. - const whereToPutLabwareBackLeftBottom: Vector3D = { - x: 0, - y: deckDef.cornerOffsetFromOrigin[1], - z: 0, - } - - const labwareOrigin = getVectorSum( - whereToPutLabwareBackLeftBottom, - getLabwareBackLeftBottomToOrigin(labwareDef) - ) - - return [ - ...acc, - { - labwareOrigin, - labwareId: labware.id, - labwareDef, - }, - ] - } - }, []) - } - return [] -} diff --git a/app/src/organisms/InterventionModal/utils/index.ts b/app/src/organisms/InterventionModal/utils/index.ts index 5b72cc9fac7..7cad1721469 100644 --- a/app/src/organisms/InterventionModal/utils/index.ts +++ b/app/src/organisms/InterventionModal/utils/index.ts @@ -1,4 +1,3 @@ -export * from './getRunLabwareRenderInfo' export * from './getRunModuleRenderInfo' export * from './getLabwareNameFromRunData' export * from './getModuleDisplayLocationFromRunData' diff --git a/app/src/organisms/LegacyLabwarePositionCheck/PrepareSpace.tsx b/app/src/organisms/LegacyLabwarePositionCheck/PrepareSpace.tsx index ed3a0fc03d1..7fdfd4c370b 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/PrepareSpace.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/PrepareSpace.tsx @@ -20,6 +20,7 @@ import { import { getModuleType, THERMOCYCLER_MODULE_TYPE } from '@opentrons/shared-data' import { SmallButton } from '/app/atoms/buttons' +import { getStandardDeckViewLayerBlockList } from '/app/local-resources/deck_configuration/getStandardDeckViewLayerBlockList' import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { getIsOnDevice } from '/app/redux/config' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' @@ -104,6 +105,7 @@ export const PrepareSpace = (props: PrepareSpaceProps): JSX.Element | null => { > ({ moduleModel: mod.model, moduleLocation: mod.location, diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/LabwareMapView.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/LabwareMapView.tsx index 908171da189..97553ea6c5b 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/LabwareMapView.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/LabwareMapView.tsx @@ -56,7 +56,8 @@ export function LabwareMapView(props: LabwareMapViewProps): JSX.Element { ? getWellFillFromLabwareId( topLabwareInfo.labwareId, mostRecentAnalysis?.liquids ?? [], - labwareByLiquidId + labwareByLiquidId, + mostRecentAnalysis?.commands ?? [] ) : undefined return { @@ -94,7 +95,8 @@ export function LabwareMapView(props: LabwareMapViewProps): JSX.Element { const wellFill = getWellFillFromLabwareId( topLabwareInfo.labwareId, mostRecentAnalysis?.liquids ?? [], - labwareByLiquidId + labwareByLiquidId, + mostRecentAnalysis?.commands ?? [] ) return { diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/SetupLabwareStackView.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/SetupLabwareStackView.tsx index d277190e23e..d75f08162ed 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/SetupLabwareStackView.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/SetupLabwareStackView.tsx @@ -86,8 +86,9 @@ export function SetupLabwareStackView({ const wellFill = getWellFillFromLabwareId( selectedLabware.labwareId, - mostRecentAnalysis?.liquids ?? [], - labwareByLiquidId + mostRecentAnalysis.liquids, + labwareByLiquidId, + mostRecentAnalysis.commands ) const hasLiquids = Object.keys(wellFill).length > 0 const labwareDefinition = diff --git a/app/src/organisms/ODD/QuickTransferFlow/Aspirate/hooks/useAspirateSettingsConfig.ts b/app/src/organisms/ODD/QuickTransferFlow/Aspirate/hooks/useAspirateSettingsConfig.ts index 8b846a2e6c9..601822d9088 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/Aspirate/hooks/useAspirateSettingsConfig.ts +++ b/app/src/organisms/ODD/QuickTransferFlow/Aspirate/hooks/useAspirateSettingsConfig.ts @@ -1,5 +1,7 @@ import { useTranslation } from 'react-i18next' +import { POSITION_REFERENCE_TOP } from '@opentrons/shared-data' + import { useToaster } from '/app/organisms/ToasterOven' import { ASPIRATE_SETTING_OPTIONS as SETTING_OPTIONS } from '../../constants' @@ -31,7 +33,6 @@ export function useAspirateSettingsConfig({ const { makeSnackbar } = useToaster() const touchTipEnabled = getIsTouchTipEnabled(state.source) - const hasLiquidClass = state.liquidClassName !== 'none' const aspirateSettingsItems: SettingItem[] = [ { @@ -65,7 +66,12 @@ export function useAspirateSettingsConfig({ ? t('submerge_value', { speed: state.submergeAspirate.speed, delayDuration: state.submergeAspirate.delayDuration, - position: state.submergeAspirate.positionFromBottom, + position: state.submergeAspirate.position, + positionReference: + state.submergeAspirate.positionReference === + POSITION_REFERENCE_TOP + ? t('top') + : t('bottom'), }) : t('option_disabled'), enabled: true, @@ -86,7 +92,7 @@ export function useAspirateSettingsConfig({ option: SETTING_OPTIONS.ASPIRATE_MIX, copy: t('mix'), value: - state.mixOnAspirate !== undefined && hasLiquidClass + state.mixOnAspirate !== undefined ? t('mix_value', { volume: state.mixOnAspirate?.mixVolume, reps: state.mixOnAspirate?.repetitions, @@ -110,7 +116,8 @@ export function useAspirateSettingsConfig({ option: SETTING_OPTIONS.ASPIRATE_CONDITION, copy: t('condition'), value: - state.conditionAspirate != null || state.conditionAspirate !== 0 + (state.conditionAspirate != null || state.conditionAspirate !== 0) && + isMultiTransfer ? t('volume', { volume: state.conditionAspirate }) : t('option_disabled'), enabled: isMultiTransfer, @@ -122,11 +129,11 @@ export function useAspirateSettingsConfig({ option: SETTING_OPTIONS.ASPIRATE_DELAY, copy: t('delay'), value: - state.delayAspirate !== undefined && hasLiquidClass + state.delayAspirate != null ? t('delay_value', { delay: state.delayAspirate.delayDuration, }) - : '', + : t('option_disabled'), enabled: true, onClick: () => { setSelectedSetting(SETTING_OPTIONS.ASPIRATE_DELAY) @@ -140,7 +147,12 @@ export function useAspirateSettingsConfig({ ? t('retract_value', { speed: state.retractAspirate.speed, delayDuration: state.retractAspirate.delayDuration, - position: state.retractAspirate.positionFromBottom, + position: state.retractAspirate.position, + positionReference: + state.retractAspirate.positionReference === + POSITION_REFERENCE_TOP + ? t('top') + : t('bottom'), }) : '', enabled: true, @@ -171,7 +183,7 @@ export function useAspirateSettingsConfig({ option: SETTING_OPTIONS.ASPIRATE_AIR_GAP, copy: t('air_gap'), value: - state.airGapAspirate !== undefined && hasLiquidClass + state.airGapAspirate !== undefined ? t('air_gap_value', { volume: state.airGapAspirate }) : t('option_disabled'), enabled: true, diff --git a/app/src/organisms/ODD/QuickTransferFlow/Dispense/hooks/useDispenseSettingsConfig.ts b/app/src/organisms/ODD/QuickTransferFlow/Dispense/hooks/useDispenseSettingsConfig.ts index 65deddc735f..95e7328316b 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/Dispense/hooks/useDispenseSettingsConfig.ts +++ b/app/src/organisms/ODD/QuickTransferFlow/Dispense/hooks/useDispenseSettingsConfig.ts @@ -1,6 +1,7 @@ import { useTranslation } from 'react-i18next' import { + POSITION_REFERENCE_TOP, TRASH_BIN_ADAPTER_FIXTURE, WASTE_CHUTE_FIXTURES, } from '@opentrons/shared-data' @@ -31,7 +32,6 @@ export function useDispenseSettingsConfig({ }: UseDispenseSettingsConfigProps): SettingItem[] { const { t, i18n } = useTranslation(['quick_transfer', 'shared']) const { makeSnackbar } = useToaster() - const getBlowoutValueCopy = (): string | undefined => { if (state.blowOutDispense == null) { return t('option_disabled') @@ -58,21 +58,20 @@ export function useDispenseSettingsConfig({ const touchTipEnabled = getIsTouchTipEnabled(state.destination) const hasLiquidClass = state.liquidClassName !== 'none' - const dispenseSettingsItems = [ { - option: 'dispense_flow_rate', + option: SETTING_OPTIONS.DISPENSE_FLOW_RATE, copy: t('dispense_flow_rate'), value: t('flow_rate_value', { flow_rate: state.dispenseFlowRate.toFixed(DIGIT), }), enabled: true, onClick: () => { - setSelectedSetting('dispense_flow_rate') + setSelectedSetting(SETTING_OPTIONS.DISPENSE_FLOW_RATE) }, }, { - option: 'dispense_tip_position', + option: SETTING_OPTIONS.DISPENSE_TIP_POSITION, copy: t('tip_position'), value: state.tipPositionDispense !== undefined @@ -80,18 +79,23 @@ export function useDispenseSettingsConfig({ : t('option_disabled'), enabled: true, onClick: () => { - setSelectedSetting('dispense_tip_position') + setSelectedSetting(SETTING_OPTIONS.DISPENSE_TIP_POSITION) }, }, { - option: 'dispense_submerge', + option: SETTING_OPTIONS.DISPENSE_SUBMERGE, copy: t('submerge'), value: state.submergeDispense !== undefined ? t('submerge_value', { speed: state.submergeDispense.speed, delayDuration: state.submergeDispense.delayDuration, - position: state.submergeDispense.positionFromBottom, + position: state.submergeDispense.position, + positionReference: + state.submergeDispense.positionReference === + POSITION_REFERENCE_TOP + ? t('top') + : t('bottom'), }) : t('option_disabled'), enabled: true, @@ -100,21 +104,21 @@ export function useDispenseSettingsConfig({ }, }, { - option: 'dispense_delay', + option: SETTING_OPTIONS.DISPENSE_DELAY, copy: t('delay'), value: - state.delayDispense !== undefined + state.delayDispense != null ? t('delay_value', { delay: state.delayDispense.delayDuration, }) - : '', + : t('option_disabled'), enabled: true, onClick: () => { - setSelectedSetting('dispense_delay') + setSelectedSetting(SETTING_OPTIONS.DISPENSE_DELAY) }, }, { - option: 'dispense_mix', + option: SETTING_OPTIONS.DISPENSE_MIX, copy: t('mix'), value: state.mixOnDispense !== undefined @@ -131,14 +135,14 @@ export function useDispenseSettingsConfig({ state.transferType === 'transfer' || state.transferType === 'consolidate' ) { - setSelectedSetting('dispense_mix') + setSelectedSetting(SETTING_OPTIONS.DISPENSE_MIX) } else { makeSnackbar(t('dispense_setting_disabled') as string) } }, }, { - option: 'dispense_push_out', + option: SETTING_OPTIONS.DISPENSE_PUSH_OUT, copy: t('push_out'), value: state.pushOutDispense != null && state.pushOutDispense.volume != null @@ -146,30 +150,35 @@ export function useDispenseSettingsConfig({ : t('option_disabled'), enabled: true, onClick: () => { - setSelectedSetting('dispense_push_out') + setSelectedSetting(SETTING_OPTIONS.DISPENSE_PUSH_OUT) }, }, { - option: 'dispense_retract', + option: SETTING_OPTIONS.DISPENSE_RETRACT, copy: t('retract'), value: state.retractDispense !== undefined ? t('retract_value', { speed: state.retractDispense.speed, delayDuration: state.retractDispense.delayDuration, - position: state.retractDispense.positionFromBottom, + position: state.retractDispense.position, + positionReference: + state.retractDispense.positionReference === + POSITION_REFERENCE_TOP + ? t('top') + : t('bottom'), }) : t('option_disabled'), enabled: true, onClick: () => { - setSelectedSetting('dispense_retract') + setSelectedSetting(SETTING_OPTIONS.DISPENSE_RETRACT) }, }, { - option: 'dispense_blow_out', + option: SETTING_OPTIONS.DISPENSE_BLOW_OUT, copy: t('blow_out'), value: - state.transferType === 'distribute' && hasLiquidClass + state.transferType === 'distribute' ? t('disabled') : i18n.format(getBlowoutValueCopy(), 'capitalize'), enabled: state.transferType !== 'distribute', @@ -177,15 +186,15 @@ export function useDispenseSettingsConfig({ if (state.transferType === 'distribute') { makeSnackbar(t('dispense_setting_disabled') as string) } else { - setSelectedSetting('dispense_blow_out') + setSelectedSetting(SETTING_OPTIONS.DISPENSE_BLOW_OUT) } }, }, { - option: 'dispense_disposal_volume', + option: SETTING_OPTIONS.DISPENSE_DISPOSAL_VOLUME, copy: t('disposal_volume'), value: - state.disposalVolumeDispenseSettings != null + state.disposalVolumeDispenseSettings != null && isMultiTransfer ? t('disposal_volume_label', { volume: state.disposalVolumeDispenseSettings.volume, location: @@ -199,14 +208,14 @@ export function useDispenseSettingsConfig({ enabled: isMultiTransfer, onClick: () => { if (isMultiTransfer) { - setSelectedSetting('dispense_disposal_volume') + setSelectedSetting(SETTING_OPTIONS.DISPENSE_DISPOSAL_VOLUME) } else { makeSnackbar(t('dispense_setting_disabled') as string) } }, }, { - option: 'dispense_touch_tip', + option: SETTING_OPTIONS.DISPENSE_TOUCH_TIP, copy: t('touch_tip'), value: state.touchTipDispense !== undefined && @@ -220,14 +229,14 @@ export function useDispenseSettingsConfig({ enabled: touchTipEnabled, onClick: () => { if (touchTipEnabled) { - setSelectedSetting('dispense_touch_tip') + setSelectedSetting(SETTING_OPTIONS.DISPENSE_TOUCH_TIP) } else { makeSnackbar(t('dispense_setting_disabled') as string) } }, }, { - option: 'dispense_air_gap', + option: SETTING_OPTIONS.DISPENSE_AIR_GAP, copy: t('air_gap'), value: state.airGapDispense !== undefined @@ -235,7 +244,7 @@ export function useDispenseSettingsConfig({ : t('option_disabled'), enabled: true, onClick: () => { - setSelectedSetting('dispense_air_gap') + setSelectedSetting(SETTING_OPTIONS.DISPENSE_AIR_GAP) }, }, ] diff --git a/app/src/organisms/ODD/QuickTransferFlow/Overview.tsx b/app/src/organisms/ODD/QuickTransferFlow/Overview.tsx index c06edbc70e8..f034873ab0c 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/Overview.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/Overview.tsx @@ -111,7 +111,7 @@ export function Overview(props: OverviewProps): JSX.Element | null { diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx index 96dd5cc4ff0..71c36faa7bb 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx @@ -121,11 +121,10 @@ export function BlowOut(props: BlowOutProps): JSX.Element { const [currentStep, setCurrentStep] = useState(1) const [blowOutLocation, setBlowOutLocation] = useState< BlowOutLocation | undefined - >(state.blowOutDispense?.location as BlowOutLocation | undefined) + >(state.blowOutDispense?.location ?? undefined) const [speed, setSpeed] = useState( (state.blowOutDispense?.flowRate as number) ?? null ) - const enableBlowOutDisplayItems = [ { option: true, @@ -142,12 +141,10 @@ export function BlowOut(props: BlowOutProps): JSX.Element { }, }, ] - const blowOutLocationItems = useBlowOutLocationOptions( deckConfig, state.transferType ) - const handleClickBackOrExit = (): void => { currentStep > 1 ? setCurrentStep(currentStep - 1) : onBack() } @@ -244,7 +241,7 @@ export function BlowOut(props: BlowOutProps): JSX.Element { numAspirateWells: state.sourceWells.length, numDispenseWells: state.destinationWells.length, aspirateAirGapByVolume: - (retract?.airGapByVolume as Array<[number, number]>) ?? null, + (retract?.airGapByVolume as Array<[number, number]>) ?? [], conditioningByVolume: (correctionByVolume as Array<[number, number]>) ?? null, disposalByVolume: null, // note always null because blowout is available only for single dispense diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Retract.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Retract.tsx index 91186e6f28b..d45522372b5 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Retract.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Retract.tsx @@ -12,6 +12,7 @@ import { SPACING, StyledText, } from '@opentrons/components' +import { POSITION_REFERENCE_TOP } from '@opentrons/shared-data' import { getTopPortalEl } from '/app/App/portal' import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' @@ -23,6 +24,7 @@ import { ACTIONS } from '../constants' import type { Dispatch } from 'react' import type { KeyboardReactInterface } from 'react-simple-keyboard' +import type { PositionReference } from '@opentrons/shared-data' import type { FlowRateKind, QuickTransferSummaryAction, @@ -53,9 +55,13 @@ export function Retract({ const [delayDuration, setDelayDuration] = useState( retractSettings?.delayDuration ?? null ) - const [position, setPosition] = useState( - retractSettings?.positionFromBottom ?? null + const [position, setPosition] = useState( + String(retractSettings?.position) ?? null ) + const positionReference = + kind === 'aspirate' + ? state.retractAspirate?.positionReference + : state.retractDispense?.positionReference const action = kind === 'aspirate' @@ -81,7 +87,8 @@ export function Retract({ retractSettings: { speed, delayDuration, - positionFromBottom: position, + position: Number(position), + positionReference: positionReference ?? undefined, }, }) trackEventWithRobotSerial({ @@ -108,7 +115,7 @@ export function Retract({ if (delayDuration == null && currentStep === 2) { buttonIsDisabled = true } - if (position == null && currentStep === 3) { + if ((position == null || isNaN(Number(position))) && currentStep === 3) { buttonIsDisabled = true } @@ -145,6 +152,7 @@ export function Retract({ position={position} setPosition={setPosition} currentStep={currentStep} + positionReference={positionReference} /> , @@ -156,12 +164,13 @@ interface RetractSettingComponentProps { kind: FlowRateKind state: QuickTransferSummaryState setSpeed: (speed: number | null) => void - setPosition: (position: number | null) => void + setPosition: (position: string | null) => void delayDuration: number | null setDelayDuration: (delayDuration: number | null) => void speed: number | null - position: number | null + position: string | null currentStep: number + positionReference?: PositionReference } function RetractSettingComponent({ @@ -174,10 +183,17 @@ function RetractSettingComponent({ position, setPosition, currentStep, + positionReference, }: RetractSettingComponentProps): JSX.Element { const { t } = useTranslation(['quick_transfer']) const keyboardRef = useRef(null) + // TODO: accommodate arbitrary position reference + const positionText = + positionReference === POSITION_REFERENCE_TOP + ? t('distance_top_of_well_mm') + : t('distance_bottom_of_well_mm') + let wellHeight = 1 if ( kind === 'aspirate' && @@ -204,10 +220,20 @@ function RetractSettingComponent({ ) ) } - const positionRange = { min: 1, max: Math.floor(wellHeight * 2) } + const positionRange = + positionReference === POSITION_REFERENCE_TOP + ? { + min: -wellHeight, + max: 2, + } + : { + min: 0, + max: wellHeight + 2, + } const positionError = position != null && - (position < positionRange.min || position > positionRange.max) + (Number(position) < positionRange.min || + Number(position) > positionRange.max) ? t(`value_out_of_range`, { min: positionRange.min, max: positionRange.max, @@ -236,8 +262,7 @@ function RetractSettingComponent({ if (userInput === '') { setPosition(null) } else { - const parsedValue = Number(userInput) - setPosition(!isNaN(parsedValue) ? parsedValue : null) + setPosition(userInput) } } @@ -311,6 +336,10 @@ function RetractSettingComponent({ } const positionSetting = (): JSX.Element => { + const caption = + positionReference === POSITION_REFERENCE_TOP + ? t('from_top', { min: -wellHeight }) + : t('from_bottom', { max: wellHeight + 2 }) return ( <> {positionError == null ? ( - {t('from_bottom', { max: positionRange.max })} + {caption} ) : null} @@ -343,6 +372,7 @@ function RetractSettingComponent({ keyboardRef={keyboardRef} initialValue={String(position ?? '')} onChange={handlePositionChange} + hasHyphen={positionReference === POSITION_REFERENCE_TOP} /> diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Submerge.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Submerge.tsx index 48ec28c23c0..20c140e166f 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Submerge.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Submerge.tsx @@ -12,6 +12,7 @@ import { SPACING, StyledText, } from '@opentrons/components' +import { POSITION_REFERENCE_TOP } from '@opentrons/shared-data' import { getTopPortalEl } from '/app/App/portal' import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' @@ -23,6 +24,7 @@ import { ACTIONS } from '../constants' import type { Dispatch } from 'react' import type { KeyboardReactInterface } from 'react-simple-keyboard' +import type { PositionReference } from '@opentrons/shared-data' import type { FlowRateKind, QuickTransferSummaryAction, @@ -53,9 +55,13 @@ export function Submerge({ const [delayDuration, setDelayDuration] = useState( submergeSettings?.delayDuration ?? null ) - const [position, setPosition] = useState( - submergeSettings?.positionFromBottom ?? null + const [position, setPosition] = useState( + String(submergeSettings?.position) ?? null ) + const positionReference = + kind === 'aspirate' + ? state.submergeAspirate?.positionReference + : state.submergeDispense?.positionReference const action = kind === 'aspirate' @@ -81,7 +87,8 @@ export function Submerge({ submergeSettings: { speed, delayDuration, - positionFromBottom: position, + position: Number(position), + positionReference: positionReference ?? undefined, }, }) trackEventWithRobotSerial({ @@ -108,7 +115,7 @@ export function Submerge({ if (delayDuration == null && currentStep === 2) { buttonIsDisabled = true } - if (position == null && currentStep === 3) { + if ((position == null || isNaN(Number(position))) && currentStep === 3) { buttonIsDisabled = true } @@ -145,6 +152,7 @@ export function Submerge({ speed={speed} position={position} currentStep={currentStep} + positionReference={positionReference} /> , @@ -156,12 +164,13 @@ interface SubmergeSettingComponentProps { kind: FlowRateKind state: QuickTransferSummaryState setSpeed: (speed: number | null) => void - setPosition: (position: number | null) => void + setPosition: (position: string | null) => void delayDuration: number | null setDelayDuration: (delayDuration: number | null) => void speed: number | null - position: number | null + position: string | null currentStep: number + positionReference?: PositionReference } function SubmergeSettingComponent({ @@ -174,10 +183,17 @@ function SubmergeSettingComponent({ position, setPosition, currentStep, + positionReference, }: SubmergeSettingComponentProps): JSX.Element { const { t } = useTranslation(['quick_transfer']) const keyboardRef = useRef(null) + // TODO: accommodate arbitrary position reference + const positionText = + positionReference === POSITION_REFERENCE_TOP + ? t('distance_top_of_well_mm') + : t('distance_bottom_of_well_mm') + let wellHeight = 1 if ( kind === 'aspirate' && @@ -204,10 +220,22 @@ function SubmergeSettingComponent({ ) ) } - const positionRange = { min: 1, max: Math.floor(wellHeight * 2) } + const positionRange = + positionReference === POSITION_REFERENCE_TOP + ? { + min: -wellHeight, + max: 2, + } + : { + min: 0, + max: wellHeight + 2, + } + + console.log(positionRange) const positionError = position != null && - (position < positionRange.min || position > positionRange.max) + (Number(position) < positionRange.min || + Number(position) > positionRange.max) ? t(`value_out_of_range`, { min: positionRange.min, max: positionRange.max, @@ -236,8 +264,7 @@ function SubmergeSettingComponent({ if (userInput === '') { setPosition(null) } else { - const parsedValue = Number(userInput) - setPosition(!isNaN(parsedValue) ? parsedValue : null) + setPosition(userInput) } } @@ -310,6 +337,10 @@ function SubmergeSettingComponent({ } const positionSetting = (): JSX.Element => { + const caption = + positionReference === POSITION_REFERENCE_TOP + ? t('from_top', { min: -wellHeight }) + : t('from_bottom', { max: wellHeight + 2 }) return ( <> {positionError == null ? ( - {t('from_bottom', { max: positionRange.max })} + {caption} ) : null} @@ -343,6 +374,7 @@ function SubmergeSettingComponent({ keyboardRef={keyboardRef} initialValue={String(position ?? '')} onChange={handlePositionChange} + hasHyphen={positionReference === POSITION_REFERENCE_TOP} /> diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx index f44718066cc..f9343739c64 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx @@ -63,8 +63,9 @@ export function TipPositionEntry(props: TipPositionEntryProps): JSX.Element { ) } - // the maxiumum allowed position is 2x the height of the well - const tipPositionRange = { min: 1, max: Math.floor(wellHeight * 2) } // TODO: set this based on range + // the maxiumum allowed position is 2mm above the height of the well + // this currently assumes bottom position reference + const tipPositionRange = { min: 1, max: Math.floor(wellHeight + 2) } // TODO: set this based on range const textEntryCopy: string = t('distance_bottom_of_well_mm') const tipPositionAction = diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/__tests__/ResetAdvancedSettingsModal.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/__tests__/ResetAdvancedSettingsModal.test.tsx index a6c49a6adad..83514b35ace 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/__tests__/ResetAdvancedSettingsModal.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/__tests__/ResetAdvancedSettingsModal.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { WATER_LIQUID_CLASS_NAME } from '@opentrons/shared-data' +import { WATER_LIQUID_CLASS_NAME_V2 } from '@opentrons/shared-data' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' @@ -27,7 +27,7 @@ describe('ResetAdvancedSettingsModal', () => { kind: 'aspirate', state: { ...QuickTransferState, - liquidClassName: WATER_LIQUID_CLASS_NAME, + liquidClassName: WATER_LIQUID_CLASS_NAME_V2, } as any, dispatch: vi.fn(), onClose: vi.fn(), diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/__tests__/Retract.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/__tests__/Retract.test.tsx index 8c05720ac95..d74d0030fbd 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/__tests__/Retract.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/__tests__/Retract.test.tsx @@ -80,7 +80,12 @@ describe('Retract', () => { }) it('renders test, buttons, input field, and keyboard for retract after aspirating - position', () => { - render(props) + render({ + ...props, + state: { + retractAspirate: { positionReference: 'well-bottom', position: 0 }, + } as any, + }) fireEvent.click(screen.getByRole('button', { name: '1' })) fireEvent.click(screen.getByRole('button', { name: '1' })) fireEvent.click(screen.getByText('Continue')) @@ -90,14 +95,20 @@ describe('Retract', () => { fireEvent.click(screen.getByRole('button', { name: '6' })) fireEvent.click(screen.getByText('Continue')) screen.getByText('Distance from bottom of well (mm)') - screen.getByText('Between 0 and 2 mm') + screen.getByText('Between 0 and 3 mm') fireEvent.click(screen.getByRole('button', { name: '2' })) fireEvent.click(screen.getByRole('button', { name: '2' })) fireEvent.click(screen.getByRole('button', { name: '2' })) - screen.getByText('Value must be between 1 to 2') + screen.getByText('Value must be between 0 to 3') screen.getByText('Save') }) it('calls dispatch with correct action and settings when save is clicked', () => { + props.state.retractAspirate = { + speed: 0, + delayDuration: 0, + position: 0, + positionReference: 'well-bottom', + } render(props) fireEvent.click(screen.getByRole('button', { name: '1' })) fireEvent.click(screen.getByRole('button', { name: '1' })) @@ -114,7 +125,8 @@ describe('Retract', () => { retractSettings: { speed: 11, delayDuration: 0.5, - positionFromBottom: 22, + position: 22, + positionReference: 'well-bottom', }, }) }) diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/__tests__/Submerge.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/__tests__/Submerge.test.tsx index dfb78696c21..b6769c1e9ec 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/__tests__/Submerge.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/__tests__/Submerge.test.tsx @@ -67,7 +67,12 @@ describe('Submerge', () => { }) it('renders text, buttons, input field, and keyboard for submerge before aspirating - position', () => { - render(props) + render({ + ...props, + state: { + submergeAspirate: { positionReference: 'well-bottom', position: 0 }, + } as any, + }) fireEvent.click(screen.getByRole('button', { name: '1' })) fireEvent.click(screen.getByRole('button', { name: '1' })) fireEvent.click(screen.getByText('Continue')) @@ -84,14 +89,20 @@ describe('Submerge', () => { fireEvent.click(screen.getByText('Continue')) screen.getByText('Save') screen.getByText('Distance from bottom of well (mm)') - screen.getByText('Between 0 and 2 mm') + screen.getByText('Between 0 and 3 mm') fireEvent.click(screen.getByRole('button', { name: '2' })) fireEvent.click(screen.getByRole('button', { name: '2' })) fireEvent.click(screen.getByRole('button', { name: '2' })) - screen.getByText('Value must be between 1 to 2') + screen.getByText('Value must be between 0 to 3') }) it('should call dispatch when clicking save button', () => { + props.state.submergeAspirate = { + speed: 0, + delayDuration: 0, + position: 0, + positionReference: 'well-top', + } render(props) fireEvent.click(screen.getByRole('button', { name: '1' })) fireEvent.click(screen.getByRole('button', { name: '1' })) @@ -108,7 +119,8 @@ describe('Submerge', () => { submergeSettings: { speed: 11, delayDuration: 0.5, - positionFromBottom: 22, + position: 22, + positionReference: 'well-top', }, }) }) diff --git a/app/src/organisms/ODD/QuickTransferFlow/README.md b/app/src/organisms/ODD/QuickTransferFlow/README.md index 63bc57e7aa5..b112c34b90c 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/README.md +++ b/app/src/organisms/ODD/QuickTransferFlow/README.md @@ -57,7 +57,7 @@ touchTipAspirate = -(sourceWellHeight - prevTouchTipAspirate) touchTipDispense = -(destWellHeight - prevTouchTipDispense) ``` -## [WIP] Version 1.2.0 +## Version 1.2.0 Due to changes in the Quick Transfer setup flow, there will be changes to QuickTransferWizardState and QuickTransferSummaryState. The changes are as follows: the comment `this has been added` will be removed before feature freeze. @@ -110,13 +110,15 @@ export interface QuickTransferSummaryState { // this has been added speed: number delayDuration: number - positionFromBottom: number + position: number + positionReference: PositionReference } retractAspirate?: { // this has been added speed: number delayDuration: number - positionFromBottom: number + position: number + positionReference: PositionReference } delayAspirate?: { // this has been updated - removed positionFromBottom @@ -134,13 +136,15 @@ export interface QuickTransferSummaryState { // this has been added speed: number delayDuration: number - positionFromBottom: number + position: number + positionReference: PositionReference } retractDispense?: { // this has been added speed: number delayDuration: number - positionFromBottom: number + position: number + positionReference: PositionReference } delayDispense?: { // this has been updated - removed positionFromBottom @@ -168,3 +172,20 @@ export interface QuickTransferSummaryState { liquidClassValuesInitialized: boolean // this has been added } ``` + +## Version 2.0.0 + +Introduction of Python protocol generation starting in robot stack v8.7.0. + +The shape of the Python file is as follows: + +```ts +imports +metadata +requirements +commands +``` + +Note that the `designerApplicationData` is not included in the generated Python, therefore, cloning/editing the generated protocol is currently not possible (which is never has been). + +You can still generate JSON behind a feature flag. diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectLiquidClass.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectLiquidClass.tsx index 9841be67a41..c311780b882 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectLiquidClass.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectLiquidClass.tsx @@ -9,11 +9,11 @@ import { StyledText, } from '@opentrons/components' import { - ETHANOL_LIQUID_CLASS_NAME, + ETHANOL_LIQUID_CLASS_NAME_V2, getAllLiquidClassDefs, - GLYCEROL_LIQUID_CLASS_NAME, + GLYCEROL_LIQUID_CLASS_NAME_V2, NONE_LIQUID_CLASS_NAME, - WATER_LIQUID_CLASS_NAME, + WATER_LIQUID_CLASS_NAME_V2, } from '@opentrons/shared-data' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' @@ -62,9 +62,9 @@ export function SelectLiquidClass({ LiquidClass['liquidClassName'], string > = { - ethanol_80: ETHANOL_LIQUID_CLASS_NAME, - glycerol_50: GLYCEROL_LIQUID_CLASS_NAME, - water: WATER_LIQUID_CLASS_NAME, + ethanol_80: ETHANOL_LIQUID_CLASS_NAME_V2, + glycerol_50: GLYCEROL_LIQUID_CLASS_NAME_V2, + water: WATER_LIQUID_CLASS_NAME_V2, none: NONE_LIQUID_CLASS_NAME, } diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectTipRack.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectTipRack.tsx index 121bf1dd31a..eecedce323e 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectTipRack.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectTipRack.tsx @@ -7,7 +7,10 @@ import { RadioButton, SPACING, } from '@opentrons/components' -import { getAllDefinitions } from '@opentrons/shared-data' +import { + getAllDefinitions, + LABWAREV2_DO_NOT_LIST, +} from '@opentrons/shared-data' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' @@ -31,9 +34,16 @@ export function SelectTipRack(props: SelectTipRackProps): JSX.Element { const { onNext, onBack, exitButtonProps, state, dispatch } = props const { i18n, t } = useTranslation(['quick_transfer', 'shared']) + // (kk:2025-09-30) this should be temporary until fix getAllDefinitions cache issue + const allLabwareDefinition2sByUri = getAllDefinitions() const selectedPipetteDefaultTipracks = - state.pipette?.liquids.default.defaultTipracks ?? [] + state.pipette?.liquids.default.defaultTipracks.filter(tiprackUri => { + // "opentrons/opentrons_flex_96_tiprack_20ul/1" -> "opentrons_flex_96_tiprack_20ul" + const loadName = tiprackUri.split('/')[1] + const isBlockedTiprack = LABWAREV2_DO_NOT_LIST.includes(loadName) + return !isBlockedTiprack + }) ?? [] const [selectedTipRack, setSelectedTipRack] = useState< LabwareDefinition2 | undefined diff --git a/app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx b/app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx index 30ad8d84cee..8e23abb9ca3 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx @@ -60,7 +60,10 @@ export function SummaryAndSettings( const host = useHost() const { t } = useTranslation(['quick_transfer', 'shared']) const [showSaveOrRunModal, setShowSaveOrRunModal] = useState(false) - const enableExportPython = useFeatureFlag('quickTransferExportPython') + const enableExportJSON = useFeatureFlag('quickTransferExportJSON') + const enableProtocolContentsLog = useFeatureFlag( + 'quickTransferProtocolContentsLog' + ) const displayCategory: string[] = ['overview', 'aspirate', 'dispense'] @@ -105,8 +108,7 @@ export function SummaryAndSettings( } }) - const isMultiTransferAspirate = state?.path === 'multiDispense' - const isMultiTransferDispense = state?.path === 'multiAspirate' + const isMultiTransferDispense = state?.path === 'multiDispense' const handleClickCreateTransfer = (): void => { setShowSaveOrRunModal(true) @@ -120,9 +122,19 @@ export function SummaryAndSettings( } const handleClickSave = (protocolName: string): void => { - const protocolFile = enableExportPython - ? createQuickTransferPythonFile(state, deckConfig, protocolName) - : createQuickTransferFile(state, deckConfig, protocolName) + const protocolFile = enableExportJSON + ? createQuickTransferFile( + state, + deckConfig, + protocolName, + enableProtocolContentsLog + ) + : createQuickTransferPythonFile( + state, + deckConfig, + protocolName, + enableProtocolContentsLog + ) createProtocolAsync({ files: [protocolFile], @@ -139,9 +151,19 @@ export function SummaryAndSettings( } const handleClickRun = (): void => { - const protocolFile = enableExportPython - ? createQuickTransferPythonFile(state, deckConfig) - : createQuickTransferFile(state, deckConfig) + const protocolFile = enableExportJSON + ? createQuickTransferFile( + state, + deckConfig, + undefined, + enableProtocolContentsLog + ) + : createQuickTransferPythonFile( + state, + deckConfig, + undefined, + enableProtocolContentsLog + ) createProtocolAsync({ files: [protocolFile], @@ -196,7 +218,7 @@ export function SummaryAndSettings( ) : null} {selectedCategory === 'dispense' ? ( diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/Overview.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/Overview.test.tsx index d56da0364fc..0ac8e1fe4d3 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/Overview.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/Overview.test.tsx @@ -1,7 +1,7 @@ import { screen } from '@testing-library/react' import { afterEach, beforeEach, describe, it, vi } from 'vitest' -import { WATER_LIQUID_CLASS_NAME } from '@opentrons/shared-data' +import { WATER_LIQUID_CLASS_NAME_V2 } from '@opentrons/shared-data' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' @@ -44,7 +44,7 @@ describe('Overview', () => { } as any, transferType: 'transfer', volume: 25, - liquidClassName: WATER_LIQUID_CLASS_NAME, + liquidClassName: WATER_LIQUID_CLASS_NAME_V2, } as any, } }) diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TipPosition.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TipPosition.test.tsx index 64c42ca7223..9038cc97be6 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TipPosition.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TipPosition.test.tsx @@ -131,7 +131,7 @@ describe('TipPosition', () => { expect(vi.mocked(InputField)).toHaveBeenCalledWith( { title: 'Distance from bottom of well (mm)', - error: 'Value must be between 1 to 100', + error: 'Value must be between 1 to 52', readOnly: true, type: 'text', value: 0, @@ -154,7 +154,7 @@ describe('TipPosition', () => { expect(vi.mocked(InputField)).toHaveBeenCalledWith( { title: 'Distance from bottom of well (mm)', - error: 'Value must be between 1 to 400', + error: 'Value must be between 1 to 202', readOnly: true, type: 'text', value: 0, diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectLiquidClass.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectLiquidClass.test.tsx index 1f1f744ad6d..d19b511c2c8 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectLiquidClass.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectLiquidClass.test.tsx @@ -2,11 +2,11 @@ import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { - ETHANOL_LIQUID_CLASS_NAME, + ETHANOL_LIQUID_CLASS_NAME_V2, getAllLiquidClassDefs, - GLYCEROL_LIQUID_CLASS_NAME, + GLYCEROL_LIQUID_CLASS_NAME_V2, NONE_LIQUID_CLASS_NAME, - WATER_LIQUID_CLASS_NAME, + WATER_LIQUID_CLASS_NAME_V2, } from '@opentrons/shared-data' import { renderWithProviders } from '/app/__testing-utils__' @@ -55,7 +55,7 @@ const mockByPipette = [ ] const mockLiquidClasses = { - [ETHANOL_LIQUID_CLASS_NAME]: { + [ETHANOL_LIQUID_CLASS_NAME_V2]: { liquidClassName: 'ethanol_80', displayName: 'Volatile', description: '80% ethanol', @@ -63,7 +63,7 @@ const mockLiquidClasses = { namespace: '', byPipette: mockByPipette, }, - [GLYCEROL_LIQUID_CLASS_NAME]: { + [GLYCEROL_LIQUID_CLASS_NAME_V2]: { liquidClassName: 'glycerol_50', displayName: 'Viscous', description: '50% glycerol', @@ -71,7 +71,7 @@ const mockLiquidClasses = { namespace: '', byPipette: mockByPipette, }, - [WATER_LIQUID_CLASS_NAME]: { + [WATER_LIQUID_CLASS_NAME_V2]: { displayName: 'Aqueous', liquidClassName: 'water', description: 'Deionized water', @@ -220,7 +220,7 @@ describe('SelectLiquidClass', () => { expect(props.onNext).toHaveBeenCalled() expect(props.dispatch).toHaveBeenCalledWith({ type: 'SET_LIQUID_CLASS', - liquidClassName: WATER_LIQUID_CLASS_NAME, + liquidClassName: WATER_LIQUID_CLASS_NAME_V2, }) }) diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx index 2e43ec3e20a..175094d7175 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx @@ -15,7 +15,7 @@ import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configurati import { NameQuickTransfer } from '../NameQuickTransfer' import { Overview } from '../Overview' import { SummaryAndSettings } from '../SummaryAndSettings' -import { createQuickTransferFile, getInitialSummaryState } from '../utils' +import { createQuickTransferPythonFile, getInitialSummaryState } from '../utils' import type { ComponentProps } from 'react' import type { NavigateFunction } from 'react-router-dom' @@ -92,7 +92,7 @@ describe('SummaryAndSettings', () => { vi.mocked(useCreateRunMutation).mockReturnValue({ createRun, } as any) - vi.mocked(createQuickTransferFile).mockReturnValue('' as any) + vi.mocked(createQuickTransferPythonFile).mockReturnValue('' as any) vi.mocked(getInitialSummaryState).mockReturnValue({ liquidClassValuesInitialized: true, } as any) @@ -151,7 +151,7 @@ describe('SummaryAndSettings', () => { name: ANALYTICS_QUICK_TRANSFER_RUN_NOW, properties: {}, }) - expect(vi.mocked(createQuickTransferFile)).toHaveBeenCalled() + expect(vi.mocked(createQuickTransferPythonFile)).toHaveBeenCalled() expect(vi.mocked(createProtocol)).toHaveBeenCalled() }) }) diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts b/app/src/organisms/ODD/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts index 71b296a05c3..69e9aa16385 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts @@ -76,11 +76,14 @@ describe('getInitialSummaryState', () => { ...props, state: { ...props.state, + volume: 1, + path: 'multiAspirate', transferType: 'consolidate', }, }) expect(initialSummaryState).toEqual({ ...props.state, + volume: 1, transferType: 'consolidate', aspirateFlowRate: 50, dispenseFlowRate: 75, @@ -126,13 +129,14 @@ describe('getInitialSummaryState', () => { ...props, state: { ...props.state, - volume: 10, + volume: 1, + path: 'multiDispense', transferType: 'distribute', }, }) expect(initialSummaryState).toEqual({ ...props.state, - volume: 10, + volume: 1, transferType: 'distribute', aspirateFlowRate: 50, dispenseFlowRate: 75, @@ -145,7 +149,7 @@ describe('getInitialSummaryState', () => { cutoutId: 'cutoutA3', cutoutFixtureId: 'trashBinAdapter', }, - disposalVolume: 10, + disposalVolume: 1, blowOutDispense: { location: { cutoutId: 'cutoutA3', cutoutFixtureId: 'trashBinAdapter' }, flowRate: 75, diff --git a/app/src/organisms/ODD/QuickTransferFlow/__tests__/utils/retrieveLiquidClassValues.test.ts b/app/src/organisms/ODD/QuickTransferFlow/__tests__/utils/retrieveLiquidClassValues.test.ts new file mode 100644 index 00000000000..19e0a53e8b6 --- /dev/null +++ b/app/src/organisms/ODD/QuickTransferFlow/__tests__/utils/retrieveLiquidClassValues.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest' + +import { + fixture96Plate, + fixtureP100096V2Specs, + fixtureTiprack300ul, + NONE_LIQUID_CLASS_NAME, +} from '@opentrons/shared-data' + +import { retrieveLiquidClassValues } from '../../utils' + +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { QuickTransferSummaryState } from '../../types' + +const STATE: QuickTransferSummaryState = { + pipette: fixtureP100096V2Specs, + mount: 'left', + tipRack: fixtureTiprack300ul as LabwareDefinition2, + source: fixture96Plate as LabwareDefinition2, + sourceWells: ['A1'], + destination: fixture96Plate as LabwareDefinition2, + destinationWells: ['A1'], + transferType: 'transfer', + volume: 10, + aspirateFlowRate: 716, + dispenseFlowRate: 716, + path: 'single', + tipPositionAspirate: 0, + preWetTip: false, + tipPositionDispense: 5, + changeTip: 'once', + dropTipLocation: { cutoutFixtureId: 'trashBinAdapter', cutoutId: 'cutoutA3' }, + liquidClassName: NONE_LIQUID_CLASS_NAME, + liquidClassValuesInitialized: false, +} + +describe('retrieveLiquidClassValues', () => { + it('returns the correct shape and values for getNoLiquidClassValues', () => { + const results = { + aspirateFlowRate: 716, + changeTip: 'once', + destination: fixture96Plate as LabwareDefinition2, + destinationWells: ['A1'], + dispenseFlowRate: 716, + dropTipLocation: { + cutoutFixtureId: 'trashBinAdapter', + cutoutId: 'cutoutA3', + }, + liquidClassName: 'none', + liquidClassValuesInitialized: false, + mount: 'left', + path: 'single', + pipette: fixtureP100096V2Specs, + preWetTip: false, + source: fixture96Plate as LabwareDefinition2, + sourceWells: ['A1'], + tipPositionAspirate: 0, + tipPositionDispense: 5, + tipRack: fixtureTiprack300ul as LabwareDefinition2, + transferType: 'transfer', + volume: 10, + } + expect(retrieveLiquidClassValues(STATE, 'all')).toStrictEqual(results) + }) +}) diff --git a/app/src/organisms/ODD/QuickTransferFlow/constants.ts b/app/src/organisms/ODD/QuickTransferFlow/constants.ts index 80142c09952..7dcf56f479a 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/constants.ts +++ b/app/src/organisms/ODD/QuickTransferFlow/constants.ts @@ -65,7 +65,7 @@ export const SINGLE_CHANNEL_COMPATIBLE_LABWARE = [ 'opentrons/geb_96_tiprack_1000ul/1', 'opentrons/geb_96_tiprack_10ul/1', 'opentrons/ibidi_96_square_well_plate_300ul/2', - 'opentrons/milliplex_microtiter_plate/1', + 'opentrons/milliplex_r_96_well_microtiter_plate/1', 'opentrons/nest_12_reservoir_15ml/2', 'opentrons/nest_12_reservoir_22ml/1', 'opentrons/nest_1_reservoir_195ml/3', @@ -140,7 +140,7 @@ export const EIGHT_CHANNEL_COMPATIBLE_LABWARE = [ 'opentrons/geb_96_tiprack_1000ul/1', 'opentrons/geb_96_tiprack_10ul/1', 'opentrons/ibidi_96_square_well_plate_300ul/2', - 'opentrons/milliplex_microtiter_plate/1', + 'opentrons/milliplex_r_96_well_microtiter_plate/1', 'opentrons/nest_12_reservoir_15ml/2', 'opentrons/nest_12_reservoir_22ml/1', 'opentrons/nest_1_reservoir_195ml/3', @@ -194,7 +194,7 @@ export const NINETY_SIX_CHANNEL_COMPATIBLE_LABWARE = [ 'opentrons/geb_96_tiprack_1000ul/1', 'opentrons/geb_96_tiprack_10ul/1', 'opentrons/ibidi_96_square_well_plate_300ul/2', - 'opentrons/milliplex_microtiter_plate/1', + 'opentrons/milliplex_r_96_well_microtiter_plate/1', 'opentrons/nest_12_reservoir_15ml/2', 'opentrons/nest_12_reservoir_22ml/1', 'opentrons/nest_1_reservoir_195ml/3', diff --git a/app/src/organisms/ODD/QuickTransferFlow/types.ts b/app/src/organisms/ODD/QuickTransferFlow/types.ts index 6e7eff4e5d7..d680eb521af 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/types.ts +++ b/app/src/organisms/ODD/QuickTransferFlow/types.ts @@ -3,6 +3,7 @@ import type { CutoutConfig, LabwareDefinition2, PipetteV2Specs, + PositionReference, } from '@opentrons/shared-data' import type { ACTIONS, @@ -73,12 +74,14 @@ export interface QuickTransferSummaryState { submergeAspirate?: { speed: number delayDuration: number - positionFromBottom: number + position: number + positionReference?: PositionReference } retractAspirate?: { speed: number delayDuration: number - positionFromBottom: number + position: number + positionReference?: PositionReference } delayAspirate?: { delayDuration: number @@ -94,12 +97,14 @@ export interface QuickTransferSummaryState { submergeDispense?: { speed: number delayDuration: number - positionFromBottom: number + position: number + positionReference?: PositionReference } retractDispense?: { speed: number delayDuration: number - positionFromBottom: number + position: number + positionReference?: PositionReference } delayDispense?: { delayDuration: number @@ -216,7 +221,8 @@ interface SetSubmergeAspirate { submergeSettings?: { speed: number delayDuration: number - positionFromBottom: number + position: number + positionReference?: PositionReference } } interface SetRetractAspirate { @@ -224,7 +230,8 @@ interface SetRetractAspirate { retractSettings?: { speed: number delayDuration: number - positionFromBottom: number + position: number + positionReference?: PositionReference } } interface SetDispenseTipPosition { @@ -262,7 +269,8 @@ interface SetSubmergeDispense { submergeSettings?: { speed: number delayDuration: number - positionFromBottom: number + position: number + positionReference?: PositionReference } } interface SetRetractDispense { @@ -270,7 +278,8 @@ interface SetRetractDispense { retractSettings?: { speed: number delayDuration: number - positionFromBottom: number + position: number + positionReference?: PositionReference } } interface SetChangeTip { diff --git a/app/src/organisms/ODD/QuickTransferFlow/utils/calculateAdjustWells.ts b/app/src/organisms/ODD/QuickTransferFlow/utils/calculateAdjustWells.ts new file mode 100644 index 00000000000..57428cab886 --- /dev/null +++ b/app/src/organisms/ODD/QuickTransferFlow/utils/calculateAdjustWells.ts @@ -0,0 +1,117 @@ +import { linearInterpolate } from '@opentrons/shared-data' + +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { PathOption, QuickTransferSummaryState } from '../types' + +interface CalculateWellLimitsParams { + state: QuickTransferSummaryState + tipRack: LabwareDefinition2 + volume: number + path: PathOption + conditioningByVolume: Array<[number, number]> + disposalByVolume: Array<[number, number]> + aspirateAirGapVolume: number +} + +interface AdjustWellsResult { + adjustedSourceWells: string[] + adjustedDestinationWells: string[] +} + +export function calculateAdjustWells({ + state, + tipRack, + volume, + path, + conditioningByVolume, + disposalByVolume, + aspirateAirGapVolume, +}: CalculateWellLimitsParams): AdjustWellsResult { + const tipCapacity = tipRack?.wells?.A1?.totalLiquidVolume + + // calculate extra volumes based on path + let extraVolumes = 0 + let minRequiredVolume = volume + let finalMaxWells = 1 + + if (path === 'multiDispense') { + minRequiredVolume = volume * 2 + + // calculate the maximum number of wells + // by iterating and checking the total volume including disposal/conditioning + let maxWellsForTip = 0 + let totalVolumeInTip = 0 + + for (let i = 0; i < state.destinationWells.length; i++) { + const wellsToTest = i + 1 + const volumeForWells = wellsToTest * volume + + const actualConditioningVolume = + linearInterpolate(volumeForWells, conditioningByVolume) ?? 0 + const actualDisposalVolume = + linearInterpolate(volumeForWells, disposalByVolume) ?? 0 + + const isDisposalVolumeEnabled = actualDisposalVolume > 0 + const isConditioningVolumeEnabled = actualConditioningVolume > 0 + const airGapVolume = + isDisposalVolumeEnabled || isConditioningVolumeEnabled + ? 0 + : aspirateAirGapVolume + + totalVolumeInTip = + volumeForWells + + actualConditioningVolume + + actualDisposalVolume + + airGapVolume + + if (totalVolumeInTip <= tipCapacity) { + maxWellsForTip = wellsToTest + } else { + break + } + } + + finalMaxWells = Math.max(1, maxWellsForTip) + + // calculate extraVolumes for the final number of wells + const finalVolumeForWells = finalMaxWells * volume + const finalConditioningVolume = + linearInterpolate(finalVolumeForWells, conditioningByVolume) ?? 0 + const finalDisposalVolume = + linearInterpolate(finalVolumeForWells, disposalByVolume) ?? 0 + const finalIsDisposalVolumeEnabled = finalDisposalVolume > 0 + const finalIsConditioningVolumeEnabled = finalConditioningVolume > 0 + const finalAirGapVolume = + finalIsDisposalVolumeEnabled || finalIsConditioningVolumeEnabled + ? 0 + : aspirateAirGapVolume + extraVolumes = finalDisposalVolume + finalAirGapVolume + } else if (path === 'multiAspirate') { + minRequiredVolume = volume * 2 + extraVolumes = aspirateAirGapVolume + + const maxWellsPerTip = Math.floor((tipCapacity - extraVolumes) / volume) + const maxWellsWithMinVolume = + Math.floor((tipCapacity - extraVolumes) / minRequiredVolume) * 2 + finalMaxWells = Math.min(maxWellsPerTip, maxWellsWithMinVolume) + } else { + // Single path + finalMaxWells = 1 + } + + // Limit wells based on path + const adjustedDestinationWells = + path === 'multiDispense' && finalMaxWells > 0 + ? state.destinationWells.slice(0, finalMaxWells) + : state.destinationWells + + const adjustedSourceWells = + path === 'multiAspirate' && finalMaxWells > 0 + ? state.sourceWells.slice(0, finalMaxWells) + : state.sourceWells + + return { + adjustedSourceWells, + adjustedDestinationWells, + } +} diff --git a/app/src/organisms/ODD/QuickTransferFlow/utils/createQuickTransferFile.ts b/app/src/organisms/ODD/QuickTransferFlow/utils/createQuickTransferFile.ts index 67a62cb07e0..c828e88ae57 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/utils/createQuickTransferFile.ts +++ b/app/src/organisms/ODD/QuickTransferFlow/utils/createQuickTransferFile.ts @@ -39,7 +39,8 @@ const uuid: () => string = uuidv1 export function createQuickTransferFile( quickTransferState: QuickTransferSummaryState, deckConfig: DeckConfiguration, - protocolName?: string + protocolName?: string, + enableQuickTransferProtocolContentsLog?: boolean ): File { const { stepArgs, @@ -154,7 +155,6 @@ export function createQuickTransferFile( robotStateTimeline.timeline, timelineFrame => timelineFrame.commands ) - const commands: CreateCommand[] = [ loadPipetteCommand, ...loadAdapterCommands, @@ -181,7 +181,7 @@ export function createQuickTransferFile( // see QuickTransferFlow/README.md for versioning details designerApplication: { name: 'opentrons/quick-transfer', - version: '1.1.0', + version: '2.0.0', data: quickTransferState, }, } @@ -226,6 +226,38 @@ export function createQuickTransferFile( ...commandAnnotionaV1Mixin, }) + // temporary logging for debugging + if (enableQuickTransferProtocolContentsLog) { + const protocolObject = { + ...protocolBase, + ...flexDeckSpec, + ...labwareV2Mixin, + ...liquidV1Mixin, + ...commandv8Mixin, + ...commandAnnotionaV1Mixin, + } + + console.group('🧪 Quick Transfer Protocol Contents') + console.log(JSON.stringify(protocolObject, null, 2)) + const downloadProtocolObject = (): void => { + const jsonString = JSON.stringify(protocolObject, null, 2) + const blob = new Blob([jsonString], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = `debug-${protocolObject.metadata.protocolName + .replace(/[^a-z0-9]/gi, '_') + .toLowerCase()}-${Date.now()}.json` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } + ;(window as any).downloadjson = downloadProtocolObject + console.log('💾 Or copy/paste: downloadjson()') + console.groupEnd() + } + return new File( [protocolContents], `${protocolBase.metadata.protocolName}.json` @@ -235,7 +267,8 @@ export function createQuickTransferFile( export function createQuickTransferPythonFile( quickTransferState: QuickTransferSummaryState, deckConfig: DeckConfiguration, - protocolName?: string + protocolName?: string, + enableQuickTransferProtocolContentsLog?: boolean ): File { const sourceLabwareName = quickTransferState.source.metadata.displayName let destinationLabwareName = sourceLabwareName @@ -247,26 +280,55 @@ export function createQuickTransferPythonFile( protocolName ?? `Quick Transfer ${quickTransferState.volume}µL`, description: `This quick transfer moves liquids from a ${sourceLabwareName} to a ${destinationLabwareName}`, source: 'Quick Transfer', - // TODO: increase version for when we export python // see QuickTransferFlow/README.md for versioning details - version: '1.1.0', + version: '2.0.0', category: null, subcategory: null, tags: [], } + const designerApplication = { + name: 'opentrons/quick-transfer', + version: '2.0.0', + data: quickTransferState, + } + const stringifiedDesignerApplication = JSON.stringify(designerApplication) + const designerApplicationBlob = `\nDESIGNER_APPLICATION = """${stringifiedDesignerApplication}"""\n` + const protocolContents = [ pythonImports(), pythonMetadata(fileMetadata), pythonRequirements(FLEX_ROBOT_TYPE), pythonDef(quickTransferState, deckConfig), + designerApplicationBlob, ] .filter(section => section) .join('\n\n') + '\n' - // so you can view the string in devTools: - console.log(protocolContents) + // temporary logging for debugging + if (enableQuickTransferProtocolContentsLog) { + console.group('🧪 Quick Transfer Protocol Contents') + console.log(protocolContents) + const downloadProtocolPython = (): void => { + const blob = new Blob([protocolContents], { type: 'text/x-python' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + const safeName = (protocolName ?? 'protocol name') + .replace(/[^a-z0-9]/gi, '_') + .toLowerCase() + + link.download = `debug-${safeName}-${Date.now()}.py` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } + ;(window as any).downloadpy = downloadProtocolPython + console.log('💾 Or copy/paste: downloadpy()') + console.groupEnd() + } return new File([protocolContents], `${fileMetadata.protocolName}.py`, { type: 'text/x-python', diff --git a/app/src/organisms/ODD/QuickTransferFlow/utils/generateQuickTransferArgs.ts b/app/src/organisms/ODD/QuickTransferFlow/utils/generateQuickTransferArgs.ts index b256566b865..55b18bd66bc 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/utils/generateQuickTransferArgs.ts +++ b/app/src/organisms/ODD/QuickTransferFlow/utils/generateQuickTransferArgs.ts @@ -4,7 +4,6 @@ import uuidv1 from 'uuid/v4' import { getAllDefinitions, getLabwareDefURI, - getTipTypeFromTipRackDefinition, orderWells, POSITION_REFERENCE_BOTTOM, TRASH_BIN_ADAPTER_FIXTURE, @@ -352,9 +351,6 @@ export function generateQuickTransferArgs( dropTipWasteChuteLocationEntity?.id ?? '' - const tipType = getTipTypeFromTipRackDefinition(quickTransferState.tipRack) - const flowRatesForSupportedTip = - quickTransferState.pipette.liquids.default.supportedTips[tipType] const pipetteEntity = Object.values(invariantContext.pipetteEntities)[0] const sourceLabwareId = Object.keys(robotState.labware).find( @@ -399,8 +395,7 @@ export function generateQuickTransferArgs( aspirateOffsetFromBottomMm: quickTransferState.tipPositionAspirate, dispenseOffsetFromBottomMm: quickTransferState.tipPositionDispense, blowoutLocation, - blowoutFlowRateUlSec: - flowRatesForSupportedTip.defaultBlowOutFlowRate.default, + blowoutFlowRateUlSec: quickTransferState.blowOutDispense?.flowRate ?? 0, blowoutOffsetFromTopMm: DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, changeTip: quickTransferState.changeTip, preWetTip: quickTransferState.preWetTip, @@ -434,41 +429,63 @@ export function generateQuickTransferArgs( name: null, description: null, nozzles, - pushOut: null, + pushOut: quickTransferState.pushOutDispense?.volume ?? 0, liquidClass: quickTransferState.liquidClassName !== 'none' ? quickTransferState.liquidClassName : null, aspiratePositionReference: POSITION_REFERENCE_BOTTOM, - aspirateZOffset: 0, - aspirateSubmergeSpeed: null, + aspirateZOffset: quickTransferState.tipPositionAspirate, + aspirateSubmergeSpeed: quickTransferState.submergeAspirate?.speed ?? 0, aspirateSubmergeXOffset: 0, aspirateSubmergeYOffset: 0, - aspirateSubmergeZOffset: 0, - aspirateSubmergePositionReference: POSITION_REFERENCE_BOTTOM, - aspirateSubmergeDelay: null, - aspirateRetractSpeed: null, + aspirateSubmergeZOffset: quickTransferState.submergeAspirate?.position ?? 0, + aspirateSubmergePositionReference: + quickTransferState.submergeAspirate?.positionReference ?? + POSITION_REFERENCE_BOTTOM, + aspirateSubmergeDelay: + quickTransferState.submergeAspirate?.delayDuration != null + ? { seconds: quickTransferState.submergeAspirate.delayDuration } + : null, + aspirateRetractSpeed: quickTransferState.retractAspirate?.speed ?? 0, aspirateRetractXOffset: 0, aspirateRetractYOffset: 0, - aspirateRetractZOffset: 0, - aspirateRetractPositionReference: POSITION_REFERENCE_BOTTOM, - aspirateRetractDelay: null, + aspirateRetractZOffset: quickTransferState.retractAspirate?.position ?? 0, + aspirateRetractPositionReference: + quickTransferState.retractAspirate?.positionReference ?? + POSITION_REFERENCE_BOTTOM, + aspirateRetractDelay: + quickTransferState.retractAspirate?.delayDuration != null + ? { seconds: quickTransferState.retractAspirate.delayDuration } + : null, dispensePositionReference: POSITION_REFERENCE_BOTTOM, - dispenseZOffset: 0, - dispenseSubmergeSpeed: null, + dispenseZOffset: quickTransferState.tipPositionDispense, + dispenseSubmergeSpeed: quickTransferState.submergeDispense?.speed ?? 0, dispenseSubmergeXOffset: 0, dispenseSubmergeYOffset: 0, - dispenseSubmergeZOffset: 0, - dispenseSubmergePositionReference: POSITION_REFERENCE_BOTTOM, - dispenseSubmergeDelay: null, - dispenseRetractSpeed: null, + dispenseSubmergeZOffset: quickTransferState.submergeDispense?.position ?? 0, + dispenseSubmergePositionReference: + quickTransferState.submergeDispense?.positionReference ?? + POSITION_REFERENCE_BOTTOM, + dispenseSubmergeDelay: + quickTransferState.submergeDispense?.delayDuration != null + ? { seconds: quickTransferState.submergeDispense.delayDuration } + : null, + dispenseRetractSpeed: quickTransferState.retractDispense?.speed ?? 0, dispenseRetractXOffset: 0, dispenseRetractYOffset: 0, - dispenseRetractZOffset: 0, - dispenseRetractPositionReference: POSITION_REFERENCE_BOTTOM, - dispenseRetractDelay: null, - touchTipAfterAspirateMmFromEdge: null, - touchTipAfterDispenseMmFromEdge: null, + dispenseRetractZOffset: quickTransferState.retractDispense?.position ?? 0, + dispenseRetractPositionReference: + quickTransferState.retractDispense?.positionReference ?? + POSITION_REFERENCE_BOTTOM, + dispenseRetractDelay: + quickTransferState.retractDispense?.delayDuration != null + ? { seconds: quickTransferState.retractDispense.delayDuration } + : null, + touchTipAfterAspirateMmFromEdge: + quickTransferState.touchTipAspirate ?? null, + touchTipAfterDispenseMmFromEdge: + quickTransferState.touchTipDispense ?? null, } switch (quickTransferState.path) { @@ -544,7 +561,8 @@ export function generateQuickTransferArgs( const distributeStepArguments: DistributeArgs = { ...commonFields, commandCreatorFnName: 'distribute', - disposalVolume: quickTransferState.disposalVolume, + disposalVolume: + quickTransferState.disposalVolumeDispenseSettings?.volume ?? null, mixBeforeAspirate: quickTransferState.mixOnAspirate != null ? { @@ -554,7 +572,7 @@ export function generateQuickTransferArgs( : null, sourceWell: sourceWells[0], destWells, - conditioningVolume: null, + conditioningVolume: quickTransferState.conditionAspirate ?? null, } return { stepArgs: distributeStepArguments, diff --git a/app/src/organisms/ODD/QuickTransferFlow/utils/getInitialSummaryState.ts b/app/src/organisms/ODD/QuickTransferFlow/utils/getInitialSummaryState.ts index fbf93d09f50..53334d4c4f3 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/utils/getInitialSummaryState.ts +++ b/app/src/organisms/ODD/QuickTransferFlow/utils/getInitialSummaryState.ts @@ -59,17 +59,21 @@ export function getInitialSummaryState( let path: PathOption = state.path // for multiDispense the volume capacity must be at least 3x the volume per well // to account for the 1x volume per well disposal volume default + // otherwise, we set the path to single if ( state.transferType === 'distribute' && - maxTipCapacity >= state.volume * 3 + maxTipCapacity < state.volume * 3 && + state.path === 'multiDispense' ) { - path = 'multiDispense' + path = 'single' // for multiAspirate the volume capacity must be at least 2x the volume per well + // otherwise, we set the path to single } else if ( state.transferType === 'consolidate' && - maxTipCapacity >= state.volume * 2 + maxTipCapacity < state.volume * 2 && + state.path === 'multiAspirate' ) { - path = 'multiAspirate' + path = 'single' } const trashConfigCutout = deckConfig.find( diff --git a/app/src/organisms/ODD/QuickTransferFlow/utils/getTransferPlanAndReferenceVolumes.ts b/app/src/organisms/ODD/QuickTransferFlow/utils/getTransferPlanAndReferenceVolumes.ts deleted file mode 100644 index 8f17995f00b..00000000000 --- a/app/src/organisms/ODD/QuickTransferFlow/utils/getTransferPlanAndReferenceVolumes.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { linearInterpolate } from '@opentrons/shared-data' - -import type { PipetteV2Specs } from '@opentrons/shared-data' -import type { PathOption, ReferenceVolumes } from '@opentrons/step-generation' - -export const getTransferPlanAndReferenceVolumes = (args: { - pipetteSpecs: PipetteV2Specs - maxWorkingVolumeTip?: number - volume: number - path: PathOption - numDispenseWells: number - aspirateAirGapByVolume: Array<[number, number]> - conditioningByVolume: Array<[number, number]> | null - disposalByVolume: Array<[number, number]> | null -}): { - referenceVolumes: ReferenceVolumes - multiWellHandling: { - isSupported: boolean - numWellsToFitInTip?: number - } -} => { - const { - path, - volume, - pipetteSpecs, - maxWorkingVolumeTip, - conditioningByVolume, - disposalByVolume, - numDispenseWells, - aspirateAirGapByVolume, - } = args - const { liquids } = pipetteSpecs - const isInLowVolumeMode = - volume < liquids.default.minVolume && 'lowVolumeDefault' in liquids - const maxWorkingVolumePipette = isInLowVolumeMode - ? liquids.lowVolumeDefault.maxVolume - : liquids.default.maxVolume - const maxWorkingVolume = - maxWorkingVolumeTip == null - ? maxWorkingVolumePipette - : Math.min(maxWorkingVolumePipette, maxWorkingVolumeTip) - const minVolumeForMultiAspirateDispense = volume * 2 - const conditioningVolumeForMultiAspirateDispense = - conditioningByVolume != null - ? linearInterpolate( - minVolumeForMultiAspirateDispense, - conditioningByVolume - ) ?? 0 - : 0 - const isMultiDispenseAvailable = - conditioningByVolume != null && - disposalByVolume != null && - maxWorkingVolume >= - minVolumeForMultiAspirateDispense + - conditioningVolumeForMultiAspirateDispense + - (linearInterpolate( - minVolumeForMultiAspirateDispense, - disposalByVolume - ) ?? 0) + - // don't take air gap into account if conditioning volume is present - (conditioningVolumeForMultiAspirateDispense === 0 - ? linearInterpolate( - minVolumeForMultiAspirateDispense, - aspirateAirGapByVolume - ) ?? 0 - : 0) - const isMultiAspirateAvailable = - maxWorkingVolume > minVolumeForMultiAspirateDispense - - // early return if multiAspirate/multiDispense cannot be accommodated - if ( - path === 'single' || - (path === 'multiDispense' && !isMultiDispenseAvailable) || - (path === 'multiAspirate' && !isMultiAspirateAvailable) - ) { - const aspirateAirGapAtSpecifiedVolume = - linearInterpolate(volume, aspirateAirGapByVolume) ?? 0 - // split if target volume + air gap volume > maxWorkingVolume - const numAspirations = Math.ceil( - (volume + aspirateAirGapAtSpecifiedVolume) / maxWorkingVolume - ) - const volumePerAspiration = volume / numAspirations - return { - referenceVolumes: { - airGap: { - aspirate: aspirateAirGapAtSpecifiedVolume, - dispense: 0, - }, - correction: { - aspirate: volumePerAspiration, - dispense: volumePerAspiration, - }, - pushOut: volumePerAspiration, - flowRate: { - aspirate: volumePerAspiration, - dispense: volumePerAspiration, - }, - }, - multiWellHandling: { - isSupported: false, - }, - } - } - - if (path === 'multiDispense') { - let totalVolumeForMultiDispense: number = 0 - let numDestinationsPerAspiration: number = 0 - for (let i = 0; i < numDispenseWells; i++) { - const next = _getTotalVolumeForMultiDispense( - (i + 1) * volume, - conditioningByVolume ?? [], - disposalByVolume ?? [] - ) - if (next > maxWorkingVolume) { - break - } else { - totalVolumeForMultiDispense = (i + 1) * volume - numDestinationsPerAspiration += 1 - } - } - return { - referenceVolumes: { - airGap: { - aspirate: _getTotalVolumeForMultiDispense( - totalVolumeForMultiDispense, - conditioningByVolume ?? [], - disposalByVolume ?? [], - false - ), - dispense: _getTotalVolumeForMultiDispense( - // here, we interpolate the post-dispense air gap volume based on the total volume in the tip - // after the first dispense - (numDestinationsPerAspiration - 1) * volume, - conditioningByVolume ?? [], - disposalByVolume ?? [], - false - ), - }, - correction: { - aspirate: _getTotalVolumeForMultiDispense( - totalVolumeForMultiDispense, - conditioningByVolume ?? [], - disposalByVolume ?? [] - ), - dispense: volume, - }, - flowRate: { - aspirate: _getTotalVolumeForMultiDispense( - totalVolumeForMultiDispense, - conditioningByVolume ?? [], - disposalByVolume ?? [] - ), - dispense: volume, - }, - pushOut: volume, - conditioning: totalVolumeForMultiDispense, - disposal: totalVolumeForMultiDispense, - }, - multiWellHandling: { - isSupported: true, - numWellsToFitInTip: numDestinationsPerAspiration, - }, - } - } - // path is valid multiAspirate - const maxSourcesPerAspiration = Math.floor(maxWorkingVolume / volume) - const volumeTotalAspiration = maxSourcesPerAspiration * volume - return { - referenceVolumes: { - airGap: { - // here, we interpolate the post-aspirate air gap volume based on the total volume in the tip - // after the final aspiration - aspirate: volumeTotalAspiration, - dispense: 0, - }, - pushOut: volumeTotalAspiration, - correction: { - aspirate: volumeTotalAspiration, - dispense: volumeTotalAspiration, - }, - flowRate: { - aspirate: volume, - dispense: volumeTotalAspiration, - }, - }, - multiWellHandling: { - isSupported: true, - numWellsToFitInTip: maxSourcesPerAspiration, - }, - } -} - -// ToDo (kk:06/25/2025) in the future, we would like to export this from step-generation -const _getTotalVolumeForMultiDispense = ( - targetVol: number, - conditioningByVolume: Array<[number, number]>, - disposalByVolume: Array<[number, number]>, - includeConditioning: boolean = true -): number => { - const interpolatedConditioningVolume = - linearInterpolate(targetVol, conditioningByVolume) ?? 0 - const interpolatedDisposalVolume = - linearInterpolate(targetVol, disposalByVolume) ?? 0 - return ( - targetVol + - (includeConditioning ? interpolatedConditioningVolume : 0) + - interpolatedDisposalVolume - ) -} diff --git a/app/src/organisms/ODD/QuickTransferFlow/utils/index.ts b/app/src/organisms/ODD/QuickTransferFlow/utils/index.ts index d6b57f1eb19..524360de580 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/utils/index.ts +++ b/app/src/organisms/ODD/QuickTransferFlow/utils/index.ts @@ -1,5 +1,6 @@ export { checkLiquidClassCompatibility } from './checkLiquidClassCompatibility' export { createQuickTransferFile } from './createQuickTransferFile' +export { createQuickTransferPythonFile } from './createQuickTransferFile' export { generateCompatibleLabwareForPipette } from './generateCompatibleLabwareForPipette' export { generateQuickTransferArgs } from './generateQuickTransferArgs' export { getAspirateAirGapVolumeRange } from './getAspirateAirGapVolumeRange' diff --git a/app/src/organisms/ODD/QuickTransferFlow/utils/retrieveLiquidClassValues.ts b/app/src/organisms/ODD/QuickTransferFlow/utils/retrieveLiquidClassValues.ts index 92c623e578a..7f1ce6fb937 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/utils/retrieveLiquidClassValues.ts +++ b/app/src/organisms/ODD/QuickTransferFlow/utils/retrieveLiquidClassValues.ts @@ -1,14 +1,16 @@ +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ import { - ETHANOL_LIQUID_CLASS_NAME, + ETHANOL_LIQUID_CLASS_NAME_V2, FLEX_ROBOT_TYPE, getAllLiquidClassDefs, getFlexNameConversion, getLabwareDefURI, - GLYCEROL_LIQUID_CLASS_NAME, + GLYCEROL_LIQUID_CLASS_NAME_V2, linearInterpolate, NONE_LIQUID_CLASS_NAME, + POSITION_REFERENCE_TOP, SAFE_MOVE_TO_WELL_OFFSET_FROM_TOP_MM, - WATER_LIQUID_CLASS_NAME, + WATER_LIQUID_CLASS_NAME_V2, } from '@opentrons/shared-data' import { DEST_WELL_BLOWOUT_DESTINATION, @@ -17,12 +19,15 @@ import { SOURCE_WELL_BLOWOUT_DESTINATION, } from '@opentrons/step-generation' +import { calculateAdjustWells } from './calculateAdjustWells' import { getFlowRateFields } from './getFlowRaiteFields' import { getMatchingTipLiquidSpecsFromSpec } from './getMatchingTipLiquidSpecsFromSpec' import { getMaxUiFlowRate } from './getMaxUiFlowRate' import type { BlowOutLocation, QuickTransferSummaryState } from '../types' +const DEFAULT_MM_OFFSET_FROM_BOTTOM = 1 + export const retrieveLiquidClassValues = ( state: QuickTransferSummaryState, liquidHandlingAction: 'aspirate' | 'dispense' | 'all' @@ -75,7 +80,9 @@ const getNoLiquidClassValues = ( ): QuickTransferSummaryState => { const { tipRack, path, volume, pipette } = state const tiprackUri = getLabwareDefURI(tipRack) - const referenceLiquidClass = getAllLiquidClassDefs()[WATER_LIQUID_CLASS_NAME] + const referenceLiquidClass = getAllLiquidClassDefs()[ + WATER_LIQUID_CLASS_NAME_V2 + ] const liquidClassValuesForPipette = referenceLiquidClass.byPipette.find( ({ pipetteModel }) => convertedPipetteName === pipetteModel ) @@ -104,19 +111,39 @@ const getNoLiquidClassValues = ( > const numAspirateWells = state.sourceWells.length const numDispenseWells = state.destinationWells.length - const byVolumeLookup = getTransferPlanAndReferenceVolumes({ + + const { + referenceVolumes: byVolumeLookup, + } = getTransferPlanAndReferenceVolumes({ pipetteSpecs: pipette, tiprackDefinition: tipRack, - numAspirateWells: numAspirateWells, - volume: volume, - path: path, - numDispenseWells: numDispenseWells, - aspirateAirGapByVolume: aspirateAirGapByVolume, - conditioningByVolume: conditioningByVolume, - disposalByVolume: disposalByVolume, - }).referenceVolumes + numAspirateWells, + volume, + path, + numDispenseWells, + aspirateAirGapByVolume, + conditioningByVolume, + disposalByVolume, + }) - const { conditioning, correction } = byVolumeLookup + const actualConditioningVolume = + linearInterpolate(volume, conditioningByVolume) ?? 0 + const aspirateAirGapVolume = aspirate?.retract.airGapByVolume[0][1] ?? 0 + + // Calculate extra volumes based on path + const { + adjustedSourceWells, + adjustedDestinationWells, + } = calculateAdjustWells({ + state, + tipRack, + volume, + path, + conditioningByVolume, + disposalByVolume, + aspirateAirGapVolume, + }) + const { correction } = byVolumeLookup const aspirateCorrectionVolume = linearInterpolate( correction.aspirate, @@ -130,7 +157,7 @@ const getNoLiquidClassValues = ( const matchingTipLiquidSpecs = getMatchingTipLiquidSpecsFromSpec( pipette, volume, - tiprackUri as string + tiprackUri ) const aspirateMaxUiFlowRate = getMaxUiFlowRate({ @@ -169,46 +196,40 @@ const getNoLiquidClassValues = ( const aspirateState = { aspirateFlowRate: aspirateFlowRateFields.aspirate_flowRate ?? 0, - tipPositionAspirate: aspirate.aspiratePosition.offset.z, + tipPositionAspirate: DEFAULT_MM_OFFSET_FROM_BOTTOM, submergeAspirate: { speed: aspirate.submerge.speed, - positionFromBottom: aspirate.submerge.startPosition.offset.z, + positionReference: POSITION_REFERENCE_TOP, + position: SAFE_MOVE_TO_WELL_OFFSET_FROM_TOP_MM, delayDuration: aspirate.submerge.delay.params?.duration ?? 0, }, preWetTip: aspirate.preWet, mixOnAspirate: { - mixVolume: aspirate.mix.params?.volume ?? 0, - repetitions: aspirate.mix.params?.repetitions ?? 0, - }, - delayAspirate: { - delayDuration: aspirate.delay.params?.duration ?? 0, + mixVolume: 0, + repetitions: 0, }, retractAspirate: { speed: aspirate.retract.speed ?? 0, - positionFromBottom: aspirate.retract.endPosition.offset.z ?? 0, + positionReference: POSITION_REFERENCE_TOP, + position: SAFE_MOVE_TO_WELL_OFFSET_FROM_TOP_MM, delayDuration: aspirate.retract.delay.params?.duration ?? 0, }, touchTipAspirate: !aspirate.retract.touchTip.enable ? undefined : aspirate.retract.touchTip.params?.zOffset, touchTipAspirateSpeed: aspirate.retract.touchTip.params?.speed, - airGapAspirate: aspirate.retract.airGapByVolume[0][1] ?? 0, - conditionAspirate: conditioning ?? 0, + conditionAspirate: actualConditioningVolume ?? 0, } const dispenseState = { dispenseFlowRate: dispenseFlowRateFields.dispense_flowRate ?? 0, - tipPositionDispense: dispense.dispensePosition.offset.z, + tipPositionDispense: DEFAULT_MM_OFFSET_FROM_BOTTOM, submergeDispense: { speed: dispense.submerge.speed, - positionFromBottom: SAFE_MOVE_TO_WELL_OFFSET_FROM_TOP_MM, + position: SAFE_MOVE_TO_WELL_OFFSET_FROM_TOP_MM, + positionReference: POSITION_REFERENCE_TOP, delayDuration: dispense.submerge.delay.params?.duration ?? 0, }, - delayDispense: !dispense.delay.enable - ? undefined - : { - delayDuration: dispense.delay.params?.duration ?? 0, - }, pushOutDispense: { volume: linearInterpolate( @@ -218,23 +239,16 @@ const getNoLiquidClassValues = ( }, retractDispense: { speed: dispense.retract.speed, - positionFromBottom: SAFE_MOVE_TO_WELL_OFFSET_FROM_TOP_MM, + position: SAFE_MOVE_TO_WELL_OFFSET_FROM_TOP_MM, + positionReference: POSITION_REFERENCE_TOP, delayDuration: dispense.retract.delay.params?.duration ?? 0, }, - blowOutDispense: { - location: convertBlowoutLocation( - dispense.retract.blowout?.params?.location, - state - ), - flowRate: dispense.retract.blowout?.params?.flowRate ?? 0, - }, touchTipDispense: !dispense.retract.touchTip.enable ? undefined : dispense.retract.touchTip.params?.zOffset, touchTipDispenseSpeed: dispense.retract.touchTip.params?.speed, - airGapDispense: dispense.retract.airGapByVolume[0][1] ?? 0, disposalVolumeDispenseSettings: { - volume: pipette.liquids.default.minVolume, + volume: 0, blowOutLocation: convertBlowoutLocation( dispense?.retract.blowout?.params?.location, @@ -247,6 +261,8 @@ const getNoLiquidClassValues = ( if (liquidHandlingAction === 'all') { return { ...state, + sourceWells: adjustedSourceWells, + destinationWells: adjustedDestinationWells, ...aspirateState, ...dispenseState, } @@ -254,11 +270,15 @@ const getNoLiquidClassValues = ( if (liquidHandlingAction === 'aspirate') { return { ...state, + sourceWells: adjustedSourceWells, + destinationWells: adjustedDestinationWells, ...aspirateState, } } else { return { ...state, + sourceWells: adjustedSourceWells, + destinationWells: adjustedDestinationWells, ...dispenseState, } } @@ -284,9 +304,9 @@ const getLiquidClassValues = ( const allLiquidClassDefs = getAllLiquidClassDefs() const liquidClassMap = new Map([ - ['water', WATER_LIQUID_CLASS_NAME], - ['glycerol_50', GLYCEROL_LIQUID_CLASS_NAME], - ['ethanol_80', ETHANOL_LIQUID_CLASS_NAME], + ['water', WATER_LIQUID_CLASS_NAME_V2], + ['glycerol_50', GLYCEROL_LIQUID_CLASS_NAME_V2], + ['ethanol_80', ETHANOL_LIQUID_CLASS_NAME_V2], ]) const selectedLiquidClass = liquidClassMap.get( getLiquidClassName(state.liquidClassName) ?? 'none' @@ -312,26 +332,46 @@ const getLiquidClassValues = ( conditioningByVolume: rawConditioningByVolume = [], disposalByVolume: rawDisposalByVolume = [], } = multiDispense ?? {} + const conditioningByVolume = rawConditioningByVolume as Array< [number, number] > + const disposalByVolume = rawDisposalByVolume as Array<[number, number]> const aspirateAirGapByVolume = aspirate?.retract.airGapByVolume as Array< [number, number] > const numAspirateWells = state.sourceWells.length const numDispenseWells = destinationWells.length - const byVolumeLookup = getTransferPlanAndReferenceVolumes({ + + const { + referenceVolumes: byVolumeLookup, + } = getTransferPlanAndReferenceVolumes({ pipetteSpecs, tiprackDefinition: tipRack, - numAspirateWells: numAspirateWells, - volume: volume, - path: path, - numDispenseWells: numDispenseWells, - aspirateAirGapByVolume: aspirateAirGapByVolume, - conditioningByVolume: conditioningByVolume, - disposalByVolume: disposalByVolume, - }).referenceVolumes + numAspirateWells, + volume, + path, + numDispenseWells, + aspirateAirGapByVolume, + conditioningByVolume, + disposalByVolume, + }) + + const aspirateAirGapVolume = aspirate?.retract.airGapByVolume[0][1] ?? 0 + + const { + adjustedSourceWells, + adjustedDestinationWells, + } = calculateAdjustWells({ + state, + tipRack, + volume, + path, + conditioningByVolume, + disposalByVolume, + aspirateAirGapVolume, + }) const matchingTipLiquidSpecs = getMatchingTipLiquidSpecsFromSpec( pipetteSpecs, @@ -383,14 +423,19 @@ const getLiquidClassValues = ( dispenseMaxUiFlowRate ) - const { conditioning, disposal } = byVolumeLookup + const conditioningVolume = + linearInterpolate(volume, conditioningByVolume) ?? 0 + + const disposalVolume = linearInterpolate(volume, disposalByVolume) ?? 0 const aspirateState = { aspirateFlowRate: aspirateFlowRateFields.aspirate_flowRate ?? 0, tipPositionAspirate: aspirate?.aspiratePosition.offset.z ?? 0, submergeAspirate: { speed: aspirate?.submerge.speed ?? 0, - positionFromBottom: aspirate?.submerge.startPosition.offset.z ?? 0, + position: aspirate?.submerge.startPosition.offset.z ?? 0, + positionReference: + aspirate?.submerge.startPosition.positionReference ?? undefined, delayDuration: aspirate?.submerge.delay.params?.duration ?? 0, }, preWetTip: preWet ?? false, @@ -409,7 +454,9 @@ const getLiquidClassValues = ( }, retractAspirate: { speed: aspirate?.retract.speed ?? 0, - positionFromBottom: aspirate?.retract.endPosition.offset.z ?? 0, + position: aspirate?.retract.endPosition.offset.z ?? 0, + positionReference: + aspirate?.retract.endPosition.positionReference ?? undefined, delayDuration: aspirate?.retract.delay.params?.duration ?? 0, }, touchTipAspirate: @@ -421,7 +468,7 @@ const getLiquidClassValues = ( ? undefined : aspirate?.retract.touchTip.params?.speed, airGapAspirate: aspirate?.retract.airGapByVolume[0][1] ?? 0, - conditionAspirate: conditioning ?? 0, + conditionAspirate: conditioningVolume ?? 0, } const dispenseState = { @@ -429,7 +476,9 @@ const getLiquidClassValues = ( tipPositionDispense: dispense?.dispensePosition.offset.z ?? 0, submergeDispense: { speed: dispense?.submerge.speed ?? 0, - positionFromBottom: dispense?.submerge.startPosition.offset.z ?? 0, + position: dispense?.submerge.startPosition.offset.z ?? 0, + positionReference: + dispense?.submerge.startPosition.positionReference ?? undefined, delayDuration: dispense?.submerge.delay.params?.duration ?? 0, }, delayDispense: @@ -454,7 +503,9 @@ const getLiquidClassValues = ( }, retractDispense: { speed: dispense?.retract.speed ?? 0, - positionFromBottom: dispense?.retract.endPosition.offset.z ?? 0, + position: dispense?.retract.endPosition.offset.z ?? 0, + positionReference: + dispense?.retract.endPosition.positionReference ?? undefined, delayDuration: dispense?.retract.delay.params?.duration ?? 0, }, blowOutDispense: @@ -477,20 +528,22 @@ const getLiquidClassValues = ( : dispense?.retract.touchTip.params?.speed, airGapDispense: dispense?.retract.airGapByVolume[0][1] ?? 0, disposalVolumeDispenseSettings: { - volume: disposal ?? 0, + volume: disposalVolume, blowOutLocation: convertBlowoutLocation( dispense?.retract.blowout?.params?.location, state ) ?? state.dropTipLocation, - flowRate: dispense?.retract.blowout?.params?.flowRate ?? 0, + flowRate: dispenseFlowRateFields.dispense_flowRate ?? 0, }, } if (liquidHandlingAction === 'all') { return { ...state, + sourceWells: adjustedSourceWells, + destinationWells: adjustedDestinationWells, ...aspirateState, ...dispenseState, } @@ -498,11 +551,15 @@ const getLiquidClassValues = ( if (liquidHandlingAction === 'aspirate') { return { ...state, + sourceWells: adjustedSourceWells, + destinationWells: adjustedDestinationWells, ...aspirateState, } } else { return { ...state, + sourceWells: adjustedSourceWells, + destinationWells: adjustedDestinationWells, ...dispenseState, } } diff --git a/app/src/redux/config/constants.ts b/app/src/redux/config/constants.ts index 0bc3aa7f0d6..8ba3ee6d75c 100644 --- a/app/src/redux/config/constants.ts +++ b/app/src/redux/config/constants.ts @@ -8,8 +8,9 @@ export const DEV_INTERNAL_FLAGS: DevInternalFlag[] = [ 'enableLabwareCreator', 'reactQueryDevtools', 'reactScan', - 'quickTransferExportPython', + 'quickTransferExportJSON', 'camera', + 'quickTransferProtocolContentsLog', ] // action type constants diff --git a/app/src/redux/config/schema-types.ts b/app/src/redux/config/schema-types.ts index 43b6418c639..3dfc045f78f 100644 --- a/app/src/redux/config/schema-types.ts +++ b/app/src/redux/config/schema-types.ts @@ -16,8 +16,9 @@ export type DevInternalFlag = | 'enableLabwareCreator' | 'reactQueryDevtools' | 'reactScan' - | 'quickTransferExportPython' + | 'quickTransferExportJSON' | 'camera' + | 'quickTransferProtocolContentsLog' export type FeatureFlags = Partial> diff --git a/app/src/transformations/analysis/__tests__/liquids.test.ts b/app/src/transformations/analysis/__tests__/liquids.test.ts index ee28c605c33..0ae39fbe764 100644 --- a/app/src/transformations/analysis/__tests__/liquids.test.ts +++ b/app/src/transformations/analysis/__tests__/liquids.test.ts @@ -7,41 +7,15 @@ import { getLiquidsByIdForLabware, getTotalVolumePerLiquidId, getTotalVolumePerLiquidLabwarePair, - getWellFillFromLabwareId, getWellGroupForLiquidId, } from '../liquids' -import type { LabwareByLiquidId, Liquid } from '@opentrons/shared-data' +import type { LabwareByLiquidId } from '@opentrons/shared-data' const LABWARE_ID = '60e8b050-3412-11eb-ad93-ed232a2337cf:opentrons/corning_24_wellplate_3.4ml_flat/1' const LIQUID_ID = '7' -const MOCK_LIQUIDS_IN_LOAD_ORDER: Liquid[] = [ - { - description: 'water', - displayColor: '#00d781', - displayName: 'liquid 2', - id: '7', - }, - { - description: 'saline', - displayColor: '#0076ff', - displayName: 'liquid 1', - id: '123', - }, - { - description: 'reagent', - displayColor: '#ff4888', - displayName: 'liquid 3', - id: '19', - }, - { - description: 'saliva', - displayColor: '#B925FF', - displayName: 'liquid 4', - id: '4', - }, -] + const MOCK_LABWARE_BY_LIQUID_ID: LabwareByLiquidId = { '4': [ { @@ -202,44 +176,6 @@ const MOCK_VOLUME_BY_WELL = { D4: 100, } -describe('getWellFillFromLabwareId', () => { - it('returns wellfill object for the labwareId', () => { - const expected = { - A1: '#00d781', - A2: '#00d781', - A3: '#B925FF', - A4: '#B925FF', - A5: '#ff4888', - A6: '#ff4888', - B1: '#00d781', - B2: '#00d781', - B3: '#B925FF', - B4: '#B925FF', - B5: '#ff4888', - B6: '#ff4888', - C1: '#00d781', - C2: '#00d781', - C3: '#B925FF', - C4: '#B925FF', - C5: '#ff4888', - C6: '#ff4888', - D1: '#00d781', - D2: '#00d781', - D3: '#B925FF', - D4: '#B925FF', - D5: '#ff4888', - D6: '#ff4888', - } - expect( - getWellFillFromLabwareId( - LABWARE_ID, - MOCK_LIQUIDS_IN_LOAD_ORDER, - MOCK_LABWARE_BY_LIQUID_ID - ) - ).toEqual(expected) - }) -}) - describe('getTotalVolumePerLiquidId', () => { it('returns volume of liquid needed accross all labware', () => { const expected = 1000 diff --git a/app/src/transformations/analysis/liquids.ts b/app/src/transformations/analysis/liquids.ts index 30f05a12009..9f6de8cf442 100644 --- a/app/src/transformations/analysis/liquids.ts +++ b/app/src/transformations/analysis/liquids.ts @@ -3,33 +3,6 @@ import { COLORS } from '@opentrons/components' import type { WellGroup } from '@opentrons/components' import type { LabwareByLiquidId, Liquid } from '@opentrons/shared-data' -export function getWellFillFromLabwareId( - labwareId: string, - liquidsInLoadOrder: Liquid[], - labwareByLiquidId: LabwareByLiquidId -): { [well: string]: string } { - let labwareWellFill: { [well: string]: string } = {} - const liquidIds = Object.keys(labwareByLiquidId) - const labwareInfo = Object.values(labwareByLiquidId) - - labwareInfo.forEach((labwareArray, index) => { - labwareArray.forEach(labware => { - if (labware.labwareId === labwareId) { - const liquidId = liquidIds[index] - const liquid = liquidsInLoadOrder.find(liquid => liquid.id === liquidId) - const wellFill: { - [well: string]: string - } = {} - Object.keys(labware.volumeByWell).forEach(key => { - wellFill[key] = liquid?.displayColor ?? COLORS.transparent - }) - labwareWellFill = { ...labwareWellFill, ...wellFill } - } - }) - }) - return labwareWellFill -} - export function getDisabledWellFillFromLabwareId( labwareId: string, liquidsInLoadOrder: Liquid[], diff --git a/components/src/assets/localization/en/protocol_command_text.json b/components/src/assets/localization/en/protocol_command_text.json index 3efea4f1ca1..d045b10226e 100644 --- a/components/src/assets/localization/en/protocol_command_text.json +++ b/components/src/assets/localization/en/protocol_command_text.json @@ -69,6 +69,7 @@ "move_to_coordinates": "Moving to (X: {{x}}, Y: {{y}}, Z: {{z}})", "move_to_slot": "Moving to Slot {{slot_name}}", "move_to_well": "Moving to X {{xOffset}} Y {{yOffset}} Z {{zOffset}} relative to {{positionRelative}} of well {{wellName}} of {{labware}} in {{displayLocation}}", + "ninety_six_channel_cam": "pipette tip attach cam", "multiple": "multiple", "notes": "notes", "off_deck": "off deck", diff --git a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx index eadecf7a466..74a5b4a7060 100644 --- a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx +++ b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx @@ -13,7 +13,7 @@ import { BaseDeck } from '../BaseDeck' import { LabwareRender } from '../Labware' import { resolveLabwareLocation } from './resolveLabwareLocation' -import type { PropsWithChildren, ReactNode } from 'react' +import type { PropsWithChildren } from 'react' import type { DeckConfiguration, DeckDefinition, @@ -24,7 +24,7 @@ import type { RobotType, Vector3D, } from '@opentrons/shared-data' -import type { ModuleOnDeck } from '../../hardware-sim/BaseDeck' +import type { LabwareOnDeck, ModuleOnDeck } from '../../hardware-sim/BaseDeck' import type { StyleProps } from '../../primitives' const SPLASH_Y_BUFFER_MM = 10 @@ -37,9 +37,9 @@ interface MoveLabwareOnDeckProps extends StyleProps { loadedModules: LoadedModule[] loadedLabware: LoadedLabware[] modulesOnDeck?: ModuleOnDeck[] + labwareOnDeck?: LabwareOnDeck[] labwareDefinitions: LabwareDefinition[] deckConfig: DeckConfiguration - backgroundItems?: ReactNode deckFill?: string } export function MoveLabwareOnDeck( @@ -50,12 +50,12 @@ export function MoveLabwareOnDeck( movedLabwareDef, loadedLabware, modulesOnDeck, + labwareOnDeck, labwareDefinitions, initialLabwareLocation, finalLabwareLocation, loadedModules, deckConfig, - backgroundItems = null, ...styleProps } = props const deckDef = useMemo(() => getDeckDefFromRobotType(robotType), [robotType]) @@ -163,6 +163,7 @@ export function MoveLabwareOnDeck( deckConfig={deckConfig} robotType={robotType} modulesOnDeck={modulesOnDeck} + labwareOnDeck={labwareOnDeck} svgProps={{ style: { opacity: springProps.deckOpacity }, ...styleProps, @@ -171,7 +172,6 @@ export function MoveLabwareOnDeck( // add fixedTrash not to display trash bin on OT-2 deck deckLayerBlocklist={robotType === 'OT-2 Standard' ? ['fixedTrash'] : []} > - {backgroundItems} { leftPlunger: 6, rightPlunger: 7, extensionJaw: 8, + axis96ChannelCam: 9, }, }, }) screen.getByText( - 'Moving robot to (X: 1, Y: 2, left Z: 3, right Z: 4, extension Z: 5, left plunger: 6, right plunger: 7, extension jaw: 8)' + 'Moving robot to (X: 1, Y: 2, left Z: 3, right Z: 4, extension Z: 5, left plunger: 6, right plunger: 7, extension jaw: 8, pipette tip attach cam: 9)' ) }) it('should render moveAxesTo with not all axes', () => { @@ -92,11 +93,12 @@ describe('getRobotCommandText', () => { leftPlunger: 6, rightPlunger: 7, extensionJaw: 8, + axis96ChannelCam: 9, }, }, }) screen.getByText( - 'Moving robot by (X: 1, Y: 2, left Z: 3, right Z: 4, extension Z: 5, left plunger: 6, right plunger: 7, extension jaw: 8)' + 'Moving robot by (X: 1, Y: 2, left Z: 3, right Z: 4, extension Z: 5, left plunger: 6, right plunger: 7, extension jaw: 8, pipette tip attach cam: 9)' ) }) it('should render moveAxesRelative with not all axes', () => { diff --git a/components/src/organisms/CommandText/useCommandTextString/utils/commandText/getRobotCommandText.ts b/components/src/organisms/CommandText/useCommandTextString/utils/commandText/getRobotCommandText.ts index 1c43033c6b8..1bea2c8c66b 100644 --- a/components/src/organisms/CommandText/useCommandTextString/utils/commandText/getRobotCommandText.ts +++ b/components/src/organisms/CommandText/useCommandTextString/utils/commandText/getRobotCommandText.ts @@ -22,6 +22,7 @@ const formatAxisMap = (axisMap: RobotMotorAxisMap, t: TFunction): string => { 'leftPlunger', 'rightPlunger', 'extensionJaw', + 'axis96ChannelCam', ] as const const names: Record = { x: 'X', @@ -32,6 +33,7 @@ const formatAxisMap = (axisMap: RobotMotorAxisMap, t: TFunction): string => { rightPlunger: t('right_plunger'), extensionZ: t('extension_z'), extensionJaw: t('extension_jaw'), + axis96ChannelCam: t('ninety_six_channel_cam'), } const coordinateStr = sortedAxes .map(axis => { diff --git a/components/src/organisms/ProtocolDeck/index.tsx b/components/src/organisms/ProtocolDeck/index.tsx index b9f28a955fb..5c29d59a1d9 100644 --- a/components/src/organisms/ProtocolDeck/index.tsx +++ b/components/src/organisms/ProtocolDeck/index.tsx @@ -68,7 +68,8 @@ export function ProtocolDeck(props: ProtocolDeckProps): JSX.Element | null { ? getWellFillFromLabwareId( topLabwareInfo.labwareId, protocolAnalysis.liquids, - labwareByLiquidId + labwareByLiquidId, + protocolAnalysis.commands ) : undefined, } @@ -90,7 +91,8 @@ export function ProtocolDeck(props: ProtocolDeckProps): JSX.Element | null { wellFill: getWellFillFromLabwareId( topLabwareInfo.labwareId, protocolAnalysis.liquids, - labwareByLiquidId + labwareByLiquidId, + protocolAnalysis.commands ), } : null diff --git a/hardware-testing/hardware_testing/gravimetric/protocol_replacement/gravimetric.py b/hardware-testing/hardware_testing/gravimetric/protocol_replacement/gravimetric.py index 6ad99193891..ae0a8065910 100644 --- a/hardware-testing/hardware_testing/gravimetric/protocol_replacement/gravimetric.py +++ b/hardware-testing/hardware_testing/gravimetric/protocol_replacement/gravimetric.py @@ -36,7 +36,7 @@ from opentrons.protocols.advanced_control.transfers import common as tx_ctl_lib metadata = {"protocolName": "Gravimetric QC"} -requirements = {"robotType": "Flex", "apiLevel": "2.25"} +requirements = {"robotType": "Flex", "apiLevel": "2.26"} SCALE_SECONDS_TO_TRUE_STABILIZE = 60 * 3 diff --git a/hardware-testing/hardware_testing/modules/flex_stacker/stacker_qc_protocol.py b/hardware-testing/hardware_testing/modules/flex_stacker/stacker_qc_protocol_v1.1.py similarity index 75% rename from hardware-testing/hardware_testing/modules/flex_stacker/stacker_qc_protocol.py rename to hardware-testing/hardware_testing/modules/flex_stacker/stacker_qc_protocol_v1.1.py index 2d9097cd1d1..91f009e7a39 100644 --- a/hardware-testing/hardware_testing/modules/flex_stacker/stacker_qc_protocol.py +++ b/hardware-testing/hardware_testing/modules/flex_stacker/stacker_qc_protocol_v1.1.py @@ -5,7 +5,7 @@ ) metadata = { - "protocolName": "Flex Stacker PVT QC", + "protocolName": "Flex Stacker PVT QC V1.1", "author": "Opentrons ", } requirements = { @@ -57,7 +57,7 @@ def run(protocol: ProtocolContext) -> None: protocol.move_labware(tiprack, stacker, use_gripper=True) stacker.store() - # =================== FILL TIPRACKS WITH PCR PLATES ====================== + # =================== FILL STACKERS WITH PCR PLATES ====================== stacker.empty("Empty all tipracks from the hopper and load in 6 PCR plates.") stacker.set_stored_labware( @@ -75,3 +75,22 @@ def run(protocol: ProtocolContext) -> None: for plate in plates: protocol.move_labware(plate, stacker, use_gripper=True) stacker.store() + + # =================== FILL STACKERS WITH 384 wells PLATES ====================== + + stacker.empty("Empty all PCR plates from the hopper and load in 6 384 plates.") + stacker.set_stored_labware( + load_name="biorad_384_wellplate_50ul", + count=6, + ) + + # ======================= RETRIEVE/STORE 384 wells PLATES ====================== + plates = [] + for slot in SLOTS: + plate = stacker.retrieve() + protocol.move_labware(plate, slot, use_gripper=True) + plates.append(plate) + + for plate in plates: + protocol.move_labware(plate, stacker, use_gripper=True) + stacker.store() diff --git a/hardware-testing/hardware_testing/protocols/universal_photometric.py b/hardware-testing/hardware_testing/protocols/universal_photometric.py index 97fd9e0dfb5..0eff8fb3f47 100644 --- a/hardware-testing/hardware_testing/protocols/universal_photometric.py +++ b/hardware-testing/hardware_testing/protocols/universal_photometric.py @@ -20,7 +20,7 @@ metadata = {"protocolName": "96ch Universal Photometric Protocol"} -requirements = {"robotType": "Flex", "apiLevel": "2.24"} +requirements = {"robotType": "Flex", "apiLevel": "2.26"} DYE_RESERVOIR_DEAD_VOLUME = 20000 # 20k uL @@ -402,7 +402,7 @@ def _validate_dye_liquid_height(trial: int) -> None: target_volume = ctx.params.target_volume # type: ignore [attr-defined] def _get_transfer_settings(tiprack: Labware, first_trial: bool) -> LiquidClass: - liquid_class = ctx.get_liquid_class("water") + liquid_class = ctx.get_liquid_class("water", version=2) transfer_properties = liquid_class.get_for(pip, tiprack) asp_offset = Coordinate(x=0, y=0, z=-1 * ctx.params.asp_sub_depth) # type: ignore [attr-defined] diff --git a/hardware/opentrons_hardware/hardware_control/types.py b/hardware/opentrons_hardware/hardware_control/types.py index bd91f0562d9..019383ac8f6 100644 --- a/hardware/opentrons_hardware/hardware_control/types.py +++ b/hardware/opentrons_hardware/hardware_control/types.py @@ -1,7 +1,9 @@ """Types and definitions for hardware bindings.""" -from typing import Mapping, TypeVar, Dict, List, Optional, Tuple +import re from dataclasses import dataclass from enum import Enum +from typing import Mapping, TypeVar, Dict, List, Optional, Tuple +from functools import total_ordering from opentrons_hardware.firmware_bindings.constants import NodeId, MoveAckId @@ -14,7 +16,8 @@ NodeDict = Dict[NodeId, MapPayload] -@dataclass +@total_ordering +@dataclass(frozen=True) class PCBARevision: """The electrical revision of a PCBA.""" @@ -23,6 +26,36 @@ class PCBARevision: tertiary: Optional[str] = None #: An often-not-present tertiary + @classmethod + def from_string(cls, rev: str) -> "PCBARevision": + """Parse a revision string of the form 'xy.z'.""" + match = re.match(r"^([A-Za-z]\d+)(?:\.(\d+))?$", rev) + if not match: + raise ValueError(f"Invalid revision format: {rev}") + main, tertiary = match.groups() + return cls(main, tertiary) + + def __repr__(self) -> str: + """Readable representation of the PCB revision.""" + return f"{self.main}.{self.tertiary or 0}".upper() + + def _as_tuple(self) -> Tuple[str, int, int]: + if not self.main: + return ("", 0, 0) + + prim = self.main[0] + sec = int(self.main[1:]) if len(self.main) > 1 else 0 + tert = int(self.tertiary) if self.tertiary else 0 + return (prim, sec, tert) + + def __gt__(self, other: "PCBARevision") -> bool: + return self._as_tuple() > other._as_tuple() + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PCBARevision): + return False + return self._as_tuple() == other._as_tuple() + class MoveCompleteAck(Enum): """Move Complete Ack.""" diff --git a/labware-library/src/components/labware-ui/labware-images.ts b/labware-library/src/components/labware-ui/labware-images.ts index 826d665a6a2..df98091a822 100644 --- a/labware-library/src/components/labware-ui/labware-images.ts +++ b/labware-library/src/components/labware-ui/labware-images.ts @@ -44,6 +44,10 @@ export const labwareImages: Record = { import.meta.url ).href, ], + corning_96_wellplate_360ul_lid: [ + new URL('../../images/corning_96_wellplate_360ul_lid.png', import.meta.url) + .href, + ], 'corning_48_wellplate_1.6ml_flat': [ new URL( '../../images/corning_48_wellplate_1.6ml_flat_photo_three_quarters.jpg', @@ -78,6 +82,14 @@ export const labwareImages: Record = { .href, new URL('../../images/geb_10ul_tip_side_view.jpg', import.meta.url).href, ], + milliplex_r_96_well_microtiter_plate: [ + new URL('../../images/milliplex_microtiter_plate.png', import.meta.url) + .href, + ], + black_96_well_microtiter_plate_lid: [ + new URL('../../images/milliplex_microtiter_plate_lid.png', import.meta.url) + .href, + ], nest_1_reservoir_195ml: [ new URL( '../../images/nest_1_reservoir_195ml_three_quarters.jpg', @@ -87,12 +99,21 @@ export const labwareImages: Record = { nest_1_reservoir_290ml: [ new URL('../../images/nest_1_reservoir_290ml.jpg', import.meta.url).href, ], + nest_8_reservoir_22ml: [ + new URL('../../images/nest_8_reservoir_22ml.png', import.meta.url).href, + ], nest_12_reservoir_15ml: [ new URL( '../../images/nest_12_reservoir_15ml_three_quarters.jpg', import.meta.url ).href, ], + nest_12_reservoir_22ml: [ + new URL('../../images/nest_12_reservoir_22ml.png', import.meta.url).href, + ], + 'nest_24_wellplate_10.4ml': [ + new URL('../../images/nest_24_wellplate_10.4ml.png', import.meta.url).href, + ], nest_96_wellplate_100ul_pcr_full_skirt: [ new URL( '../../images/nest_96_wellplate_100ul_pcr_full_skirt_three_quarters.jpg', @@ -462,6 +483,10 @@ export const labwareImages: Record = { import.meta.url ).href, ], + opentrons_flex_tiprack_lid: [ + new URL('../../images/opentrons_flex_tiprack_lid.png', import.meta.url) + .href, + ], opentrons_96_wellplate_200ul_pcr_full_skirt: [ new URL( '../../images/opentrons_96_wellplate_200ul_pcr_full_skirt.jpg', @@ -474,12 +499,34 @@ export const labwareImages: Record = { import.meta.url ).href, ], + opentrons_tough_1_reservoir_300ml: [ + new URL( + '../../images/opentrons_tough_1_reservoir_300ml.png', + import.meta.url + ).href, + ], + opentrons_tough_4_reservoir_72ml: [ + new URL( + '../../images/opentrons_tough_4_reservoir_72ml.png', + import.meta.url + ).href, + ], + opentrons_tough_12_reservoir_22ml: [ + new URL( + '../../images/opentrons_tough_12_reservoir_22ml.png', + import.meta.url + ).href, + ], opentrons_tough_pcr_auto_sealing_lid: [ new URL( '../../images/opentrons_tough_pcr_auto_sealing_lid.jpg', import.meta.url ).href, ], + opentrons_tough_universal_lid: [ + new URL('../../images/opentrons_tough_universal_lid.jpg', import.meta.url) + .href, + ], opentrons_flex_deck_riser: [ new URL('../../images/opentrons_flex_deck_riser.png', import.meta.url).href, ], @@ -503,9 +550,8 @@ export const labwareImages: Record = { import.meta.url ).href, ], - axygen_96_well_plate_500_µL: [ - new URL('../../images/axygen_96_well_plate_500uL.png', import.meta.url) - .href, + axygen_96_wellplate_500ul: [ + new URL('../../images/axygen_96_wellplate_500ul.png', import.meta.url).href, ], smc_384_read_plate: [ new URL('../../images/smc_384_read_plate.png', import.meta.url).href, @@ -516,4 +562,10 @@ export const labwareImages: Record = { import.meta.url ).href, ], + ibidi_96_square_well_plate_300ul_lid: [ + new URL( + '../../images/ibidi_96_square_well_plate_300ul_lid.png', + import.meta.url + ).href, + ], } diff --git a/labware-library/src/images/axygen_96_well_plate_500uL.png b/labware-library/src/images/axygen_96_well_plate_500uL.png deleted file mode 100644 index 9894d528100..00000000000 Binary files a/labware-library/src/images/axygen_96_well_plate_500uL.png and /dev/null differ diff --git a/labware-library/src/images/axygen_96_wellplate_500ul.png b/labware-library/src/images/axygen_96_wellplate_500ul.png new file mode 100644 index 00000000000..f2ba0ba5176 Binary files /dev/null and b/labware-library/src/images/axygen_96_wellplate_500ul.png differ diff --git a/labware-library/src/images/corning_96_wellplate_360ul_lid.png b/labware-library/src/images/corning_96_wellplate_360ul_lid.png new file mode 100644 index 00000000000..2e58cce4481 Binary files /dev/null and b/labware-library/src/images/corning_96_wellplate_360ul_lid.png differ diff --git a/labware-library/src/images/ibidi_96_square_well_plate_300ul_lid.png b/labware-library/src/images/ibidi_96_square_well_plate_300ul_lid.png new file mode 100644 index 00000000000..4ad44d8ceab Binary files /dev/null and b/labware-library/src/images/ibidi_96_square_well_plate_300ul_lid.png differ diff --git a/labware-library/src/images/milliplex_microtiter_plate.png b/labware-library/src/images/milliplex_microtiter_plate.png new file mode 100644 index 00000000000..98b96901e12 Binary files /dev/null and b/labware-library/src/images/milliplex_microtiter_plate.png differ diff --git a/labware-library/src/images/milliplex_microtiter_plate_lid.png b/labware-library/src/images/milliplex_microtiter_plate_lid.png new file mode 100644 index 00000000000..6788660b8e3 Binary files /dev/null and b/labware-library/src/images/milliplex_microtiter_plate_lid.png differ diff --git a/labware-library/src/images/nest_12_reservoir_22ml.png b/labware-library/src/images/nest_12_reservoir_22ml.png new file mode 100644 index 00000000000..da7d2ee66d7 Binary files /dev/null and b/labware-library/src/images/nest_12_reservoir_22ml.png differ diff --git a/labware-library/src/images/nest_24_wellplate_10.4ml.png b/labware-library/src/images/nest_24_wellplate_10.4ml.png new file mode 100644 index 00000000000..1210c4dddee Binary files /dev/null and b/labware-library/src/images/nest_24_wellplate_10.4ml.png differ diff --git a/labware-library/src/images/nest_8_reservoir_22ml.png b/labware-library/src/images/nest_8_reservoir_22ml.png new file mode 100644 index 00000000000..b596ab84d6a Binary files /dev/null and b/labware-library/src/images/nest_8_reservoir_22ml.png differ diff --git a/labware-library/src/images/opentrons_flex_tiprack_lid.png b/labware-library/src/images/opentrons_flex_tiprack_lid.png new file mode 100644 index 00000000000..13c1706f55b Binary files /dev/null and b/labware-library/src/images/opentrons_flex_tiprack_lid.png differ diff --git a/labware-library/src/images/opentrons_tough_12_reservoir_22ml.png b/labware-library/src/images/opentrons_tough_12_reservoir_22ml.png new file mode 100644 index 00000000000..5d81c0e3d95 Binary files /dev/null and b/labware-library/src/images/opentrons_tough_12_reservoir_22ml.png differ diff --git a/labware-library/src/images/opentrons_tough_1_reservoir_300ml.png b/labware-library/src/images/opentrons_tough_1_reservoir_300ml.png new file mode 100644 index 00000000000..88197d183b0 Binary files /dev/null and b/labware-library/src/images/opentrons_tough_1_reservoir_300ml.png differ diff --git a/labware-library/src/images/opentrons_tough_4_reservoir_72ml.png b/labware-library/src/images/opentrons_tough_4_reservoir_72ml.png new file mode 100644 index 00000000000..b5663339019 Binary files /dev/null and b/labware-library/src/images/opentrons_tough_4_reservoir_72ml.png differ diff --git a/labware-library/src/images/opentrons_tough_universal_lid.jpg b/labware-library/src/images/opentrons_tough_universal_lid.jpg new file mode 100644 index 00000000000..d63cc71aa73 Binary files /dev/null and b/labware-library/src/images/opentrons_tough_universal_lid.jpg differ diff --git a/opentrons-ai-client/src/assets/images/labwares/black_96_well_microtiter_plate_lid.jpg b/opentrons-ai-client/src/assets/images/labwares/black_96_well_microtiter_plate_lid.jpg new file mode 100644 index 00000000000..63acaeb02b5 Binary files /dev/null and b/opentrons-ai-client/src/assets/images/labwares/black_96_well_microtiter_plate_lid.jpg differ diff --git a/opentrons-ai-client/src/assets/images/labwares/milliplex_r_96_well_microtiter_plate.jpg b/opentrons-ai-client/src/assets/images/labwares/milliplex_r_96_well_microtiter_plate.jpg new file mode 100644 index 00000000000..9debd94b385 Binary files /dev/null and b/opentrons-ai-client/src/assets/images/labwares/milliplex_r_96_well_microtiter_plate.jpg differ diff --git a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json index 5b38d1277c0..1edfa144400 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json @@ -4391,7 +4391,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4453,7 +4453,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4570,7 +4570,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4632,7 +4632,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4749,7 +4749,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4811,7 +4811,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4928,7 +4928,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4990,7 +4990,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -5107,7 +5107,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -5169,7 +5169,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -5286,7 +5286,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -5348,7 +5348,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -5465,7 +5465,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -5527,7 +5527,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -5644,7 +5644,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -5706,7 +5706,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -5823,7 +5823,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -5885,7 +5885,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -6002,7 +6002,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -6064,7 +6064,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -6181,7 +6181,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -6243,7 +6243,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -6360,7 +6360,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -6422,7 +6422,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -6539,7 +6539,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -6601,7 +6601,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -6718,7 +6718,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -6780,7 +6780,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -6897,7 +6897,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -6959,7 +6959,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -7076,7 +7076,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -7138,7 +7138,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -7455,7 +7455,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -7535,7 +7535,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -7615,7 +7615,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -7695,7 +7695,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -7775,7 +7775,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -7855,7 +7855,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -7935,7 +7935,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -8015,7 +8015,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -8095,7 +8095,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -8175,7 +8175,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -8273,7 +8273,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -8343,7 +8343,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -8423,7 +8423,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -8503,7 +8503,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -8583,7 +8583,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -8663,7 +8663,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -8743,7 +8743,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -8823,7 +8823,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -8903,7 +8903,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -8983,7 +8983,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -9063,7 +9063,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -9161,7 +9161,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -9231,7 +9231,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -9311,7 +9311,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -9391,7 +9391,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -9471,7 +9471,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -9551,7 +9551,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -9649,7 +9649,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 diff --git a/protocol-designer/fixtures/protocol/8/doItAllV8.json b/protocol-designer/fixtures/protocol/8/doItAllV8.json index 50d36d169b0..88d90162ee1 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV8.json @@ -14,7 +14,7 @@ }, "designerApplication": { "name": "opentrons/protocol-designer", - "version": "8.5.0", + "version": "8.5.5", "data": { "_internalAppBuildDate": "Mon, 16 Jun 2025 20:43:09 GMT", "pipetteTiprackAssignments": { @@ -3793,7 +3793,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -3855,7 +3855,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -3971,7 +3971,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4033,7 +4033,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4149,7 +4149,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4211,7 +4211,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4327,7 +4327,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4389,7 +4389,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4505,7 +4505,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4567,7 +4567,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4683,7 +4683,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4745,7 +4745,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4861,7 +4861,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -4923,7 +4923,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -5039,7 +5039,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 @@ -5101,7 +5101,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 diff --git a/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json b/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json index 806ad6be2ac..b353ccf2bb1 100644 --- a/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json +++ b/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json @@ -3870,7 +3870,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } }, "speed": 100 diff --git a/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json index da0ca1a77b3..ea47ae2404a 100644 --- a/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json +++ b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json @@ -2525,7 +2525,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } } } @@ -2673,7 +2673,7 @@ "offset": { "x": 0, "y": 0, - "z": 0 + "z": 1 } } } diff --git a/protocol-designer/release-notes.md b/protocol-designer/release-notes.md index cb4352a7ad5..5f0036e0b35 100644 --- a/protocol-designer/release-notes.md +++ b/protocol-designer/release-notes.md @@ -8,6 +8,31 @@ By using Opentrons Protocol Designer, you agree to the Opentrons End-User Licens --- +## Opentrons Protocol Designer Changes in 8.5.6 + +**Welcome to Protocol Designer 8.5.6!** + +This hotfix release addresses a bug when dispensing into a trash bin or waste chute. + +## Opentrons Protocol Designer Changes in 8.5.5 + +**Welcome to Protocol Designer 8.5.5!** + +This hotfix release addresses a bug to allow full use of pipettes and tip racks during liquid class transfers. + +## Opentrons Protocol Designer Changes in 8.5.4 + +**Welcome to Protocol Designer 8.5.4!** + +This hotfix release addresses several bugs. + +### Bug Fixes + +- Set a custom aspirate tip position for any dispense location. +- Protocol Designer correctly reassigns default tip settings when changing pipettes in your protocol. +- Protocol Designer no longer crashes when encountering missing tip rack errors in imported protocols. +- Aspirate and dispense tip positions default to 1 mm above the well bottom if unspecified in your protocols. + ## Opentrons Protocol Designer Changes in 8.5.3 **Welcome to Protocol Designer 8.5.3!** @@ -16,11 +41,15 @@ This hotfix release addresses several bugs. ### Bug Fixes +- Crashes and protocol loss no longer occur when: - Crashes and protocol loss no longer occur when: - deleting a pipette involved in a mix step. - deleting a Protocol Designer step title. - checking labware details after deleting a liquid. + - deleting a pipette involved in a mix step. + - deleting a Protocol Designer step title. + - checking labware details after deleting a liquid. - All staging area slots remain when adding new staging areas after initial deck setup. diff --git a/protocol-designer/src/assets/localization/en/protocol_command_text.json b/protocol-designer/src/assets/localization/en/protocol_command_text.json index 3efea4f1ca1..3da08ea33cf 100644 --- a/protocol-designer/src/assets/localization/en/protocol_command_text.json +++ b/protocol-designer/src/assets/localization/en/protocol_command_text.json @@ -70,6 +70,7 @@ "move_to_slot": "Moving to Slot {{slot_name}}", "move_to_well": "Moving to X {{xOffset}} Y {{yOffset}} Z {{zOffset}} relative to {{positionRelative}} of well {{wellName}} of {{labware}} in {{displayLocation}}", "multiple": "multiple", + "ninety_six_channel_cam": "pipette tip attach cam", "notes": "notes", "off_deck": "off deck", "offdeck": "offdeck", diff --git a/protocol-designer/src/components/organisms/EditInstrumentsModal/editPipettes.ts b/protocol-designer/src/components/organisms/EditInstrumentsModal/editPipettes.ts index f7c08047a17..1ae8cce6cde 100644 --- a/protocol-designer/src/components/organisms/EditInstrumentsModal/editPipettes.ts +++ b/protocol-designer/src/components/organisms/EditInstrumentsModal/editPipettes.ts @@ -213,6 +213,7 @@ export const editPipettes = ( substitutionMap, startStepId: orderedStepIds[0], endStepId: last(orderedStepIds) ?? '', + newTiprackURI: Array.from(newTiprackUris)[0], }) ) } diff --git a/protocol-designer/src/file-data/__tests__/createFile.test.ts b/protocol-designer/src/file-data/__tests__/createFile.test.ts index 041f750e2c0..93a724744a7 100644 --- a/protocol-designer/src/file-data/__tests__/createFile.test.ts +++ b/protocol-designer/src/file-data/__tests__/createFile.test.ts @@ -285,7 +285,7 @@ CUSTOM_LABWARE = json.loads("""{"fixture/fixture_trash/1":{"ordering":[["A1"]]," }, }, }, - version: '8.5.0', + version: '8.5.5', name: 'opentrons/protocol-designer', }, robot: { model: OT2_ROBOT_TYPE }, diff --git a/protocol-designer/src/load-file/migration/8_5_5.ts b/protocol-designer/src/load-file/migration/8_5_5.ts new file mode 100644 index 00000000000..9b4f688c24e --- /dev/null +++ b/protocol-designer/src/load-file/migration/8_5_5.ts @@ -0,0 +1,57 @@ +import type { ProtocolFile } from '@opentrons/shared-data' +import type { PDMetadata } from '../../file-types' + +export const migrateFile = ( + appData: ProtocolFile +): ProtocolFile => { + const { designerApplication } = appData + + if (designerApplication == null || designerApplication?.data == null) { + throw Error('The designerApplication key in your file is corrupt.') + } + const { + savedStepForms, + pipetteTiprackAssignments, + labware, + } = designerApplication.data + + const savedStepsWithUpdatedPipettingFields = Object.entries( + savedStepForms + ).reduce((acc: typeof savedStepForms, [stepId, form]) => { + if (form.stepType === 'moveLiquid' || form.stepType === 'mix') { + const { tipRack, pipette } = form + const assignedTipracks = pipetteTiprackAssignments[pipette] ?? [] + + // if the tiprack assigned in the form step isn't in the pipette's assigned tipracks + // then update it + if (!assignedTipracks.includes(tipRack as string)) { + // check that the new assigned tiprack is even a labware entity, + // otherwise default to null + const newLoadLabwareInfo = Object.values(labware).find(lw => + assignedTipracks.includes(lw.labwareDefURI) + ) + acc[stepId] = { + ...form, + tipRack: newLoadLabwareInfo?.labwareDefURI ?? null, + } + return acc + } + } + acc[stepId] = form + return acc + }, {}) + + return { + ...appData, + designerApplication: { + ...designerApplication, + data: { + ...designerApplication.data, + savedStepForms: { + ...designerApplication.data.savedStepForms, + ...savedStepsWithUpdatedPipettingFields, + }, + }, + }, + } +} diff --git a/protocol-designer/src/load-file/migration/index.ts b/protocol-designer/src/load-file/migration/index.ts index faf3554c631..675395084e2 100644 --- a/protocol-designer/src/load-file/migration/index.ts +++ b/protocol-designer/src/load-file/migration/index.ts @@ -16,6 +16,7 @@ import { migrateFile as migrateFileEightTwo } from './8_2_0' import { migrateFile as migrateFileEightTwoPointTwo } from './8_2_2' import { migrateFile as migrateFileEightFourFour } from './8_4_4' import { migrateFile as migrateFileEightFive } from './8_5_0' +import { migrateFile as migrateFileEightFiveFive } from './8_5_5' import type { PDProtocolFile, @@ -68,6 +69,8 @@ const allMigrationsByVersion: MigrationsByVersion = { '8.4.4': migrateFileEightFourFour, // @ts-expect-error '8.5.0': migrateFileEightFive, + // @ts-expect-error + '8.5.5': migrateFileEightFiveFive, } export const migration = ( file: any diff --git a/protocol-designer/src/load-file/migration/utils/__tests__/getLoadLiquidClassCommands.test.ts b/protocol-designer/src/load-file/migration/utils/__tests__/getLoadLiquidClassCommands.test.ts index 830fee3382b..7e87cdf2d60 100644 --- a/protocol-designer/src/load-file/migration/utils/__tests__/getLoadLiquidClassCommands.test.ts +++ b/protocol-designer/src/load-file/migration/utils/__tests__/getLoadLiquidClassCommands.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { - ETHANOL_LIQUID_CLASS_NAME, + ETHANOL_LIQUID_CLASS_NAME_V2, getAllLiquidClassDefs, - GLYCEROL_LIQUID_CLASS_NAME, + GLYCEROL_LIQUID_CLASS_NAME_V2, } from '@opentrons/shared-data' import { getLoadLiquidClassCommands } from '../getLoadLiquidClassCommands' @@ -32,14 +32,14 @@ const MOCK_PIPETTE_ENTITIES = ({ const MOCK_SAVED_STEP_FORMS = ({ step0: { pipette: 'mockPipette1', - liquidClass: ETHANOL_LIQUID_CLASS_NAME, + liquidClass: ETHANOL_LIQUID_CLASS_NAME_V2, tipRack: 'mockTipRack1', stepType: 'moveLiquid', id: 'step0', }, step1: { pipette: 'mockPipette1', - liquidClass: GLYCEROL_LIQUID_CLASS_NAME, + liquidClass: GLYCEROL_LIQUID_CLASS_NAME_V2, tipRack: 'mockTipRack3', stepType: 'mix', id: 'step1', @@ -47,7 +47,7 @@ const MOCK_SAVED_STEP_FORMS = ({ } as unknown) as SavedStepFormState const mockLiquidClasses = { - [ETHANOL_LIQUID_CLASS_NAME]: { + [ETHANOL_LIQUID_CLASS_NAME_V2]: { liquidClassName: 'ethanol_80', byPipette: [ { @@ -63,7 +63,7 @@ const mockLiquidClasses = { }, ], }, - [GLYCEROL_LIQUID_CLASS_NAME]: { + [GLYCEROL_LIQUID_CLASS_NAME_V2]: { liquidClassName: 'glycerol_50', byPipette: [ { @@ -126,7 +126,7 @@ describe('getLoadLiquidClassCommands', () => { stepType: 'moveLiquid', id: 'step2', pipette: 'mockPipette1', - liquidClass: GLYCEROL_LIQUID_CLASS_NAME, + liquidClass: GLYCEROL_LIQUID_CLASS_NAME_V2, tipRack: 'mockTipRack3', }, }) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateField.tsx index cf0bab4897f..92954382a6e 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateField.tsx @@ -8,7 +8,7 @@ import { getFlexNameConversion, linearInterpolate, OT2_ROBOT_TYPE, - WATER_LIQUID_CLASS_NAME, + WATER_LIQUID_CLASS_NAME_V2, } from '@opentrons/shared-data' import { getTransferPlanAndReferenceVolumes } from '@opentrons/step-generation' @@ -53,7 +53,7 @@ export function FlowRateField(props: FlowRateFieldProps): JSX.Element { const allLiquidClassDefs = getAllLiquidClassDefs() const liquidClassDef = allLiquidClassDefs[formData?.liquidClass ?? ''] ?? - allLiquidClassDefs[WATER_LIQUID_CLASS_NAME] + allLiquidClassDefs[WATER_LIQUID_CLASS_NAME_V2] const convertedPipetteName = pipette != null ? getFlexNameConversion(pipette.spec) : null const liquidClassValuesForPipette = liquidClassDef.byPipette.find( diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/TiprackField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/TiprackField.tsx index 382ae4dfa8b..59a3420af94 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/TiprackField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/TiprackField.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' @@ -21,12 +20,7 @@ interface TiprackFieldProps extends FieldProps { pipetteId?: unknown } export function TiprackField(props: TiprackFieldProps): JSX.Element { - const { - value, - updateValue, - pipetteId, - padding = `0 ${SPACING.spacing16}`, - } = props + const { value, pipetteId, padding = `0 ${SPACING.spacing16}` } = props const { t } = useTranslation('protocol_steps') const pipetteEntities = useSelector(getPipetteEntities) const options = useSelector(getTiprackOptions) @@ -36,16 +30,6 @@ export function TiprackField(props: TiprackFieldProps): JSX.Element { defaultTiprackUris.includes(option.value) ) - useEffect(() => { - // if default value is not included in the pipette's tiprack uris then - // change it so it is - if ( - !defaultTiprackUris.includes(value as string) && - defaultTiprackUris.length > 0 - ) { - updateValue(defaultTiprackUris[0]) - } - }, [defaultTiprackUris, value, updateValue]) const hasMissingTiprack = defaultTiprackUris.length > tiprackOptions.length return ( <> diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/SecondStepsMoveLiquidTools.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/SecondStepsMoveLiquidTools.tsx index c48fa067cfe..5652a332808 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/SecondStepsMoveLiquidTools.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/SecondStepsMoveLiquidTools.tsx @@ -167,11 +167,18 @@ export const SecondStepsMoveLiquidTools = ({ formData.tipRack, ] ) + const labwareId = formData[`${tab}_labware`] + const shouldCheckLabwareDef = tab === 'aspirate' || !isDestinationTrash + // The getMinXYDimension() call below is crashing quite often, but I'm not sure why + if (shouldCheckLabwareDef && !labwareEntities[labwareId]?.def) { + throw new Error( + `missing ${tab}_labware def for ${labwareId}, ` + + `in labwareEntities: ${!!labwareEntities[labwareId]}` + ) + } const minXYDimension = isDestinationTrash ? null - : getMinXYDimension(labwareEntities[formData[`${tab}_labware`]]?.def, [ - 'A1', - ]) + : getMinXYDimension(labwareEntities[labwareId]?.def, ['A1']) const minRadiusForTouchTip = minXYDimension != null ? round(minXYDimension / 2, 1) : null @@ -311,7 +318,7 @@ export const SecondStepsMoveLiquidTools = ({ /> )} - {isDestinationTrash ? null : ( + {isDestinationTrash && tab === 'dispense' ? null : ( <> { orderedLiquidClassOptions: [ { name: 'mockname', - value: WATER_LIQUID_CLASS_NAME, + value: WATER_LIQUID_CLASS_NAME_V2, subButtonLabel: '', }, ], diff --git a/protocol-designer/src/resources/sentry.ts b/protocol-designer/src/resources/sentry.ts index 31ed08aad5c..f3e6171be82 100644 --- a/protocol-designer/src/resources/sentry.ts +++ b/protocol-designer/src/resources/sentry.ts @@ -41,6 +41,7 @@ export const initializeSentry = (state: BaseState): void => { replayIntegration(), browserTracingIntegration(), ], + attachStacktrace: true, // include stack traces in captureConsoleIntegration tracesSampleRate: 1.0, tracePropagationTargets: [ 'localhost', diff --git a/protocol-designer/src/step-forms/actions/pipettes.ts b/protocol-designer/src/step-forms/actions/pipettes.ts index bb5ceb1a567..ed2c52b1924 100644 --- a/protocol-designer/src/step-forms/actions/pipettes.ts +++ b/protocol-designer/src/step-forms/actions/pipettes.ts @@ -31,6 +31,8 @@ export interface SubstituteStepFormPipettesAction { endStepId: StepIdType // old pipette id -> new id substitutionMap: Record + // 1st assosciated tiprack with the pipetteId + newTiprackURI: string } } export const substituteStepFormPipettes = ( diff --git a/protocol-designer/src/step-forms/reducers/index.ts b/protocol-designer/src/step-forms/reducers/index.ts index 498a143b2e0..c84c0e5da01 100644 --- a/protocol-designer/src/step-forms/reducers/index.ts +++ b/protocol-designer/src/step-forms/reducers/index.ts @@ -206,7 +206,12 @@ export const unsavedForm = ( case 'SUBSTITUTE_STEP_FORM_PIPETTES': { // only substitute unsaved step form if its ID is in the start-end range - const { substitutionMap, startStepId, endStepId } = action.payload + const { + substitutionMap, + startStepId, + endStepId, + newTiprackURI, + } = action.payload const stepIdsToUpdate = getIdsInRange( rootState.orderedStepIds, startStepId, @@ -226,6 +231,7 @@ export const unsavedForm = ( ...handleFormChange( { pipette: substitutionMap[unsavedFormState.pipette], + tipRack: newTiprackURI, }, unsavedFormState, _getPipetteEntitiesRootState(rootState), @@ -865,7 +871,12 @@ export const savedStepForms = ( } case 'SUBSTITUTE_STEP_FORM_PIPETTES': { - const { startStepId, endStepId, substitutionMap } = action.payload + const { + startStepId, + endStepId, + substitutionMap, + newTiprackURI, + } = action.payload const stepIdsToUpdate = getIdsInRange( rootState.orderedStepIds, startStepId, @@ -882,6 +893,7 @@ export const savedStepForms = ( const updatedFields = handleFormChange( { pipette: substitutionMap[prevStepForm.pipette], + tipRack: newTiprackURI, }, prevStepForm, _getPipetteEntitiesRootState(rootState), @@ -1329,7 +1341,10 @@ export const pipetteInvariantProperties: Reducer< acc: NormalizedPipetteById, [id, pipetteLoadInfo]: [string, PipetteLoadInfo] ) => { - const tiprackDefURI = metadata.pipetteTiprackAssignments[id] + const tiprackDefURI = metadata.pipetteTiprackAssignments[id] ?? [] + // If the pipette doesn't exist in the metadata.pipetteTiprackAssignments, + // then the protocol file is malformed, but there's nothing we can do about + // that, so just assign an empty tiprackDefURI to the pipette in that case. const latestTiprackDefURIs = tiprackDefURI.map(uri => getMigratedURI(uri, allLabwareDefs, latestDefs) ) diff --git a/protocol-designer/src/step-forms/test/reducers.test.ts b/protocol-designer/src/step-forms/test/reducers.test.ts index 234a19a31f5..d2dd7f5d3e9 100644 --- a/protocol-designer/src/step-forms/test/reducers.test.ts +++ b/protocol-designer/src/step-forms/test/reducers.test.ts @@ -1450,6 +1450,7 @@ describe('unsavedForm reducer', () => { }, startStepId: '3', endStepId: '5', + newTiprackURI: 'mockURI', }, } const rootState: RootState = { @@ -1483,6 +1484,7 @@ describe('unsavedForm reducer', () => { [ { pipette: 'newPipetteId', + tipRack: 'mockURI', }, rootState.unsavedForm, 'pipetteEntitiesPlaceholder', diff --git a/protocol-designer/src/step-forms/utils/createPresavedStepForm.ts b/protocol-designer/src/step-forms/utils/createPresavedStepForm.ts index 6d1a64d6c17..0e4a655d9d5 100644 --- a/protocol-designer/src/step-forms/utils/createPresavedStepForm.ts +++ b/protocol-designer/src/step-forms/utils/createPresavedStepForm.ts @@ -74,6 +74,13 @@ const _patchDefaultPipette = (args: { orderedStepIds, initialDeckSetup.pipettes ) + const pipetteTipracks = pipetteEntities[defaultPipetteId].tiprackDefURI + const labwareURIsOnDeck = new Set( + Object.values(labwareEntities).map(({ labwareDefURI }) => labwareDefURI) + ) + const firstDefaultTiprackURIOnDeck = pipetteTipracks.find(uri => + labwareURIsOnDeck.has(uri) + ) const hasPartialTipSupportedChannel = pipetteEntities[defaultPipetteId]?.spec.channels !== 1 // If there is a `pipette` field in the form, @@ -88,6 +95,7 @@ const _patchDefaultPipette = (args: { { pipette: defaultPipetteId, nozzles: hasPartialTipSupportedChannel ? ALL : null, + tipRack: firstDefaultTiprackURIOnDeck ?? null, }, formData, pipetteEntities, diff --git a/protocol-designer/src/steplist/fieldLevel/index.ts b/protocol-designer/src/steplist/fieldLevel/index.ts index b6d5d8a6072..6a11dded517 100644 --- a/protocol-designer/src/steplist/fieldLevel/index.ts +++ b/protocol-designer/src/steplist/fieldLevel/index.ts @@ -167,7 +167,7 @@ const stepFieldHelperMap: Record = { castValue: Number, }, aspirate_mmFromBottom: { - castValue: Number, + castValue: numberOrNull, }, aspirate_wells: { maskValue: defaultTo([]), @@ -196,7 +196,7 @@ const stepFieldHelperMap: Record = { castValue: Number, }, dispense_mmFromBottom: { - castValue: Number, + castValue: numberOrNull, }, dispense_wells: { maskValue: defaultTo([]), @@ -333,7 +333,7 @@ const stepFieldHelperMap: Record = { castValue: Number, }, mix_mmFromBottom: { - castValue: Number, + castValue: numberOrNull, }, newLocation: { hydrate: getLabwareLocation, diff --git a/protocol-designer/src/steplist/formLevel/errors.ts b/protocol-designer/src/steplist/formLevel/errors.ts index db9ddf01b85..e2c18b6a708 100644 --- a/protocol-designer/src/steplist/formLevel/errors.ts +++ b/protocol-designer/src/steplist/formLevel/errors.ts @@ -760,8 +760,10 @@ export const volumeTooHigh = ( fields: HydratedMixFormData ): FormError | null => { const { pipette, tipRack } = fields - if (!pipette) { + if (!pipette || !tipRack) { // pipette is null if user deletes pipette + // I haven't been able to reproduce when tipRack alone becomes null, but it + // probably happens if the user deletes or changes the tip racks somehow. return null } const volume = Number(fields.volume) diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts index 864243e23ef..462dd64b8aa 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts @@ -108,24 +108,33 @@ const updatePatchOnPipetteChannelChange = ( const updatePatchOnPipetteChange = ( patch: FormPatch, rawForm: FormData, - pipetteEntities: PipetteEntities + pipetteEntities: PipetteEntities, + labwareEntities: LabwareEntities ): FormPatch => { // when pipette ID is changed (to another ID, or to null), // set any flow rates to null if (fieldHasChanged(rawForm, patch, 'pipette')) { let nozzles: NozzleConfigurationStyle | null = null + let firstDefaultTiprackURIOnDeck: string | null = null const newPipette = patch.pipette if (typeof newPipette === 'string' && newPipette in pipetteEntities) { const hasPartialTipSupportedChannel = pipetteEntities[newPipette].spec.channels !== 1 nozzles = hasPartialTipSupportedChannel ? ALL : null + const pipetteTipracks = pipetteEntities[newPipette].tiprackDefURI + const labwareURIsOnDeck = new Set( + Object.values(labwareEntities).map(({ labwareDefURI }) => labwareDefURI) + ) + firstDefaultTiprackURIOnDeck = + pipetteTipracks?.find(uri => labwareURIsOnDeck.has(uri)) ?? null } return { ...patch, - ...getDefaultFields('aspirate_flowRate', 'dispense_flowRate', 'tipRack'), + ...getDefaultFields('aspirate_flowRate', 'dispense_flowRate'), nozzles, + tipRack: firstDefaultTiprackURIOnDeck, } } @@ -169,7 +178,12 @@ export function dependentFieldsUpdateMix( pipetteEntities ), chainPatch => - updatePatchOnPipetteChange(chainPatch, rawForm, pipetteEntities), + updatePatchOnPipetteChange( + chainPatch, + rawForm, + pipetteEntities, + labwareEntities + ), chainPatch => updatePatchOnTiprackChange(chainPatch, rawForm), ]) } diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts index edcaa1c7005..50b043fdf03 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts @@ -189,7 +189,8 @@ const updatePatchOnLabwareChange = ( const updatePatchOnPipetteChange = ( patch: FormPatch, rawForm: FormData, - pipetteEntities: PipetteEntities + pipetteEntities: PipetteEntities, + labwareEntities: LabwareEntities ): FormPatch => { // when pipette ID is changed (to another ID, or to null), // set any flow rates, mix volumes, or disposal volumes to null @@ -198,9 +199,15 @@ const updatePatchOnPipetteChange = ( const newPipette = patch.pipette let airGapVolume: string | null = null let nozzles: NozzleConfigurationStyle | null = null - + let firstDefaultTiprackURIOnDeck: string | null = null if (typeof newPipette === 'string' && newPipette in pipetteEntities) { const minVolume = getMinPipetteVolume(pipetteEntities[newPipette]) + const pipetteTipracks = pipetteEntities[newPipette].tiprackDefURI + const labwareURIsOnDeck = new Set( + Object.values(labwareEntities).map(({ labwareDefURI }) => labwareDefURI) + ) + firstDefaultTiprackURIOnDeck = + pipetteTipracks?.find(uri => labwareURIsOnDeck.has(uri)) ?? null airGapVolume = minVolume.toString() const hasPartialTipSupportedChannel = pipetteEntities[newPipette].spec.channels !== 1 @@ -216,12 +223,12 @@ const updatePatchOnPipetteChange = ( 'dispense_mix_volume', 'disposalVolume_volume', 'aspirate_mmFromBottom', - 'dispense_mmFromBottom', - 'tipRack' + 'dispense_mmFromBottom' ), nozzles, aspirate_airGap_volume: airGapVolume, dispense_airGap_volume: airGapVolume, + tipRack: firstDefaultTiprackURIOnDeck, } } @@ -722,7 +729,12 @@ export function dependentFieldsUpdateMoveLiquid( pipetteEntities ), chainPatch => - updatePatchOnPipetteChange(chainPatch, rawForm, pipetteEntities), + updatePatchOnPipetteChange( + chainPatch, + rawForm, + pipetteEntities, + labwareEntities + ), chainPatch => updatePatchOnWellRatioChange(chainPatch, rawForm), chainPatch => updatePatchDisposalVolumeFields(chainPatch, rawForm, pipetteEntities), diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts index c97f6400405..52bfc3cfdb0 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts @@ -11,7 +11,7 @@ import { OT2_ROBOT_TYPE, POSITION_REFERENCE_TOP, SAFE_MOVE_TO_WELL_OFFSET_FROM_TOP_MM, - WATER_LIQUID_CLASS_NAME, + WATER_LIQUID_CLASS_NAME_V2, } from '@opentrons/shared-data' import { DEST_WELL_BLOWOUT_DESTINATION, @@ -546,7 +546,9 @@ const getNoLiquidClassValuesMoveLiquid = (args: { Object.values(labwareEntities).find( ({ labwareDefURI }) => labwareDefURI === tiprack ) ?? null - const referenceLiquidClass = getAllLiquidClassDefs()[WATER_LIQUID_CLASS_NAME] + const referenceLiquidClass = getAllLiquidClassDefs()[ + WATER_LIQUID_CLASS_NAME_V2 + ] const liquidClassValuesForPipette = referenceLiquidClass.byPipette.find( ({ pipetteModel }) => convertedPipetteName === pipetteModel ) @@ -853,7 +855,9 @@ const getNoLiquidClassValuesMix = (args: { } const { spec: pipetteSpecs } = pipetteEntity const volume = Number(rawVolume) - const referenceLiquidClass = getAllLiquidClassDefs()[WATER_LIQUID_CLASS_NAME] + const referenceLiquidClass = getAllLiquidClassDefs()[ + WATER_LIQUID_CLASS_NAME_V2 + ] const liquidClassValuesForPipette = referenceLiquidClass.byPipette.find( ({ pipetteModel }) => convertedPipetteName === pipetteModel ) diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts index 89b800014c4..9dc285fb43a 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts @@ -117,7 +117,7 @@ export const mixFormToArgs = ( nozzles, xOffset: mix_x_position ?? 0, yOffset: mix_y_position ?? 0, - zOffset: mix_mmFromBottom ?? 0, + zOffset: mix_mmFromBottom ?? DEFAULT_MM_OFFSET_FROM_BOTTOM, positionReference: mix_position_reference ?? POSITION_REFERENCE_BOTTOM, finalPushOut: pushOut_checkbox && pushOut_volume != null ? pushOut_volume : 0, diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts index 952624557d2..497fccc1a24 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts @@ -2,7 +2,7 @@ import { getAllLiquidClassDefs, getFlexNameConversion, NONE_LIQUID_CLASS_NAME, - WATER_LIQUID_CLASS_NAME, + WATER_LIQUID_CLASS_NAME_V2, } from '@opentrons/shared-data' import { DEST_WELL_BLOWOUT_DESTINATION, @@ -89,7 +89,7 @@ const getCheckedPath = ( const liquidClassValuesForTip = allLiquidClassDefs[ hydratedFormData.liquidClass === NONE_LIQUID_CLASS_NAME || hydratedFormData.liquidClass == null - ? WATER_LIQUID_CLASS_NAME + ? WATER_LIQUID_CLASS_NAME_V2 : hydratedFormData.liquidClass ?? null ]?.byPipette .find( @@ -353,11 +353,13 @@ export const moveLiquidFormToArgs = ( nozzles, aspirateXOffset: aspirate_x_position ?? 0, aspirateYOffset: aspirate_y_position ?? 0, - aspirateZOffset: hydratedFormData.aspirate_mmFromBottom ?? 0, + aspirateZOffset: + hydratedFormData.aspirate_mmFromBottom ?? DEFAULT_MM_OFFSET_FROM_BOTTOM, aspiratePositionReference: hydratedFormData.aspirate_position_reference, dispenseXOffset: dispense_x_position ?? 0, dispenseYOffset: dispense_y_position ?? 0, - dispenseZOffset: hydratedFormData.dispense_mmFromBottom ?? 0, + dispenseZOffset: + hydratedFormData.dispense_mmFromBottom ?? DEFAULT_MM_OFFSET_FROM_BOTTOM, dispensePositionReference: hydratedFormData.dispense_position_reference, aspirateSubmergeSpeed: hydratedFormData.aspirate_submerge_speed ?? null, aspirateSubmergeXOffset: hydratedFormData.aspirate_submerge_x_position ?? 0, diff --git a/protocol-designer/src/steplist/formLevel/test/warnings.test.ts b/protocol-designer/src/steplist/formLevel/test/warnings.test.ts index 35055703ade..b7885aac348 100644 --- a/protocol-designer/src/steplist/formLevel/test/warnings.test.ts +++ b/protocol-designer/src/steplist/formLevel/test/warnings.test.ts @@ -206,7 +206,7 @@ const MOCK_GLYCEROL = { ], } as LiquidClass const MOCK_WATER = { - liquidClassName: 'waterV1', + liquidClassName: 'waterV2', byPipette: [ { pipetteModel: 'flex_1channel_1000', diff --git a/robot-server/robot_server/modules/module_data_mapper.py b/robot-server/robot_server/modules/module_data_mapper.py index 427217a81a2..d9461336e8c 100644 --- a/robot-server/robot_server/modules/module_data_mapper.py +++ b/robot-server/robot_server/modules/module_data_mapper.py @@ -2,6 +2,9 @@ from typing import Annotated, List, Type, cast, Optional from fastapi import Depends +from opentrons.hardware_control.types import SubSystem +from opentrons_hardware.hardware_control.types import PCBARevision +from opentrons.hardware_control import HardwareControlAPI from opentrons_shared_data.module import load_definition from opentrons.hardware_control.modules import ( @@ -48,16 +51,21 @@ UsbPort, ) -from robot_server.hardware import get_deck_type +from robot_server.hardware import get_deck_type, get_hardware class ModuleDataMapper: """Map hardware control modules to module response.""" - def __init__(self, deck_type: Annotated[DeckType, Depends(get_deck_type)]) -> None: + def __init__( + self, + deck_type: Annotated[DeckType, Depends(get_deck_type)], + hardware: Annotated[HardwareControlAPI, Depends(get_hardware)], + ) -> None: self.deck_type = deck_type + self.hardware = hardware - def map_data( + def map_data( # noqa: C901 self, model: str, module_identity: ModuleIdentity, @@ -73,6 +81,9 @@ def map_data( module_cls: Type[AttachedModule] module_data: AttachedModuleData module_definition = load_definition(model_or_loadname=model, version="3") + compatible_with_robot = ( + self.deck_type.value not in module_definition["incompatibleWithDecks"] + ) # rely on Pydantic to check/coerce data fields from dicts at run time if module_type == ModuleType.MAGNETIC: @@ -164,6 +175,7 @@ def map_data( referenceWavelength=cast( int, live_data["data"].get("referenceWavelength") ), + errorDetails=cast(str, live_data["data"].get("errorDetails")), ) elif module_type == ModuleType.FLEX_STACKER: module_cls = FlexStackerModule @@ -178,7 +190,19 @@ def map_data( HopperDoorState, live_data["data"].get("hopperDoorState") ), installDetected=cast(bool, live_data["data"].get("installDetected")), + errorDetails=cast(str, live_data["data"].get("errorDetails")), ) + + # Make sure this robot is compatible with the Flex Stacker by + # checking the rear panel revision, which has been updated to D1 to + # support the Stacker. + compatible_with_robot = False + if self.deck_type == DeckType.OT3_STANDARD: + compatible_with_robot = self.hardware.is_simulator + rear_panel = self.hardware.attached_subsystems.get(SubSystem.rear_panel) + if rear_panel is not None: + rear_panel_rev = PCBARevision.from_string(rear_panel.pcba_revision) + compatible_with_robot = rear_panel_rev >= PCBARevision("D1") else: assert False, f"Invalid module type {module_type}" @@ -188,9 +212,7 @@ def map_data( firmwareVersion=module_identity.firmware_version, hardwareRevision=module_identity.hardware_revision, hasAvailableUpdate=has_available_update, - compatibleWithRobot=( - not (self.deck_type.value in module_definition["incompatibleWithDecks"]) - ), + compatibleWithRobot=compatible_with_robot, usbPort=UsbPort( port=usb_port.port_number, portGroup=usb_port.port_group, diff --git a/robot-server/robot_server/modules/module_models.py b/robot-server/robot_server/modules/module_models.py index 2908d089c77..a31302f07a2 100644 --- a/robot-server/robot_server/modules/module_models.py +++ b/robot-server/robot_server/modules/module_models.py @@ -336,6 +336,13 @@ class AbsorbanceReaderModuleData(BaseModel): ..., description="The reference wavelength used for single measurement mode.", ) + errorDetails: Optional[str] = Field( + ..., + description=( + "Error details, if the module hardware has encountered something" + " unexpected and unrecoverable." + ), + ) class AbsorbanceReaderModule( @@ -367,6 +374,13 @@ class FlexStackerModuleData(BaseModel): installDetected: bool = Field( ..., description="The install state of the Stacker on the Flex." ) + errorDetails: Optional[str] = Field( + ..., + description=( + "Error details, if the module hardware has encountered something" + " unexpected and unrecoverable." + ), + ) class FlexStackerModule( diff --git a/robot-server/robot_server/protocols/protocol_models.py b/robot-server/robot_server/protocols/protocol_models.py index c20bd1ae5ed..a02f8ea9e09 100644 --- a/robot-server/robot_server/protocols/protocol_models.py +++ b/robot-server/robot_server/protocols/protocol_models.py @@ -26,7 +26,6 @@ class ProtocolKind(str, Enum): class ProtocolFile(BaseModel): """A file in a protocol.""" - # TODO(mc, 2021-11-12): add unique ID to file resource name: str = Field(..., description="The file's basename, including extension") role: ProtocolFileRole = Field(..., description="The file's role in the protocol.") diff --git a/robot-server/robot_server/protocols/protocol_store.py b/robot-server/robot_server/protocols/protocol_store.py index ee769b47c01..96f2cca0b0e 100644 --- a/robot-server/robot_server/protocols/protocol_store.py +++ b/robot-server/robot_server/protocols/protocol_store.py @@ -8,6 +8,7 @@ from logging import getLogger from pathlib import Path from typing import Dict, List, Optional, Set +import shutil from anyio import Path as AsyncPath, create_task_group import sqlalchemy @@ -98,7 +99,7 @@ def __init__( self, *, _sql_engine: sqlalchemy.engine.Engine, - _sources_by_id: Dict[str, ProtocolSource], + _sources_by_id: dict[str, ProtocolSource | _BadProtocolSource], ) -> None: """Do not call directly. @@ -117,8 +118,7 @@ def create_empty( Params: sql_engine: A reference to the database that this ProtocolStore should use as its backing storage. - This is expected to already have the proper tables set up; - see `add_tables_to_db()`. + This is expected to already have the proper tables set up. This should have no protocol data currently stored. If there is data, use `rehydrate()` instead. """ @@ -141,8 +141,7 @@ async def rehydrate( Params: sql_engine: A reference to the database that this ProtocolStore should use as its backing storage. - This is expected to already have the proper tables set up; - see `add_tables_to_db()`. + This is expected to already have the proper tables set up. protocols_directory: Where to look for protocol files while rehydrating. This is expected to have one subdirectory per protocol, named after its protocol ID. @@ -157,7 +156,7 @@ async def rehydrate( sources_by_id = await _compute_protocol_sources( expected_protocol_ids=expected_ids, - protocols_directory=AsyncPath(protocols_directory), + protocols_directory=protocols_directory, protocol_reader=protocol_reader, ) @@ -171,16 +170,18 @@ def insert(self, resource: ProtocolResource) -> None: The resource must have a unique ID. """ - self._sql_insert( - resource=_DBProtocolResource( - protocol_id=resource.protocol_id, - created_at=resource.created_at, - protocol_key=resource.protocol_key, - protocol_kind=_http_protocol_kind_to_sql(resource.protocol_kind), + try: + self._sql_insert( + resource=_DBProtocolResource( + protocol_id=resource.protocol_id, + created_at=resource.created_at, + protocol_key=resource.protocol_key, + protocol_kind=_http_protocol_kind_to_sql(resource.protocol_kind), + ) ) - ) - self._sources_by_id[resource.protocol_id] = resource.source - self._clear_caches() + self._sources_by_id[resource.protocol_id] = resource.source + finally: + self._clear_caches() @lru_cache(maxsize=_CACHE_ENTRIES) def get(self, protocol_id: str) -> ProtocolResource: @@ -190,30 +191,48 @@ def get(self, protocol_id: str) -> ProtocolResource: ProtocolNotFoundError """ sql_resource = self._sql_get(protocol_id=protocol_id) - return ProtocolResource( - protocol_id=sql_resource.protocol_id, - created_at=sql_resource.created_at, - protocol_key=sql_resource.protocol_key, - protocol_kind=_sql_protocol_kind_to_http(sql_resource.protocol_kind), - source=self._sources_by_id[sql_resource.protocol_id], - ) + protocol_source = self._sources_by_id[sql_resource.protocol_id] + match protocol_source: + case ProtocolSource() as protocol_source: + return ProtocolResource( + protocol_id=sql_resource.protocol_id, + created_at=sql_resource.created_at, + protocol_key=sql_resource.protocol_key, + protocol_kind=_sql_protocol_kind_to_http( + sql_resource.protocol_kind + ), + source=protocol_source, + ) + case _BadProtocolSource(reason=reason): + raise reason @lru_cache(maxsize=_CACHE_ENTRIES) def get_all(self) -> List[ProtocolResource]: """Get all protocols currently saved in this store. Results are ordered from first-added to last-added. + + If there was an error processing a protocol, it's excluded from the returned + list. This can happen, for example, if a software downgrade left the robot with + protocol files that are too new for the software that it's running now. """ all_sql_resources = self._sql_get_all() + all_sql_resources_and_protocol_sources = ( + (r, self._sources_by_id[r.protocol_id]) for r in all_sql_resources + ) return [ ProtocolResource( - protocol_id=r.protocol_id, - created_at=r.created_at, - protocol_key=r.protocol_key, - protocol_kind=_sql_protocol_kind_to_http(r.protocol_kind), - source=self._sources_by_id[r.protocol_id], + protocol_id=sql_resource.protocol_id, + created_at=sql_resource.created_at, + protocol_key=sql_resource.protocol_key, + protocol_kind=_sql_protocol_kind_to_http(sql_resource.protocol_kind), + source=protocol_source, ) - for r in all_sql_resources + for ( + sql_resource, + protocol_source, + ) in all_sql_resources_and_protocol_sources + if not isinstance(protocol_source, _BadProtocolSource) ] @lru_cache(maxsize=_CACHE_ENTRIES) @@ -258,17 +277,20 @@ def remove(self, protocol_id: str) -> None: ProtocolUsedByRunError: the protocol could not be deleted because there is a run currently referencing the protocol. """ - self._sql_remove(protocol_id=protocol_id) - - deleted_source = self._sources_by_id.pop(protocol_id) - protocol_dir = deleted_source.directory - - for source_file in deleted_source.files: - source_file.path.unlink() - if protocol_dir: - protocol_dir.rmdir() - - self._clear_caches() + try: + self._sql_remove(protocol_id=protocol_id) + + deleted_source = self._sources_by_id.pop(protocol_id) + match deleted_source: + case ProtocolSource(directory=directory, files=files): + for source_file in files: + source_file.path.unlink() + if directory: + directory.rmdir() + case _BadProtocolSource(directory=directory): + shutil.rmtree(directory, ignore_errors=True) + finally: + self._clear_caches() # Note that this is NOT cached like the other getters because we would need # to invalidate the cache whenever the runs table changes, which is not something @@ -448,18 +470,11 @@ def _clear_caches(self) -> None: self.has.cache_clear() -# TODO(mm, 2022-04-18): -# Restructure to degrade gracefully in the face of ProtocolReader failures. -# -# * ProtocolStore.get_all() should omit protocols for which it failed to compute -# a ProtocolSource. -# * ProtocolStore.get(id) should continue to raise an exception if it failed to compute -# that protocol's ProtocolSource. async def _compute_protocol_sources( expected_protocol_ids: Set[str], - protocols_directory: AsyncPath, + protocols_directory: Path, protocol_reader: ProtocolReader, -) -> Dict[str, ProtocolSource]: +) -> dict[str, ProtocolSource | _BadProtocolSource]: """Compute `ProtocolSource` objects from protocol source files. We don't store these `ProtocolSource` objects in the SQL database because @@ -475,19 +490,19 @@ async def _compute_protocol_sources( protocol_reader: An interface to use to compute `ProtocolSource`s. Returns: - A map from protocol ID to computed `ProtocolSource`. + A map from protocol ID to computed `ProtocolSource`, or an `Exception` if + there was a problem processing that particular protocol. Raises: Exception: This is not expected to raise anything, but it might if a software update makes ProtocolReader reject files that it formerly accepted. """ - sources_by_id: Dict[str, ProtocolSource] = {} + sources_by_id: dict[str, ProtocolSource | _BadProtocolSource] = {} - directory_members = [m async for m in protocols_directory.iterdir()] + directory_members = [m async for m in AsyncPath(protocols_directory).iterdir()] directory_member_names = set(m.name for m in directory_members) extra_members = directory_member_names - expected_protocol_ids - missing_members = expected_protocol_ids - directory_member_names if extra_members: # Extra members may be left over from prior interrupted writes @@ -498,38 +513,48 @@ async def _compute_protocol_sources( f" Ignoring them." ) - if missing_members: - raise SubdirectoryMissingError( - f"Missing subdirectories for protocols: {missing_members}" - ) - async def compute_source( - protocol_id: str, protocol_subdirectory: AsyncPath + protocol_subdirectory: Path, + ) -> ProtocolSource | _BadProtocolSource: + try: + # Given that the expected protocol subdirectory exists, + # we trust that the files in it are correct. + # No extra files, and no files missing. + # + # This is a safe assumption as long as: + # * Nobody has tampered with file the storage. + # * We don't try to compute the source of any protocol whose insertion + # failed halfway through and left files behind. + protocol_files = [ + Path(f) async for f in AsyncPath(protocol_subdirectory).iterdir() + ] + protocol_source = await protocol_reader.read_saved( + files=protocol_files, + directory=Path(protocol_subdirectory), + files_are_prevalidated=True, + python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, + ) + return protocol_source + except Exception as exception: + # e.g. if a software downgrade left the robot with some protocol files that + # are too new for the software version that it's running now. + _log.exception(f"Error reading protocol in {protocol_subdirectory}.") + return _BadProtocolSource(directory=protocol_subdirectory, reason=exception) + + async def compute_source_and_store_in_result_dict( + protocol_id: str, protocol_subdirectory: Path ) -> None: - # Given that the expected protocol subdirectory exists, - # we trust that the files in it are correct. - # No extra files, and no files missing. - # - # This is a safe assumption as long as: - # * Nobody has tampered with file the storage. - # * We don't try to compute the source of any protocol whose insertion - # failed halfway through and left files behind. - protocol_files = [Path(f) async for f in protocol_subdirectory.iterdir()] - protocol_source = await protocol_reader.read_saved( - files=protocol_files, - directory=Path(protocol_subdirectory), - files_are_prevalidated=True, - python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, - ) - sources_by_id[protocol_id] = protocol_source + result = await compute_source(protocol_subdirectory) + sources_by_id[protocol_id] = result async with create_task_group() as task_group: - # Use a TaskGroup instead of asyncio.gather() so, - # if any task raises an unexpected exception, - # it cancels every other task and raises an exception to signal the bug. for protocol_id in expected_protocol_ids: protocol_subdirectory = protocols_directory / protocol_id - task_group.start_soon(compute_source, protocol_id, protocol_subdirectory) + task_group.start_soon( + compute_source_and_store_in_result_dict, + protocol_id, + protocol_subdirectory, + ) for id in expected_protocol_ids: assert id in sources_by_id @@ -547,6 +572,14 @@ class _DBProtocolResource: protocol_kind: ProtocolKindSQLEnum +@dataclass(frozen=True) +class _BadProtocolSource: + """Information about files that we failed to process into a ProtocolSource.""" + + directory: Path + reason: Exception + + def _convert_sql_row_to_dataclass( sql_row: sqlalchemy.engine.Row, ) -> _DBProtocolResource: diff --git a/robot-server/robot_server/protocols/router.py b/robot-server/robot_server/protocols/router.py index ebd98a88dc5..93713d15a69 100644 --- a/robot-server/robot_server/protocols/router.py +++ b/robot-server/robot_server/protocols/router.py @@ -393,6 +393,8 @@ async def _get_cached_protocol_analysis() -> PydanticResponse[ return await _get_cached_protocol_analysis() try: + # todo(mm, 2025-10-06): ProtocolStore should encapsulate protocol storage; + # this router function should not write directly into protocol_directory. source = await protocol_reader.save( files=buffered_files, directory=protocol_directory / protocol_id, @@ -404,6 +406,8 @@ async def _get_cached_protocol_analysis() -> PydanticResponse[ ) from e if source.robot_type != robot_type: + # fixme(mm, 2025-10-06): Since `source` has already been saved to the filesystem, + # this will leave permanent stray files inside `protocol_directory`. raise ProtocolRobotTypeMismatch( detail=( f"This protocol is for {user_facing_robot_type(source.robot_type)} robots." diff --git a/robot-server/tests/instruments/test_router.py b/robot-server/tests/instruments/test_router.py index 9989d6b0409..56dfb5a00d5 100644 --- a/robot-server/tests/instruments/test_router.py +++ b/robot-server/tests/instruments/test_router.py @@ -160,7 +160,7 @@ async def rehearse_instrument_retrievals(skip_if_would_block: bool = False) -> N next_fw_version=11, fw_update_needed=False, current_fw_sha="some-sha", - pcba_revision="A1", + pcba_revision="A1.0", update_state=None, ), HWSubSystem.pipette_right: SubSystemState( @@ -169,7 +169,7 @@ async def rehearse_instrument_retrievals(skip_if_would_block: bool = False) -> N next_fw_version=11, fw_update_needed=False, current_fw_sha="some-other-sha", - pcba_revision="A1", + pcba_revision="A1.0", update_state=None, ), HWSubSystem.gripper: SubSystemState( @@ -178,7 +178,7 @@ async def rehearse_instrument_retrievals(skip_if_would_block: bool = False) -> N next_fw_version=11, fw_update_needed=False, current_fw_sha="some-other-sha", - pcba_revision="A1", + pcba_revision="A1.0", update_state=None, ), } @@ -412,7 +412,7 @@ async def test_get_instrument_not_ok( next_fw_version=11, fw_update_needed=True, current_fw_sha="some-sha", - pcba_revision="A1", + pcba_revision="A1.0", update_state=None, ), HWSubSystem.pipette_right: SubSystemState( @@ -421,7 +421,7 @@ async def test_get_instrument_not_ok( next_fw_version=11, fw_update_needed=True, current_fw_sha="some-other-sha", - pcba_revision="A1", + pcba_revision="A1.0", update_state=None, ), HWSubSystem.gripper: SubSystemState( @@ -430,7 +430,7 @@ async def test_get_instrument_not_ok( next_fw_version=11, fw_update_needed=True, current_fw_sha="some-other-sha", - pcba_revision="A1", + pcba_revision="A1.0", update_state=None, ), } diff --git a/robot-server/tests/modules/test_module_data_mapper.py b/robot-server/tests/modules/test_module_data_mapper.py index 87bd1f19d7d..fe5eae7c549 100644 --- a/robot-server/tests/modules/test_module_data_mapper.py +++ b/robot-server/tests/modules/test_module_data_mapper.py @@ -1,13 +1,18 @@ """Tests for robot_server.modules.module_data_mapper.""" +from decoy import Decoy +from opentrons.hardware_control import HardwareControlAPI +from opentrons.hardware_control.types import SubSystem, SubSystemState import pytest from opentrons.protocol_engine import ModuleModel, DeckType from opentrons.protocol_engine.types import Vec3f from opentrons.drivers.rpi_drivers.types import USBPort as HardwareUSBPort, PortGroup from opentrons.hardware_control.modules import ( + FlexStackerStatus, LiveData, ModuleType, MagneticStatus, + PlatformState, TemperatureStatus, HeaterShakerStatus, types as hc_types, @@ -18,6 +23,8 @@ from robot_server.modules.module_data_mapper import ModuleDataMapper from robot_server.modules.module_models import ( + FlexStackerModule, + FlexStackerModuleData, UsbPort, MagneticModule, MagneticModuleData, @@ -114,6 +121,7 @@ def test_maps_magnetic_module_data( input_data: LiveData, expected_output_data: MagneticModuleData, expected_compatible: bool, + hardware_api: HardwareControlAPI, ) -> None: """It should map hardware data to a magnetic module.""" module_identity = ModuleIdentity( @@ -132,7 +140,7 @@ def test_maps_magnetic_module_data( device_path="/dev/null", ) - subject = ModuleDataMapper(deck_type=deck_type) + subject = ModuleDataMapper(deck_type=deck_type, hardware=hardware_api) result = subject.map_data( model=input_model, module_identity=module_identity, @@ -187,6 +195,7 @@ def test_maps_temperature_module_data( expected_compatible: bool, status: str, data: hc_types.TemperatureModuleData, + hardware_api: HardwareControlAPI, ) -> None: """It should map hardware data to a magnetic module.""" input_data: LiveData = {"status": status, "data": data} @@ -206,7 +215,7 @@ def test_maps_temperature_module_data( device_path="/dev/null", ) - subject = ModuleDataMapper(deck_type=deck_type) + subject = ModuleDataMapper(deck_type=deck_type, hardware=hardware_api) result = subject.map_data( model=input_model, module_identity=module_identity, @@ -297,6 +306,7 @@ def test_maps_thermocycler_module_data( expected_compatible: bool, status: str, data: hc_types.ThermocyclerData, + hardware_api: HardwareControlAPI, ) -> None: """It should map hardware data to a magnetic module.""" input_data: LiveData = {"status": status, "data": data} @@ -316,7 +326,7 @@ def test_maps_thermocycler_module_data( device_path="/dev/null", ) - subject = ModuleDataMapper(deck_type=deck_type) + subject = ModuleDataMapper(deck_type=deck_type, hardware=hardware_api) result = subject.map_data( model=input_model, module_identity=module_identity, @@ -402,7 +412,11 @@ def test_maps_thermocycler_module_data( ], ) def test_maps_heater_shaker_module_data( - input_model: str, deck_type: DeckType, status: str, data: hc_types.HeaterShakerData + input_model: str, + deck_type: DeckType, + status: str, + data: hc_types.HeaterShakerData, + hardware_api: HardwareControlAPI, ) -> None: """It should map hardware data to a magnetic module.""" input_data: LiveData = {"status": status, "data": data} @@ -422,7 +436,7 @@ def test_maps_heater_shaker_module_data( device_path="/dev/null", ) - subject = ModuleDataMapper(deck_type=deck_type) + subject = ModuleDataMapper(deck_type=deck_type, hardware=hardware_api) result = subject.map_data( model=input_model, module_identity=module_identity, @@ -463,3 +477,115 @@ def test_maps_heater_shaker_module_data( errorDetails=data["errorDetails"], ), ) + + +@pytest.mark.parametrize( + "input_model,deck_type,rear_panel_rev,compatible", + [ + ("flexStackerModuleV1", DeckType("ot2_standard"), "", False), + ("flexStackerModuleV1", DeckType("ot3_standard"), "C1", False), + ("flexStackerModuleV1", DeckType("ot3_standard"), "D1", True), + ], +) +@pytest.mark.parametrize( + "status,data", + [ + ( + "idle", + { + "latchState": "closed", + "platformState": "extended", + "hopperDoorState": "closed", + "installDetected": True, + "errorDetails": "", + }, + ), + ( + "dispensing", + { + "latchState": "closed", + "platformState": "retracted", + "hopperDoorState": "closed", + "installDetected": True, + "errorDetails": "", + }, + ), + ], +) +def test_maps_flex_stacker_module_data( + input_model: str, + deck_type: DeckType, + rear_panel_rev: str, + compatible: bool, + status: str, + data: hc_types.FlexStackerData, + hardware_api: HardwareControlAPI, + decoy: Decoy, +) -> None: + """It should map hardware data to a flex stacker.""" + input_data: LiveData = {"status": status, "data": data} + module_identity = ModuleIdentity( + module_id="module-id", + serial_number="serial-number", + firmware_version="1.2.3", + hardware_revision="4.5.6", + ) + + hardware_usb_port = HardwareUSBPort( + name="abc", + port_number=101, + port_group=PortGroup.RIGHT, + hub=True, + hub_port=1, + device_path="1.0/tty/ttyACM1/dev", + ) + decoy.when(hardware_api.attached_subsystems).then_return( + { + SubSystem.rear_panel: SubSystemState( + ok=True, + current_fw_version=63, + next_fw_version=63, + fw_update_needed=False, + current_fw_sha="", + pcba_revision=rear_panel_rev, + update_state=None, + ) + } + ) + + subject = ModuleDataMapper(deck_type=deck_type, hardware=hardware_api) + result = subject.map_data( + model=input_model, + module_identity=module_identity, + has_available_update=False, + live_data=input_data, + usb_port=hardware_usb_port, + module_offset=None, + ) + + assert result == FlexStackerModule( + id="module-id", + serialNumber="serial-number", + firmwareVersion="1.2.3", + hardwareRevision="4.5.6", + hasAvailableUpdate=False, + moduleType=ModuleType.FLEX_STACKER, + compatibleWithRobot=compatible, + moduleModel=ModuleModel(input_model), # type: ignore[arg-type] + usbPort=UsbPort( + port=101, + portGroup=PortGroup.RIGHT, + hub=True, + hubPort=1, + path="1.0/tty/ttyACM1/dev", + ), + moduleOffset=None, + data=FlexStackerModuleData( + status=FlexStackerStatus(status), + latchState=hc_types.LatchState(data["latchState"]), + platformState=PlatformState(data["platformState"]), + hopperDoorState=hc_types.HopperDoorState(data["hopperDoorState"]), + installDetected=data["installDetected"], + errorDetails=data["errorDetails"], + ), + ) diff --git a/robot-server/tests/protocols/test_protocol_store.py b/robot-server/tests/protocols/test_protocol_store.py index 499bf480cf0..0797f54962c 100644 --- a/robot-server/tests/protocols/test_protocol_store.py +++ b/robot-server/tests/protocols/test_protocol_store.py @@ -1,4 +1,5 @@ """Tests for the ProtocolStore interface.""" +import textwrap from opentrons.protocol_engine.types import CSVParameter, FileInfo import pytest from decoy import Decoy @@ -7,6 +8,7 @@ from opentrons.protocols.api_support.types import APIVersion from opentrons.protocol_reader import ( + ProtocolReader, ProtocolSource, ProtocolSourceFile, ProtocolFileRole, @@ -677,3 +679,93 @@ async def test_get_referenced_data_files( source=DataFileSource.UPLOADED, ), ] + + +async def test_error_tolerance( + decoy: Decoy, tmp_path: Path, sql_engine: SQLEngine +) -> None: + """It should tolerate "corrupted" protocol files.""" + id_1 = "id-1" + id_2 = "id-2" + + root_protocol_dir = tmp_path / "protocols" + protocol_1_dir = root_protocol_dir / id_1 + protocol_1_file = protocol_1_dir / "protocol.py" + protocol_2_dir = root_protocol_dir / id_2 + protocol_2_file = protocol_2_dir / "protocol.py" + + ok_python = textwrap.dedent( + """\ + requirements = {"apiLevel": "2.0"} + def run(context): + pass + """ + ) + bad_python = textwrap.dedent( + """\ + this ain't even Python + """ + ) + + subject_1 = ProtocolStore.create_empty(sql_engine) + + protocol_resource_1 = ProtocolResource( + protocol_id=id_1, + created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + source=ProtocolSource( + directory=protocol_1_dir, + main_file=protocol_1_file, + config=PythonProtocolConfig(APIVersion(2, 0)), + files=[], + metadata={}, + robot_type="OT-2 Standard", + content_hash="abc123", + ), + protocol_key=None, + protocol_kind=ProtocolKind.STANDARD, + ) + protocol_1_dir.mkdir(parents=True) + protocol_1_file.write_text(ok_python) + subject_1.insert(protocol_resource_1) + + protocol_resource_2 = ProtocolResource( + protocol_id=id_2, + created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + source=ProtocolSource( + directory=protocol_2_dir, + main_file=protocol_2_file, + config=PythonProtocolConfig(APIVersion(99999, 99999)), + files=[], + metadata={}, + robot_type="OT-2 Standard", + content_hash="def456", + ), + protocol_key=None, + protocol_kind=ProtocolKind.STANDARD, + ) + protocol_2_dir.mkdir(parents=True) + protocol_2_file.write_text(ok_python) + subject_1.insert(protocol_resource_2) + + # Baseline: + # With no corrupted files, all getters should work. + assert subject_1.get_all_ids() == [id_1, id_2] + assert [r.protocol_id for r in subject_1.get_all()] == [id_1, id_2] + subject_1.get(id_1) + subject_1.get(id_1) + + protocol_2_file.write_text(bad_python) + + subject_2 = await ProtocolStore.rehydrate( + sql_engine, root_protocol_dir, ProtocolReader() + ) + + # With corrupted files, getters that return details about the corrupted + # protocol should either silently omit it, if they were returning it as just one + # element of many in a collection; or raise the original error, if the caller + # was specifically asking for the corrupted protocol. + assert subject_2.get_all_ids() == [id_1, id_2] + assert [r.protocol_id for r in subject_2.get_all()] == [id_1] + subject_2.get(id_1) + with pytest.raises(Exception): + subject_2.get(id_2) diff --git a/shared-data/js/__tests__/labwareDefQuirks.test.ts b/shared-data/js/__tests__/labwareDefQuirks.test.ts index c657a02403b..82c1b0c0b4c 100644 --- a/shared-data/js/__tests__/labwareDefQuirks.test.ts +++ b/shared-data/js/__tests__/labwareDefQuirks.test.ts @@ -15,6 +15,7 @@ const EXPECTED_VALID_QUIRKS = [ 'tiprackAdapterFor96Channel', 'stackingMaxFive', 'stackingOnly', + 'disableGeometryBasedGripCheck', ] describe('check quirks for all labware defs', () => { diff --git a/shared-data/js/getLabware.ts b/shared-data/js/getLabware.ts index 77b1a4e3802..66a492625d5 100644 --- a/shared-data/js/getLabware.ts +++ b/shared-data/js/getLabware.ts @@ -48,8 +48,6 @@ export const LABWAREV2_DO_NOT_LIST = [ // temporarily blocking 20 uL Flex tip racks until they launch 'opentrons_flex_96_tiprack_20ul', 'opentrons_flex_96_filtertiprack_20ul', - // temporarily blocking tough lids until geometry and collateral is finalized - 'opentrons_tough_universal_lid', ] // NOTE(sa, 2020-7-14): in PD we do not want to list calibration blocks // or the adapter/labware combos since we migrated to splitting them up @@ -72,8 +70,6 @@ export const PD_DO_NOT_LIST = [ // temporarily blocking 20 uL Flex tip racks until they launch 'opentrons_flex_96_tiprack_20ul', 'opentrons_flex_96_filtertiprack_20ul', - // temporarily blocking tough lids until geometry and collateral is finalized - 'opentrons_tough_universal_lid', ] export function getIsLabwareV1Tiprack(def: LabwareDefinition1): boolean { diff --git a/shared-data/js/helpers/__tests__/getWellFillFromLabwareId.test.ts b/shared-data/js/helpers/__tests__/getWellFillFromLabwareId.test.ts new file mode 100644 index 00000000000..500345d64bf --- /dev/null +++ b/shared-data/js/helpers/__tests__/getWellFillFromLabwareId.test.ts @@ -0,0 +1,143 @@ +import { describe, expect, it, vi } from 'vitest' + +import { getWellFillFromLabwareId } from '../getWellFillFromLabwareId' +import { parseLiquidsInLoadOrder } from '../parseProtocolCommands' + +import type { Liquid } from '../../types' +import type { LabwareByLiquidId } from '../getLabwareInfoByLiquidId' + +vi.mock('../parseProtocolCommands') + +const LABWARE_ID = + '60e8b050-3412-11eb-ad93-ed232a2337cf:opentrons/corning_24_wellplate_3.4ml_flat/1' +const MOCK_LIQUIDS_IN_LOAD_ORDER: Liquid[] = [ + { + description: 'water', + displayColor: '#00d781', + displayName: 'liquid 2', + id: '7', + }, + { + description: 'saline', + displayColor: '#0076ff', + displayName: 'liquid 1', + id: '123', + }, + { + description: 'reagent', + displayColor: '#ff4888', + displayName: 'liquid 3', + id: '19', + }, + { + description: 'saliva', + displayColor: '#B925FF', + displayName: 'liquid 4', + id: '4', + }, +] +const MOCK_LABWARE_BY_LIQUID_ID: LabwareByLiquidId = { + '4': [ + { + labwareId: + '60e8b050-3412-11eb-ad93-ed232a2337cf:opentrons/corning_24_wellplate_3.4ml_flat/1', + volumeByWell: { + A3: 100, + A4: 100, + B3: 100, + B4: 100, + C3: 100, + C4: 100, + D3: 100, + D4: 100, + }, + }, + ], + '7': [ + { + labwareId: + '60e8b050-3412-11eb-ad93-ed232a2337cf:opentrons/corning_24_wellplate_3.4ml_flat/1', + volumeByWell: { + A1: 100, + A2: 100, + B1: 100, + B2: 100, + C1: 100, + C2: 100, + D1: 100, + D2: 100, + }, + }, + { + labwareId: '53d3b350-a9c0-11eb-bce6-9f1d5b9c1a1b', + volumeByWell: { + A3: 50, + B1: 50, + C1: 50, + D1: 50, + }, + }, + ], + '19': [ + { + labwareId: + '60e8b050-3412-11eb-ad93-ed232a2337cf:opentrons/corning_24_wellplate_3.4ml_flat/1', + volumeByWell: { + A5: 100, + A6: 100, + B5: 100, + B6: 100, + C5: 100, + C6: 100, + D5: 100, + D6: 100, + }, + }, + ], + '123': [ + { + labwareId: + '5ae317e0-3412-11eb-ad93-ed232a2337cf:opentrons/nest_1_reservoir_195ml/1', + volumeByWell: { + A1: 1000, + }, + }, + ], +} + +describe('getWellFillFromLabwareId', () => { + it('returns wellfill object for the labwareId', () => { + const expected = { + A1: '#00d781', + A2: '#00d781', + A3: '#B925FF', + A4: '#B925FF', + A5: '#ff4888', + A6: '#ff4888', + B1: '#00d781', + B2: '#00d781', + B3: '#B925FF', + B4: '#B925FF', + B5: '#ff4888', + B6: '#ff4888', + C1: '#00d781', + C2: '#00d781', + C3: '#B925FF', + C4: '#B925FF', + C5: '#ff4888', + C6: '#ff4888', + D1: '#00d781', + D2: '#00d781', + D3: '#B925FF', + D4: '#B925FF', + D5: '#ff4888', + D6: '#ff4888', + } + vi.mocked(parseLiquidsInLoadOrder).mockReturnValue( + MOCK_LIQUIDS_IN_LOAD_ORDER as any + ) + expect( + getWellFillFromLabwareId(LABWARE_ID, [], MOCK_LABWARE_BY_LIQUID_ID, []) + ).toEqual(expected) + }) +}) diff --git a/shared-data/js/helpers/__tests__/positionMath.test.ts b/shared-data/js/helpers/__tests__/positionMath.test.ts index efe33ede589..3b6647bfb0c 100644 --- a/shared-data/js/helpers/__tests__/positionMath.test.ts +++ b/shared-data/js/helpers/__tests__/positionMath.test.ts @@ -3,33 +3,82 @@ import { describe, expect, test } from 'vitest' import { computeLabwareOrigin, getAllDefinitions, - ot3StandardDeckV5, + getModuleDef, + ot3StandardDeckV5 as ot3StandardDeckV5Untyped, } from '../..' +import type { DeckDefinition } from '../..' + +const OT3_DECK_DEF = (ot3StandardDeckV5Untyped as unknown) as DeckDefinition + describe('computeLabwareOrigin()', () => { - test('legacy behavior of x and y when labware are stacked', () => { - // The adapter has a nonzero cornerOffsetFromSlot and the tip rack has a zero - // stackingOffsetWithLabware x and y. The x and y of the tip rack follow the - // x and y of the underlying slot, NOT of the adapter. - const adapter = getAllDefinitions()[ - 'opentrons/opentrons_flex_96_tiprack_adapter/1' - ] - const tipRack = getAllDefinitions()[ - 'opentrons/opentrons_flex_96_tiprack_1000ul/1' - ] - const result = computeLabwareOrigin({ - labwareDefinitionsTopToBottom: [tipRack, adapter], - moduleDefinition: null, - slotId: 'A1', - deckDefinition: ot3StandardDeckV5 as any, + // Preserving legacy misbehavior: + // If we're computing the position of a schema 2 labware, the labware below + // it don't contribute anything to its x or y offset. Only z. + describe('legacy behavior of x and y when labware are stacked', () => { + test('where the base is a deck slot', () => { + // These test labware are chosen so the adapter has a nonzero cornerOffsetFromSlot, + // and the tip rack has a zero stackingOffsetWithLabware x and y. + // + // The remainder of this test will check that the final x and y of the tip rack + // follow the x and y of the underlying slot, NOT of the adapter. + const adapter = getAllDefinitions()[ + 'opentrons/opentrons_flex_96_tiprack_adapter/1' + ] + const tipRack = getAllDefinitions()[ + 'opentrons/opentrons_flex_96_tiprack_1000ul/1' + ] + + const result = computeLabwareOrigin({ + labwareDefinitionsTopToBottom: [tipRack, adapter], + moduleDefinition: null, + slotId: 'A1', + deckDefinition: OT3_DECK_DEF, + }) + const expected = { + // x and y are the front-left of slot A1. + // z is on the surface of the adapter. + x: 0.0, + y: 321.0, + z: 11.0, + } + expect(result).toStrictEqual(expected) + }) + + test('where the base is a module', () => { + const stackerModuleDef = getModuleDef('flexStackerModuleV1') + const tipRackDef = getAllDefinitions()[ + 'opentrons/opentrons_flex_96_tiprack_1000ul/1' + ] + const lidDef = getAllDefinitions()[ + 'opentrons/opentrons_flex_tiprack_lid/1' + ] + + const resultForTipRackAndLid = computeLabwareOrigin({ + labwareDefinitionsTopToBottom: [lidDef, tipRackDef], + moduleDefinition: stackerModuleDef, + slotId: 'C3', + deckDefinition: OT3_DECK_DEF, + }) + expect(resultForTipRackAndLid).toStrictEqual({ + // todo(mm, 2025-09-17): These numbers seem wrong. Looks like an error in the module definition. + x: 489.125, + y: 105.875, + z: 66.425, + }) + + const resultForTipRackOnly = computeLabwareOrigin({ + labwareDefinitionsTopToBottom: [tipRackDef], + moduleDefinition: stackerModuleDef, + slotId: 'C3', + deckDefinition: OT3_DECK_DEF, + }) + expect(resultForTipRackOnly).toStrictEqual({ + // todo(mm, 2025-09-17): These numbers seem wrong. Looks like an error in the module definition. + x: 489.125, + y: 105.875, + z: -18.325000000000003, + }) }) - const expected = { - // x and y are the front-left of slot A1. - // z is on the surface of the adapter. - x: 0.0, - y: 321.0, - z: 11.0, - } - expect(result).toStrictEqual(expected) }) }) diff --git a/shared-data/js/helpers/getWellFillFromLabwareId.ts b/shared-data/js/helpers/getWellFillFromLabwareId.ts index 2ab85df7ca5..732b8d76351 100644 --- a/shared-data/js/helpers/getWellFillFromLabwareId.ts +++ b/shared-data/js/helpers/getWellFillFromLabwareId.ts @@ -1,3 +1,6 @@ +import { parseLiquidsInLoadOrder } from './parseProtocolCommands' + +import type { RunTimeCommand } from '../../protocol' import type { Liquid } from '../types' import type { LabwareByLiquidId } from './getLabwareInfoByLiquidId' @@ -5,10 +8,12 @@ export type WellFill = Record export function getWellFillFromLabwareId( labwareId: string, - liquidsInLoadOrder: Liquid[], - labwareByLiquidId: LabwareByLiquidId + liquids: Liquid[], + labwareByLiquidId: LabwareByLiquidId, + commands: RunTimeCommand[] ): WellFill { let labwareWellFill: WellFill = {} + const liquidsInLoadOrder = parseLiquidsInLoadOrder(liquids, commands) const liquidIds = Object.keys(labwareByLiquidId) const labwareInfo = Object.values(labwareByLiquidId) diff --git a/shared-data/js/helpers/positionMath.ts b/shared-data/js/helpers/positionMath.ts index 764df11ee11..04c401f66a3 100644 --- a/shared-data/js/helpers/positionMath.ts +++ b/shared-data/js/helpers/positionMath.ts @@ -224,24 +224,17 @@ function getLabwareStackAsArray( }) } else { // The bottom of the stack is a labware in a module in a deck slot. - const slotPositionTuple = getPositionFromSlotId(slotId, deckDefinition) - if (slotPositionTuple == null) { + const deckSlotPositionTuple = getPositionFromSlotId(slotId, deckDefinition) + if (deckSlotPositionTuple == null) { return null } - const slotPosition = coordinateTupleToVector3D(slotPositionTuple) - const slotPositionToLabwareOrigin = getModuleParentOriginToLabwareOrigin( + const deckSlotPosition = coordinateTupleToVector3D(deckSlotPositionTuple) + const deckSlotPositionToLabwareOrigin = getModuleParentOriginToLabwareOrigin( deckDefinition.otId, slotId, moduleDefinition, bottomLabware ) - // Preserving legacy misbehavior: - // If we're computing the position of a schema 2 labware, the labware below - // it don't contribute anything to its x or y offset. Only z. - if (bottomLabware !== topLabware && topLabware.schemaVersion === 2) { - slotPositionToLabwareOrigin.x = 0 - slotPositionToLabwareOrigin.y = 0 - } result.push({ debugInfo: { @@ -249,7 +242,7 @@ function getLabwareStackAsArray( parentModuleDefinition: moduleDefinition, childLabwareDefinition: bottomLabware, }, - offset: slotPositionToLabwareOrigin, + offset: deckSlotPositionToLabwareOrigin, }) result.push({ debugInfo: { @@ -259,7 +252,7 @@ function getLabwareStackAsArray( }, // Modules don't really have an origin of their own that's separate from the // origin of their parent deck slot. - offset: slotPosition, + offset: deckSlotPosition, }) } diff --git a/shared-data/js/liquidClasses.ts b/shared-data/js/liquidClasses.ts index 5c8fb62e984..9f1c728629b 100644 --- a/shared-data/js/liquidClasses.ts +++ b/shared-data/js/liquidClasses.ts @@ -1,18 +1,22 @@ -import ethanol80V1Uncasted from '../liquid-class/definitions/1/ethanol_80/1.json' -import glycerol50V1Uncasted from '../liquid-class/definitions/1/glycerol_50/1.json' -import waterV1Uncasted from '../liquid-class/definitions/1/water/1.json' +import ethanol80V2Uncasted from '../liquid-class/definitions/1/ethanol_80/2.json' +import glycerol50V2Uncasted from '../liquid-class/definitions/1/glycerol_50/2.json' +import waterV2Uncasted from '../liquid-class/definitions/1/water/2.json' import type { LiquidClass } from '.' -const ethanol80V1 = ethanol80V1Uncasted as LiquidClass -const glycerol50V1 = glycerol50V1Uncasted as LiquidClass -const waterV1 = waterV1Uncasted as LiquidClass +const ethanol80V2 = ethanol80V2Uncasted as LiquidClass +const glycerol50V2 = glycerol50V2Uncasted as LiquidClass +const waterV2 = waterV2Uncasted as LiquidClass -export const WATER_LIQUID_CLASS_NAME = 'waterV1' +export const WATER_LIQUID_CLASS_NAME_V2 = 'waterV2' export const NONE_LIQUID_CLASS_NAME = 'none' -export const GLYCEROL_LIQUID_CLASS_NAME = 'glycerol50V1' -export const ETHANOL_LIQUID_CLASS_NAME = 'ethanol80V1' +export const GLYCEROL_LIQUID_CLASS_NAME_V2 = 'glycerol50V2' +export const ETHANOL_LIQUID_CLASS_NAME_V2 = 'ethanol80V2' -const defs = { waterV1, glycerol50V1, ethanol80V1 } +const defs = { waterV2, glycerol50V2, ethanol80V2 } +// returns all liquid class defs but their latest version only +// NOTE: we should refactor this util though to get the latest versions of all the definitions types +// that exist. right now, its hard-coded in which means we need to manually +// update it every time, which is not great. export const getAllLiquidClassDefs = (): Record => defs diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 8e801bfaed8..7c1ee80c9d7 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -1076,6 +1076,7 @@ export type MotorAxis = | 'rightPlunger' | 'extensionZ' | 'extensionJaw' + | 'axis96ChannelCam' export type MotorAxes = MotorAxis[] diff --git a/shared-data/labware/definitions/2/milliplex_microtiter_plate_lid/1.json b/shared-data/labware/definitions/2/black_96_well_microtiter_plate_lid/1.json similarity index 84% rename from shared-data/labware/definitions/2/milliplex_microtiter_plate_lid/1.json rename to shared-data/labware/definitions/2/black_96_well_microtiter_plate_lid/1.json index de3d7d66491..28ff588b3f4 100644 --- a/shared-data/labware/definitions/2/milliplex_microtiter_plate_lid/1.json +++ b/shared-data/labware/definitions/2/black_96_well_microtiter_plate_lid/1.json @@ -2,11 +2,11 @@ "allowedRoles": ["labware", "lid"], "ordering": [], "brand": { - "brand": "MILLIPLEX", + "brand": "Greiner", "brandId": [] }, "metadata": { - "displayName": "MILLIPLEX Microtiter Plate Lid", + "displayName": "Black 96-well Microtiter Plate Lid", "displayCategory": "lid", "displayVolumeUnits": "\u00b5L", "tags": [] @@ -33,18 +33,18 @@ "quirks": ["stackingMaxFive"], "isTiprack": false, "isMagneticModuleCompatible": false, - "loadName": "milliplex_microtiter_plate_lid" + "loadName": "black_96_well_microtiter_plate_lid" }, "namespace": "opentrons", "version": 1, "schemaVersion": 2, "stackingOffsetWithLabware": { - "milliplex_microtiter_plate": { + "milliplex_r_96_well_microtiter_plate": { "x": 0, "y": 0, "z": 7.4 }, - "milliplex_microtiter_plate_lid": { + "black_96_well_microtiter_plate_lid": { "x": 0, "y": 0, "z": 1 @@ -64,8 +64,8 @@ "compatibleParentLabware": [ "protocol_engine_lid_stack_object", "opentrons_flex_deck_riser", - "milliplex_microtiter_plate", - "milliplex_microtiter_plate_lid" + "milliplex_r_96_well_microtiter_plate", + "black_96_well_microtiter_plate_lid" ], "gripForce": 10, "gripHeightFromLabwareBottom": 7, diff --git a/shared-data/labware/definitions/2/corning_96_wellplate_360ul_lid/1.json b/shared-data/labware/definitions/2/corning_96_wellplate_360ul_lid/1.json index 0047263bd81..8601dec5d31 100644 --- a/shared-data/labware/definitions/2/corning_96_wellplate_360ul_lid/1.json +++ b/shared-data/labware/definitions/2/corning_96_wellplate_360ul_lid/1.json @@ -51,6 +51,11 @@ "y": 0, "z": 6.5 }, + "corning_96_wellplate_360ul_lid": { + "x": 0, + "y": 0, + "z": 1 + }, "opentrons_flex_deck_riser": { "x": 0, "y": 0, @@ -66,7 +71,8 @@ "compatibleParentLabware": [ "protocol_engine_lid_stack_object", "opentrons_flex_deck_riser", - "corning_96_wellplate_360ul_flat" + "corning_96_wellplate_360ul_flat", + "corning_96_wellplate_360ul_lid" ], "gripForce": 10, "gripHeightFromLabwareBottom": 7, diff --git a/shared-data/labware/definitions/2/milliplex_microtiter_plate/1.json b/shared-data/labware/definitions/2/milliplex_r_96_well_microtiter_plate/1.json similarity index 99% rename from shared-data/labware/definitions/2/milliplex_microtiter_plate/1.json rename to shared-data/labware/definitions/2/milliplex_r_96_well_microtiter_plate/1.json index 11341d8d395..c216fd176ec 100644 --- a/shared-data/labware/definitions/2/milliplex_microtiter_plate/1.json +++ b/shared-data/labware/definitions/2/milliplex_r_96_well_microtiter_plate/1.json @@ -18,7 +18,7 @@ "brandId": [] }, "metadata": { - "displayName": "MILLIPLEX Microtiter Plate", + "displayName": "MILLIPLEX (R) 96-well Microtiter Plate", "displayCategory": "wellPlate", "displayVolumeUnits": "µL", "tags": [] @@ -1002,7 +1002,7 @@ "quirks": [], "isTiprack": false, "isMagneticModuleCompatible": false, - "loadName": "milliplex_microtiter_plate" + "loadName": "milliplex_r_96_well_microtiter_plate" }, "namespace": "opentrons", "version": 1, diff --git a/shared-data/labware/definitions/2/opentrons_flex_lid_absorbance_plate_reader_module/1.json b/shared-data/labware/definitions/2/opentrons_flex_lid_absorbance_plate_reader_module/1.json index a503e9f4a08..56b85de4e45 100644 --- a/shared-data/labware/definitions/2/opentrons_flex_lid_absorbance_plate_reader_module/1.json +++ b/shared-data/labware/definitions/2/opentrons_flex_lid_absorbance_plate_reader_module/1.json @@ -24,7 +24,7 @@ ], "parameters": { "format": "irregular", - "quirks": [], + "quirks": ["disableGeometryBasedGripCheck"], "isTiprack": false, "isMagneticModuleCompatible": false, "loadName": "opentrons_flex_lid_absorbance_plate_reader_module" diff --git a/shared-data/labware/definitions/2/opentrons_tough_universal_lid/1.json b/shared-data/labware/definitions/2/opentrons_tough_universal_lid/1.json index 50aea574a38..9203acf5f1b 100644 --- a/shared-data/labware/definitions/2/opentrons_tough_universal_lid/1.json +++ b/shared-data/labware/definitions/2/opentrons_tough_universal_lid/1.json @@ -17,7 +17,7 @@ "dimensions": { "xDimension": 127.76, "yDimension": 85.48, - "zDimension": 8.8 + "zDimension": 9.2 }, "wells": {}, "groups": [ @@ -46,12 +46,12 @@ "default": { "x": 0, "y": 0, - "z": 6.6 + "z": 7.1 }, "opentrons_96_wellplate_200ul_pcr_full_skirt": { "x": 0, "y": 0, - "z": 7.1 + "z": 6.1 }, "protocol_engine_lid_stack_object": { "x": 0, @@ -61,12 +61,12 @@ "opentrons_tough_universal_lid": { "x": 0, "y": 0, - "z": 0.8 + "z": 1 } }, "stackLimit": 5, "gripForce": 10, - "gripHeightFromLabwareBottom": 5.3, + "gripHeightFromLabwareBottom": 4.7, "gripperOffsets": { "default": { "pickUpOffset": { diff --git a/shared-data/liquid-class/definitions/1/ethanol_80/2.json b/shared-data/liquid-class/definitions/1/ethanol_80/2.json new file mode 100644 index 00000000000..8b41f95ca52 --- /dev/null +++ b/shared-data/liquid-class/definitions/1/ethanol_80/2.json @@ -0,0 +1,6886 @@ +{ + "liquidClassName": "ethanol_80", + "displayName": "Volatile", + "description": "80% ethanol", + "schemaVersion": 1, + "version": 2, + "namespace": "opentrons", + "byPipette": [ + { + "pipetteModel": "flex_1channel_50", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.7], + [10.0, -0.2], + [50.0, -0.5] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 7.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 7.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.7], + [10.0, -0.2], + [50.0, -0.5] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 0.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 7.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 7.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.7], + [10.0, -0.2], + [50.0, -0.5] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.7], + [10.0, -0.2], + [50.0, -0.5] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 7.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 7.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.7], + [10.0, -0.2], + [50.0, -0.5] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 0.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 7.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 7.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.7], + [10.0, -0.2], + [50.0, -0.5] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_8channel_50", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.5], + [10.0, -0.2], + [50.0, -1.5] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 35.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 35.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.5], + [10.0, -0.2], + [50.0, -1.5] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 0.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 35.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 35.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.5], + [10.0, -0.2], + [50.0, -1.5] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.5], + [10.0, -0.2], + [50.0, -1.5] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 35.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 35.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.5], + [10.0, -0.2], + [50.0, -1.5] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 0.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 35.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 35.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.5], + [10.0, -0.2], + [50.0, -1.5] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_1channel_1000", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.75], + [10.0, -0.7], + [50.0, -1.3] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.75], + [10.0, -0.7], + [50.0, -1.3] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [10.0, 0.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.75], + [10.0, -0.7], + [50.0, -1.3] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.75], + [10.0, -0.7], + [50.0, -1.3] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.75], + [10.0, -0.7], + [50.0, -1.3] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [10.0, 0.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.75], + [10.0, -0.7], + [50.0, -1.3] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 7.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.5], + [50.0, -2.2], + [200.0, -8.9] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.5], + [50.0, -2.2], + [200.0, -8.9] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [50.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.5], + [50.0, -2.2], + [200.0, -8.9] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 7.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.5], + [50.0, -2.2], + [200.0, -8.9] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.5], + [50.0, -2.2], + [200.0, -8.9] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [50.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.5], + [50.0, -2.2], + [200.0, -8.9] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.9], + [100.0, -2.6], + [1000.0, -32.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 300.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 300.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.9], + [100.0, -2.6], + [1000.0, -32.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [10.0, 0.0], + [100.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 300.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 300.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.9], + [100.0, -2.6], + [1000.0, -32.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.9], + [100.0, -2.6], + [1000.0, -32.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 300.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 300.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.9], + [100.0, -2.6], + [1000.0, -32.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [10.0, 0.0], + [100.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 300.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 300.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.9], + [100.0, -2.6], + [1000.0, -32.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_8channel_1000", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.75], + [10.0, -0.7], + [50.0, -1.3] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.75], + [10.0, -0.7], + [50.0, -1.3] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [10.0, 0.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.75], + [10.0, -0.7], + [50.0, -1.3] + ], + "conditioningByVolume": [ + [0.0, 0.0], + [40.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.75], + [10.0, -0.7], + [50.0, -1.3] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.75], + [10.0, -0.7], + [50.0, -1.3] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [10.0, 0.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.75], + [10.0, -0.7], + [50.0, -1.3] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 7.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.5], + [50.0, -0.2], + [200.0, -4.9] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.5], + [50.0, -0.2], + [200.0, -4.9] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [50.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.5], + [50.0, -0.2], + [200.0, -4.9] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 7.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.5], + [50.0, -0.2], + [200.0, -4.9] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.5], + [50.0, -0.2], + [200.0, -4.9] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [50.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.5], + [50.0, -0.2], + [200.0, -4.9] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.9], + [100.0, -2.6], + [1000.0, -32.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 300.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 300.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.9], + [100.0, -2.6], + [1000.0, -32.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [10.0, 0.0], + [100.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 300.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 300.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.9], + [100.0, -2.6], + [1000.0, -32.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.9], + [100.0, -2.6], + [1000.0, -32.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 300.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 300.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.9], + [100.0, -2.6], + [1000.0, -32.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [10.0, 0.0], + [100.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 300.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 300.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.9], + [100.0, -2.6], + [1000.0, -32.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_96channel_1000", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.35], + [10.0, 0.1], + [50.0, -1.3] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.35], + [10.0, 0.1], + [50.0, -1.3] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [10.0, 0.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.35], + [10.0, 0.1], + [50.0, -1.3] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.35], + [10.0, 0.1], + [50.0, -1.3] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.35], + [10.0, 0.1], + [50.0, -1.3] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [10.0, 0.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.35], + [10.0, 0.1], + [50.0, -1.3] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 7.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [50.0, -2.2], + [200.0, -8.9] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [50.0, -2.2], + [200.0, -8.9] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [50.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [50.0, -2.2], + [200.0, -8.9] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 7.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [50.0, -2.2], + [200.0, -8.9] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [50.0, -2.2], + [200.0, -8.9] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [50.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [50.0, -2.2], + [200.0, -8.9] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.4], + [100.0, -2.6], + [1000.0, -32.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 200.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.4], + [100.0, -2.6], + [1000.0, -32.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [10.0, 0.0], + [100.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 200.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.4], + [100.0, -2.6], + [1000.0, -32.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.4], + [100.0, -2.6], + [1000.0, -32.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 200.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.4], + [100.0, -2.6], + [1000.0, -32.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [10.0, 0.0], + [100.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 12.0], + [188.0, 12.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 200.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.4], + [100.0, -2.6], + [1000.0, -32.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_96channel_200", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [0.0, -0.1], + [5.0, -0.35], + [10.0, 0.08], + [50.0, -1.34] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [0.0, 7.0], + [10.0, 60.0], + [50.0, 60.0] + ], + "correctionByVolume": [ + [0.0, -0.1], + [5.0, -0.35], + [10.0, 0.08], + [50.0, -1.34] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [10.0, 0.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -2.1], + [10.0, -1.7], + [50.0, -3.3] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 30.0] + ], + "correctionByVolume": [ + [0.0, -0.1], + [5.0, -0.35], + [10.0, 0.08], + [50.0, -1.34] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [0.0, 7.0], + [10.0, 60.0], + [50.0, 60.0] + ], + "correctionByVolume": [ + [0.0, -0.1], + [5.0, -0.35], + [10.0, 0.08], + [50.0, -1.34] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [10.0, 0.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -2.1], + [10.0, -1.7], + [50.0, -3.3] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 7.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, 0.7], + [50.0, -2.2], + [200.0, -8.9] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [0.0, 7.0], + [10.0, 100.0], + [200.0, 100.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, 0.7], + [50.0, -2.2], + [200.0, -8.9] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [50.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -1.5], + [50.0, -2.2], + [200.0, -7.4] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": true, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 7.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, 0.7], + [50.0, -2.2], + [200.0, -8.9] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [0.0, 7.0], + [10.0, 100.0], + [200.0, 100.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, 0.7], + [50.0, -2.2], + [200.0, -8.9] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 0.0], + [50.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [5.0, 10.0], + [190.0, 10.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 100.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 100.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -1.5], + [50.0, -2.2], + [200.0, -7.4] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + } + ] + } + ] +} diff --git a/shared-data/liquid-class/definitions/1/glycerol_50/2.json b/shared-data/liquid-class/definitions/1/glycerol_50/2.json new file mode 100644 index 00000000000..c6007cf2fa9 --- /dev/null +++ b/shared-data/liquid-class/definitions/1/glycerol_50/2.json @@ -0,0 +1,6542 @@ +{ + "liquidClassName": "glycerol_50", + "displayName": "Viscous", + "description": "50% glycerol", + "schemaVersion": 1, + "version": 2, + "namespace": "opentrons", + "byPipette": [ + { + "pipetteModel": "flex_1channel_50", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 50.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.2], + [10.0, 0.1], + [50.0, -0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 25.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.2], + [10.0, 0.1], + [50.0, -0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 11.7], + [4.999, 11.7], + [5.0, 3.9], + [50.0, 3.9] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 25.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.2], + [10.0, 0.1], + [50.0, -0.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 50.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.2], + [10.0, 0.1], + [50.0, -0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 25.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.2], + [10.0, 0.1], + [50.0, -0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 11.7], + [4.999, 11.7], + [5.0, 3.9], + [50.0, 3.9] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 25.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.2], + [10.0, 0.1], + [50.0, -0.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_8channel_50", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 50.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.2], + [10.0, 0.1], + [50.0, -0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 25.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.2], + [10.0, 0.1], + [50.0, -0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 11.7], + [4.999, 11.7], + [5.0, 3.9], + [50.0, 3.9] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 25.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.2], + [10.0, 0.1], + [50.0, -0.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 50.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.2], + [10.0, 0.1], + [50.0, -0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 25.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.2], + [10.0, 0.1], + [50.0, -0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 11.7], + [4.999, 11.7], + [5.0, 3.9], + [50.0, 3.9] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 25.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [0.0, 0.0], + [1.0, -0.2], + [10.0, 0.1], + [50.0, -0.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_1channel_1000", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 30.0], + [10.0, 20.0], + [50.0, 20.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 30.0], + [10.0, 20.0], + [50.0, 20.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 10.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 20.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 10.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 20.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [1000.0, 800.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.7 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 250.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 35.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 250.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [1000.0, 800.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.7 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 250.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 35.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 250.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_8channel_1000", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 30.0], + [10.0, 20.0], + [50.0, 20.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 30.0], + [10.0, 20.0], + [50.0, 20.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 10.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 20.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 10.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 20.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [1000.0, 800.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.7 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 250.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 35.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 250.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [1000.0, 800.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.7 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 250.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 35.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 250.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_96channel_1000", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 40.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 30.0], + [10.0, 20.0], + [50.0, 20.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 40.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 40.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [5.0, 30.0], + [10.0, 20.0], + [50.0, 20.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 40.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 10.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 20.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 10.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 20.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [200.0, 200.0], + [1000.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.7 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 224.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 35.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 224.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [10.0, 10.0], + [100.0, 100.0], + [200.0, 200.0], + [1000.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.7 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 224.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 35.0]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 4, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 224.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 250.0]], + "correctionByVolume": [ + [0.0, 0.0], + [10.0, -0.2], + [100.0, -0.1], + [1000.0, 12] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [990.0, 5.0], + [995.0, 0.0], + [1000.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_96channel_200", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, -0.1], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 40.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [0.0, -0.1], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 11.7], + [4.999, 11.7], + [5.0, 3.9], + [50.0, 3.9] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 40.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.3], + [10.0, -0.2], + [50.0, 0.0] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, -0.1], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 2.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 40.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [0.0, -0.1], + [5.0, -0.25], + [10.0, 0.1], + [50.0, 0.2] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 11.7], + [4.999, 11.7], + [5.0, 3.9], + [50.0, 3.9] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 40.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 7.0], + [10.0, 10.0], + [50.0, 40.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.3], + [10.0, -0.2], + [50.0, 0.0] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [40.0, 5.0], + [45.0, 0.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 10.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 3.9]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.3], + [50.0, -0.3], + [200.0, -0.8] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 10.0], + [50.0, 50.0], + [200.0, 200.0] + ], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 25.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.2], + [50.0, -0.3], + [200.0, -0.8] + ], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[0.0, 3.9]], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 10, + "airGapByVolume": [[0.0, 0.0]], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[0.0, 50.0]], + "correctionByVolume": [ + [0.0, 0.0], + [5.0, -0.3], + [50.0, -0.3], + [200.0, -0.8] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [190.0, 5.0], + [195.0, 0.0], + [200.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + } + } + ] + } + ] +} diff --git a/shared-data/liquid-class/definitions/1/water/2.json b/shared-data/liquid-class/definitions/1/water/2.json new file mode 100644 index 00000000000..9047a88eeda --- /dev/null +++ b/shared-data/liquid-class/definitions/1/water/2.json @@ -0,0 +1,6332 @@ +{ + "liquidClassName": "water", + "displayName": "Aqueous", + "description": "Deionized water", + "schemaVersion": 1, + "version": 2, + "namespace": "opentrons", + "byPipette": [ + { + "pipetteModel": "flex_1channel_50", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 35.0], + [10.0, 24.0], + [50.0, 35.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 50.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 7.0], + [4.999, 7.0], + [5.0, 2.0], + [10.0, 2.0], + [50.0, 2.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 50.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 35.0], + [10.0, 24.0], + [50.0, 35.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 50.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 7.0], + [4.999, 7.0], + [5.0, 2.0], + [10.0, 2.0], + [50.0, 2.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 50.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_8channel_50", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 35.0], + [10.0, 24.0], + [50.0, 35.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 50.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 7.0], + [4.999, 7.0], + [5.0, 2.0], + [10.0, 2.0], + [50.0, 2.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 50.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [1.0, 35.0], + [10.0, 24.0], + [50.0, 35.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 50.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 7.0], + [4.999, 7.0], + [5.0, 2.0], + [10.0, 2.0], + [50.0, 2.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 50.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 50.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": true, + "params": { + "duration": 0.2 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_1channel_1000", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.0, 0.1], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.0, 0.1], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "destination", + "flowRate": 318.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 5.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.5 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 478.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.0, 0.1], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.0, 0.1], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "destination", + "flowRate": 478.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 5.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 478.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.75 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 15.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.75 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 15.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 20.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 20.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_8channel_1000", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.0, 0.1], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 1.0 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.0, 0.1], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "destination", + "flowRate": 478.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 5.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 478.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.0, 0.1], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 0.1], + [49.0, 0.1], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "destination", + "flowRate": 478.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 5.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 478.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.75 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 15.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 20.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 20.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 20.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 100, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 50, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 716.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_96channel_200", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 0.1], + [49.0, 0.1], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 6.5]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "destination", + "flowRate": 80.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 80.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 5.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 80.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 80.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 0.1], + [49.0, 0.1], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 6.5]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "destination", + "flowRate": 80.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 80.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 7.0], + [5.0, 2.0], + [10.0, 2.0], + [50.0, 5.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 80.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 80.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 2], + [50, 3.5], + [198.0, 2.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 80.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.75 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "destination", + "flowRate": 80.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 80.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 5.0], + [5.0, 5.0], + [50.0, 5.0], + [200.0, 5.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 80.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 80.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.75 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "destination", + "flowRate": 80.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 80.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [ + [1.0, 5.0], + [5.0, 5.0], + [50.0, 2.0], + [200.0, 5.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 80.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 80.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + } + ] + }, + { + "pipetteModel": "flex_96channel_1000", + "byTipType": [ + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 20.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 20.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.75 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 15.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.75 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 15.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 20.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + }, + { + "tiprack": "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", + "aspirate": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "aspiratePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "preWet": false, + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "delay": { + "enable": true, + "params": { + "duration": 0.5 + } + } + }, + "singleDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": false, + "params": { + "location": "destination", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { + "enable": false, + "params": { + "repetitions": 1, + "volume": 50 + } + }, + "pushOutByVolume": [[1.0, 20.0]], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "multiDispense": { + "submerge": { + "startPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + }, + "retract": { + "endPosition": { + "positionReference": "well-top", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "speed": 35, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], + "blowout": { + "enable": true, + "params": { + "location": "trash", + "flowRate": 200.0 + } + }, + "touchTip": { + "enable": false, + "params": { + "zOffset": -1, + "mmFromEdge": 0.5, + "speed": 30 + } + }, + "delay": { + "enable": false, + "params": { + "duration": 0 + } + } + }, + "dispensePosition": { + "positionReference": "well-bottom", + "offset": { + "x": 0, + "y": 0, + "z": 2 + } + }, + "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "delay": { + "enable": false, + "params": { + "duration": 0.0 + } + } + } + } + ] + } + ] +} diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index d6916bd8947..6490d1086b2 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -115,6 +115,10 @@ class LabwareRole(str, Enum): system = "system" +class Quirks(Enum): + disableGeometryBasedGripCheck = "disableGeometryBasedGripCheck" + + class Metadata(BaseModel): displayName: str displayCategory: DisplayCategory diff --git a/shared-data/python_tests/liquid_classes/test_load.py b/shared-data/python_tests/liquid_classes/test_load.py index 65ed5e78026..537231af258 100644 --- a/shared-data/python_tests/liquid_classes/test_load.py +++ b/shared-data/python_tests/liquid_classes/test_load.py @@ -42,4 +42,4 @@ def test_definition_exists() -> None: assert definition_exists(name="water", version=1) is True assert definition_exists(name="glycerol_50", version=1) is True assert definition_exists(name="glycerol_oh_no", version=1) is False - assert definition_exists(name="glycerol_50", version=2) is False + assert definition_exists(name="glycerol_50", version=9999) is False diff --git a/step-generation/src/__tests__/pythonFileUtils.test.ts b/step-generation/src/__tests__/pythonFileUtils.test.ts index 1b486b2b6c8..be55615f2e5 100644 --- a/step-generation/src/__tests__/pythonFileUtils.test.ts +++ b/step-generation/src/__tests__/pythonFileUtils.test.ts @@ -1,21 +1,21 @@ import { describe, expect, it } from 'vitest' import { - ETHANOL_LIQUID_CLASS_NAME, + ETHANOL_LIQUID_CLASS_NAME_V2, fixture96Plate, fixtureP300MultiV2Specs, fixtureP1000SingleV2Specs, fixtureTiprack1000ul, fixtureTiprackAdapter, FLEX_ROBOT_TYPE, - GLYCEROL_LIQUID_CLASS_NAME, + GLYCEROL_LIQUID_CLASS_NAME_V2, HEATERSHAKER_MODULE_TYPE, HEATERSHAKER_MODULE_V1, MAGNETIC_BLOCK_TYPE, MAGNETIC_BLOCK_V1, OT2_ROBOT_TYPE, WASTE_CHUTE_CUTOUT, - WATER_LIQUID_CLASS_NAME, + WATER_LIQUID_CLASS_NAME_V2, } from '@opentrons/shared-data' import { @@ -536,7 +536,7 @@ const mockLiquidEntities: LiquidEntities = { displayName: 'water', description: 'mock description', displayColor: 'mock display color', - liquidClass: WATER_LIQUID_CLASS_NAME, + liquidClass: WATER_LIQUID_CLASS_NAME_V2, }, [liquid2]: { liquidGroupId: liquid2, @@ -544,7 +544,7 @@ const mockLiquidEntities: LiquidEntities = { description: '', displayName: 'sulfur', displayColor: 'mock display color 2', - liquidClass: ETHANOL_LIQUID_CLASS_NAME, + liquidClass: ETHANOL_LIQUID_CLASS_NAME_V2, }, } @@ -665,9 +665,9 @@ describe('getLoadLiquidClasses', () => { it('should load a liquid class for each liquid class types', () => { expect( getLoadLiquidClasses([ - WATER_LIQUID_CLASS_NAME, - ETHANOL_LIQUID_CLASS_NAME, - GLYCEROL_LIQUID_CLASS_NAME, + WATER_LIQUID_CLASS_NAME_V2, + ETHANOL_LIQUID_CLASS_NAME_V2, + GLYCEROL_LIQUID_CLASS_NAME_V2, ]) ).toBe( ` diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index f12c348d06a..0abac657775 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -6345,6 +6345,13 @@ mock_pipette.transfer_with_liquid_class( }, }, }, + { + commandType: 'prepareToAspirate', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + }, + }, { commandType: 'airGapInPlace', key: expect.any(String), @@ -6522,6 +6529,13 @@ mock_pipette.transfer_with_liquid_class( }, }, }, + { + commandType: 'prepareToAspirate', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + }, + }, { commandType: 'airGapInPlace', key: expect.any(String), diff --git a/step-generation/src/commandCreators/compound/consolidate.ts b/step-generation/src/commandCreators/compound/consolidate.ts index d482dba0c19..825e1e3b31f 100644 --- a/step-generation/src/commandCreators/compound/consolidate.ts +++ b/step-generation/src/commandCreators/compound/consolidate.ts @@ -13,7 +13,7 @@ import { NONE_LIQUID_CLASS_NAME, POSITION_REFERENCE_MAPPED_TO_WELL_ORIGIN, SAFE_MOVE_TO_WELL_LOCATION, - WATER_LIQUID_CLASS_NAME, + WATER_LIQUID_CLASS_NAME_V2, WELL_ORIGIN_TOP, } from '@opentrons/shared-data' @@ -224,7 +224,7 @@ export const consolidate: CommandCreator = ( getAllLiquidClassDefs() [ liquidClass === NONE_LIQUID_CLASS_NAME || liquidClass == null - ? WATER_LIQUID_CLASS_NAME + ? WATER_LIQUID_CLASS_NAME_V2 : liquidClass ].byPipette?.find( ({ pipetteModel }) => diff --git a/step-generation/src/commandCreators/compound/distribute.ts b/step-generation/src/commandCreators/compound/distribute.ts index 3e91b9690ac..62646f64451 100644 --- a/step-generation/src/commandCreators/compound/distribute.ts +++ b/step-generation/src/commandCreators/compound/distribute.ts @@ -13,7 +13,7 @@ import { NONE_LIQUID_CLASS_NAME, POSITION_REFERENCE_MAPPED_TO_WELL_ORIGIN, SAFE_MOVE_TO_WELL_LOCATION, - WATER_LIQUID_CLASS_NAME, + WATER_LIQUID_CLASS_NAME_V2, WELL_ORIGIN_TOP, } from '@opentrons/shared-data' @@ -106,7 +106,6 @@ export const distribute: CommandCreator = ( blowoutFlowRateUlSec, blowoutLocation, changeTip, - conditioningVolume, destLabware, destWells, dispenseDelay, @@ -158,13 +157,19 @@ export const distribute: CommandCreator = ( const actionName = 'distribute' const errors: CommandCreatorError[] = [] const isMultiChannelPipette = pipetteEntities[pipette]?.spec.channels !== 1 - + const isTouchTipDisabled = labwareEntities[ + sourceLabware + ]?.def.parameters.quirks?.includes('touchTipDisabled') const aspirateAirGapVolume = args.aspirateAirGapVolume ?? 0 const dispenseAirGapVolume = args.dispenseAirGapVolume ?? 0 const disposalVolume = args.disposalVolume != null && args.disposalVolume > 0 ? args.disposalVolume : 0 + const conditioningVolume = + args.conditioningVolume != null && args.conditioningVolume > 0 + ? args.conditioningVolume + : 0 // TODO: Ian 2019-04-19 revisit these pipetteDoesNotExist errors, how to do it DRY? if ( prevRobotState.pipettes[pipette] == null || @@ -231,7 +236,7 @@ export const distribute: CommandCreator = ( getAllLiquidClassDefs() [ liquidClass === NONE_LIQUID_CLASS_NAME || liquidClass == null - ? WATER_LIQUID_CLASS_NAME + ? WATER_LIQUID_CLASS_NAME_V2 : liquidClass ].byPipette?.find( ({ pipetteModel }) => @@ -532,6 +537,8 @@ export const distribute: CommandCreator = ( (destWellChunk: string[], chunkIndex: number): CurriedCommandCreator[] => { const numDestsPerAsp = destWellChunk.length // can differ on final chunk const totalSampleAspirateVolume = volume * numDestsPerAsp + const totalGrossAspirateVolume = + totalSampleAspirateVolume + disposalVolume + conditioningVolume const isFirstChunk = chunkIndex === 0 const isLastChunk = chunkIndex === destWellChunks.length - 1 const changeTipNow = @@ -544,7 +551,7 @@ export const distribute: CommandCreator = ( ? [ curryWithoutPython(configureForVolume, { pipetteId: pipette, - volume: totalSampleAspirateVolume, + volume: totalGrossAspirateVolume, }), ] : [] @@ -762,10 +769,7 @@ export const distribute: CommandCreator = ( liquidClass, pipetteSpecs, tiprackDefUri: tipRack, - targetVolume: - totalSampleAspirateVolume + - (disposalVolume ?? 0) + - (conditioningVolume ?? 0), + targetVolume: totalGrossAspirateVolume, liquidHandlingAction: 'aspirate', byVolumeProperty: 'correctionByVolume', defaultValue: 0, @@ -775,7 +779,7 @@ export const distribute: CommandCreator = ( liquidClass, pipetteSpecs, tiprackDefUri: tipRack, - targetVolume: conditioningVolume ?? 0, + targetVolume: conditioningVolume, liquidHandlingAction: 'multiDispense', byVolumeProperty: 'correctionByVolume', defaultValue: 0, @@ -796,10 +800,7 @@ export const distribute: CommandCreator = ( const aspirateCommands = [ curryWithoutPython(aspirateInPlace, { pipetteId: pipette, - volume: - totalSampleAspirateVolume + - (disposalVolume ?? 0) + - (conditioningVolume ?? 0), + volume: totalGrossAspirateVolume, flowRate: aspirateFlowRateUlSec, correctionVolume: aspirateCorrectionVolumeForTotalAspiration, }), @@ -842,7 +843,7 @@ export const distribute: CommandCreator = ( } else if ( !isFirstWellInChunk && dispenseAirGapVolume > 0 && - (conditioningVolume == null || conditioningVolume === 0) + conditioningVolume === 0 ) { airGapInTip = dispenseAirGapVolume airGapDispenseFlowRate = dispenseAirGapDispenseFlowRate @@ -999,11 +1000,7 @@ export const distribute: CommandCreator = ( ): CurriedCommandCreator[] => dispenseAirGapVolume > 0 && // don't air gap if not last well in chunk and conditioning volume is present - !( - wellIndex < destWellChunk.length - 1 && - conditioningVolume != null && - conditioningVolume > 0 - ) && + !(wellIndex < destWellChunk.length - 1 && conditioningVolume > 0) && // don't air gap if end of full transfer and not changing tip !( changeTip === 'never' && @@ -1103,8 +1100,9 @@ export const distribute: CommandCreator = ( }, }), ...blowoutInPlaceCommand, - // touch tip at source well with dispense touch tip parameters - ...(touchTipAfterDispense + // touch tip at source well with source touch tip parameters + // only if source is touchTip-able + ...(touchTipAfterDispense && !isTouchTipDisabled ? [ curryWithoutPython(touchTip, { pipetteId: pipette, diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index 3d968990519..bf4a2eb7148 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -12,7 +12,7 @@ import { NONE_LIQUID_CLASS_NAME, POSITION_REFERENCE_MAPPED_TO_WELL_ORIGIN, SAFE_MOVE_TO_WELL_LOCATION, - WATER_LIQUID_CLASS_NAME, + WATER_LIQUID_CLASS_NAME_V2, WELL_ORIGIN_TOP, } from '@opentrons/shared-data' @@ -170,6 +170,10 @@ export const transfer: CommandCreator = ( args.destLabware ) + const isTouchTipDisabled = labwareEntities[ + sourceLabware + ]?.def.parameters.quirks?.includes('touchTipDisabled') + if ( (trashOrLabware === 'labware' && destWells != null && @@ -333,7 +337,7 @@ export const transfer: CommandCreator = ( getAllLiquidClassDefs() [ liquidClass === NONE_LIQUID_CLASS_NAME || liquidClass == null - ? WATER_LIQUID_CLASS_NAME + ? WATER_LIQUID_CLASS_NAME_V2 : liquidClass ].byPipette?.find( ({ pipetteModel }) => @@ -1000,6 +1004,9 @@ export const transfer: CommandCreator = ( ...(considerRetractSafety ? preDispenseAirGapMoveToCommand : []), + curryWithoutPython(prepareToAspirate, { + pipetteId: pipette, + }), curryWithoutPython(airGapInPlace, { pipetteId: pipette, volume: dispenseAirGapVol, @@ -1076,8 +1083,9 @@ export const transfer: CommandCreator = ( }, }), blowoutInPlaceCommand, - // touch tip at source well with dispense touch tip parameters - ...(touchTipAfterDispense + // touch tip at source well with source touch tip parameters + // only if source is touchTip-able + ...(touchTipAfterDispense && !isTouchTipDisabled ? [ curryWithoutPython(touchTip, { pipetteId: pipette, diff --git a/step-generation/src/fixtures/commandFixtures.ts b/step-generation/src/fixtures/commandFixtures.ts index 831d9346816..440d5e5b08d 100644 --- a/step-generation/src/fixtures/commandFixtures.ts +++ b/step-generation/src/fixtures/commandFixtures.ts @@ -583,6 +583,13 @@ export const blowoutInTrashCommands = (args: { }, ...(dispenseAirGap > 0 ? [ + { + commandType: 'prepareToAspirate', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + }, + }, { commandType: 'airGapInPlace', key: expect.any(String), @@ -852,6 +859,13 @@ export const dispenseHelperLiquidClass = (params: { : SAFE_MOVE_TO_WELL_LOCATION, }, }, + { + commandType: 'prepareToAspirate', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + }, + }, { commandType: 'airGapInPlace', key: expect.any(String), diff --git a/step-generation/src/utils/liquidClassUtils.ts b/step-generation/src/utils/liquidClassUtils.ts index 6e30d9bf12c..e471ad76e1f 100644 --- a/step-generation/src/utils/liquidClassUtils.ts +++ b/step-generation/src/utils/liquidClassUtils.ts @@ -51,13 +51,17 @@ export const getCustomLiquidClassProperties = ( }, flow_rate_by_volume: [[0, args.dispenseFlowRateUlSec ?? 0]], delay: { - enabled: args.dispenseDelay != null, - duration: args.dispenseDelay?.seconds ?? undefined, + enabled: !!args.dispenseDelay?.seconds, + ...(args.dispenseDelay?.seconds + ? { duration: args.dispenseDelay?.seconds } + : {}), }, submerge: { delay: { - enabled: args.dispenseSubmergeDelay != null, - duration: args.dispenseSubmergeDelay?.seconds ?? undefined, + enabled: !!args.dispenseSubmergeDelay?.seconds, + ...(args.dispenseSubmergeDelay?.seconds + ? { duration: args.dispenseSubmergeDelay?.seconds } + : {}), }, speed: args.dispenseSubmergeSpeed ?? undefined, start_position: { @@ -72,8 +76,10 @@ export const getCustomLiquidClassProperties = ( retract: { air_gap_by_volume: [[0, args.dispenseAirGapVolume ?? 0]], delay: { - enabled: args.dispenseRetractDelay != null, - duration: args.dispenseRetractDelay?.seconds ?? undefined, + enabled: !!args.dispenseRetractDelay?.seconds, + ...(args.dispenseRetractDelay?.seconds + ? { duration: args.dispenseRetractDelay?.seconds } + : {}), }, end_position: { offset: { @@ -127,18 +133,26 @@ export const getCustomLiquidClassProperties = ( correction_by_volume: liquidClassValuesForTip?.aspirate .correctionByVolume ?? [[0, 0]], // nullish coalescing for type checks. Should never hit delay: { - enabled: args.aspirateDelay != null, - duration: args.aspirateDelay?.seconds ?? undefined, + enabled: !!args.aspirateDelay?.seconds, + ...(args.aspirateDelay?.seconds + ? { duration: args.aspirateDelay?.seconds } + : {}), }, mix: { - enabled: aspirateMixArgs != null, - repetitions: aspirateMixArgs?.times ?? undefined, - volume: aspirateMixArgs?.volume ?? undefined, + enabled: !!aspirateMixArgs?.volume, + ...(aspirateMixArgs?.times + ? { repetitions: aspirateMixArgs.times } + : {}), + ...(aspirateMixArgs?.volume + ? { volume: aspirateMixArgs.volume } + : {}), }, submerge: { delay: { - enabled: args.aspirateSubmergeDelay != null, - duration: args.aspirateSubmergeDelay?.seconds ?? undefined, + enabled: !!args.aspirateSubmergeDelay?.seconds, + ...(args.aspirateSubmergeDelay?.seconds + ? { duration: args.aspirateSubmergeDelay?.seconds } + : {}), }, speed: args.aspirateSubmergeSpeed ?? undefined, start_position: { @@ -153,8 +167,10 @@ export const getCustomLiquidClassProperties = ( retract: { air_gap_by_volume: [[0, args.aspirateAirGapVolume ?? 0]], delay: { - enabled: args.aspirateRetractDelay != null, - duration: args.aspirateRetractDelay?.seconds ?? undefined, + enabled: !!args.aspirateRetractDelay?.seconds, + ...(args.aspirateRetractDelay?.seconds + ? { duration: args.aspirateRetractDelay?.seconds } + : {}), }, end_position: { offset: { diff --git a/step-generation/src/utils/pythonFileUtils.ts b/step-generation/src/utils/pythonFileUtils.ts index d4af5fc9c3d..033d9b24783 100644 --- a/step-generation/src/utils/pythonFileUtils.ts +++ b/step-generation/src/utils/pythonFileUtils.ts @@ -42,7 +42,7 @@ import type { } from '../types' const PAPI_VERSION = '2.24' // latest version from api/src/opentrons/protocols/api_support/definitions.py -export const PD_APPLICATION_VERSION = '8.5.0' // latest PD version to insert into DESIGNER_APPLICATION blob +export const PD_APPLICATION_VERSION = '8.5.5' // latest PD version to insert into DESIGNER_APPLICATION blob export function pythonImports(): string { return ['import json', 'from opentrons import protocol_api, types'].join('\n')