diff --git a/api/src/opentrons/hardware_control/api.py b/api/src/opentrons/hardware_control/api.py index c52fae64131..a5dbae274fb 100644 --- a/api/src/opentrons/hardware_control/api.py +++ b/api/src/opentrons/hardware_control/api.py @@ -1039,6 +1039,7 @@ async def aspirate( mount: top_types.Mount, volume: Optional[float] = None, rate: float = 1.0, + correction_volume: float = 0.0, ) -> None: """ Aspirate a volume of liquid (in microliters/uL) using this pipette. @@ -1073,6 +1074,7 @@ async def dispense( volume: Optional[float] = None, rate: float = 1.0, push_out: Optional[float] = None, + correction_volume: float = 0.0, ) -> None: """ Dispense a volume of liquid in microliters(uL) using this pipette. 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 ef081b95a62..7c553584500 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py @@ -502,10 +502,19 @@ def ready_for_tip_action( self._ihp_log.debug(f"{action} on {target.name}") def plunger_position( - self, instr: Pipette, ul: float, action: "UlPerMmAction" + self, + instr: Pipette, + ul: float, + action: "UlPerMmAction", + correction_volume: float = 0.0, ) -> float: - mm = ul / instr.ul_per_mm(ul, action) - position = instr.plunger_positions.bottom - mm + if ul == 0: + position = instr.plunger_positions.bottom + else: + multiplier = 1.0 + (correction_volume / ul) + mm_dist_from_bottom = ul / instr.ul_per_mm(ul, action) + mm_dist_from_bottom_corrected = mm_dist_from_bottom * multiplier + position = instr.plunger_positions.bottom - mm_dist_from_bottom_corrected return round(position, 6) def plunger_speed( @@ -531,6 +540,7 @@ def plan_check_aspirate( mount: OT3Mount, volume: Optional[float], rate: float, + correction_volume: float = 0.0, ) -> Optional[LiquidActionSpec]: """Check preconditions for aspirate, parse args, and calculate positions. @@ -566,7 +576,10 @@ def plan_check_aspirate( ), "Cannot aspirate more than pipette max volume" dist = self.plunger_position( - instrument, instrument.current_volume + asp_vol, "aspirate" + instr=instrument, + ul=instrument.current_volume + asp_vol, + action="aspirate", + correction_volume=correction_volume, ) speed = self.plunger_speed( instrument, instrument.aspirate_flow_rate * rate, "aspirate" @@ -591,6 +604,7 @@ def plan_check_dispense( volume: Optional[float], rate: float, push_out: Optional[float], + correction_volume: float = 0.0, ) -> Optional[LiquidActionSpec]: """Check preconditions for dispense, parse args, and calculate positions. @@ -659,7 +673,10 @@ def plan_check_dispense( ) dist = self.plunger_position( - instrument, instrument.current_volume - disp_vol, "dispense" + instr=instrument, + ul=instrument.current_volume - disp_vol, + action="dispense", + correction_volume=correction_volume, ) speed = self.plunger_speed( instrument, instrument.dispense_flow_rate * rate, "dispense" diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 6295757e7ab..bf59793d9dc 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2046,12 +2046,16 @@ async def aspirate( mount: Union[top_types.Mount, OT3Mount], volume: Optional[float] = None, rate: float = 1.0, + correction_volume: float = 0.0, ) -> None: """ Aspirate a volume of liquid (in microliters/uL) using this pipette.""" realmount = OT3Mount.from_mount(mount) aspirate_spec = self._pipette_handler.plan_check_aspirate( - realmount, volume, rate + mount=realmount, + volume=volume, + rate=rate, + correction_volume=correction_volume, ) if not aspirate_spec: return @@ -2088,12 +2092,17 @@ async def dispense( volume: Optional[float] = None, rate: float = 1.0, push_out: Optional[float] = None, + correction_volume: float = 0.0, ) -> None: """ Dispense a volume of liquid in microliters(uL) using this pipette.""" realmount = OT3Mount.from_mount(mount) dispense_spec = self._pipette_handler.plan_check_dispense( - realmount, volume, rate, push_out + mount=realmount, + volume=volume, + rate=rate, + push_out=push_out, + correction_volume=correction_volume, ) if not dispense_spec: return diff --git a/api/src/opentrons/hardware_control/protocols/liquid_handler.py b/api/src/opentrons/hardware_control/protocols/liquid_handler.py index 2aea15bd55b..045a21795ea 100644 --- a/api/src/opentrons/hardware_control/protocols/liquid_handler.py +++ b/api/src/opentrons/hardware_control/protocols/liquid_handler.py @@ -98,6 +98,7 @@ async def aspirate( mount: MountArgType, volume: Optional[float] = None, rate: float = 1.0, + correction_volume: float = 0.0, ) -> None: """ Aspirate a volume of liquid (in microliters/uL) using this pipette @@ -117,6 +118,7 @@ async def aspirate( volume : [float] The number of microliters to aspirate rate : [float] Set plunger speed for this aspirate, where speed = rate * aspirate_speed + correction_volume : Correction volume in uL for the specified aspirate volume """ ... @@ -126,6 +128,7 @@ async def dispense( volume: Optional[float] = None, rate: float = 1.0, push_out: Optional[float] = None, + correction_volume: float = 0.0, ) -> None: """ Dispense a volume of liquid in microliters(uL) using this pipette @@ -136,6 +139,7 @@ async def dispense( volume : [float] The number of microliters to dispense rate : [float] Set plunger speed for this dispense, where speed = rate * dispense_speed + correction_volume : Correction volume in uL for the specified dispense volume """ ... diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 9ae9b349789..30ace69e63b 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -126,7 +126,9 @@ def set_default_speed(self, speed: float) -> None: pipette_id=self._pipette_id, speed=speed ) - def air_gap_in_place(self, volume: float, flow_rate: float) -> None: + def air_gap_in_place( + self, volume: float, flow_rate: float, correction_volume: Optional[float] = None + ) -> None: """Aspirate a given volume of air from the current location of the pipette. Args: @@ -135,7 +137,10 @@ def air_gap_in_place(self, volume: float, flow_rate: float) -> None: """ self._engine_client.execute_command( cmd.AirGapInPlaceParams( - pipetteId=self._pipette_id, volume=volume, flowRate=flow_rate + pipetteId=self._pipette_id, + volume=volume, + flowRate=flow_rate, + correctionVolume=correction_volume, ) ) @@ -148,6 +153,7 @@ def aspirate( flow_rate: float, in_place: bool, is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: """Aspirate a given volume of liquid from the specified location. Args: @@ -174,7 +180,10 @@ def aspirate( self._engine_client.execute_command( cmd.AspirateInPlaceParams( - pipetteId=self._pipette_id, volume=volume, flowRate=flow_rate + pipetteId=self._pipette_id, + volume=volume, + flowRate=flow_rate, + correctionVolume=correction_volume, ) ) @@ -205,6 +214,7 @@ def aspirate( wellLocation=well_location, volume=volume, flowRate=flow_rate, + correctionVolume=correction_volume, ) ) @@ -220,6 +230,7 @@ def dispense( in_place: bool, push_out: Optional[float], is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: """Dispense a given volume of liquid into the specified location. Args: @@ -267,6 +278,7 @@ def dispense( volume=volume, flowRate=flow_rate, pushOut=push_out, + correctionVolume=correction_volume, ) ) else: @@ -297,6 +309,7 @@ def dispense( volume=volume, flowRate=flow_rate, pushOut=push_out, + correctionVolume=correction_volume, ) ) @@ -1110,6 +1123,18 @@ def distribute_liquid( ) -> None: pass + def consolidate_liquid( + self, + liquid_class: LiquidClass, + volume: float, + source: List[Tuple[Location, WellCore]], + dest: Tuple[Location, WellCore], + new_tip: TransferTipPolicyV2, + tip_racks: List[Tuple[Location, LabwareCore]], + trash_location: Union[Location, TrashBin, WasteChute], + ) -> None: + pass + def _get_location_and_well_core_from_next_tip_info( self, tip_info: NextTipInfo, diff --git a/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py b/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py index 7dc332e6a37..cf61e41909c 100644 --- a/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py +++ b/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py @@ -161,6 +161,7 @@ def aspirate_and_wait(self, volume: float) -> None: """Aspirate according to aspirate properties and wait if enabled.""" # TODO: handle volume correction aspirate_props = self._transfer_properties.aspirate + correction_volume = aspirate_props.correction_by_volume.get_for_volume(volume) self._instrument.aspirate( location=self._target_location, well_core=None, @@ -169,6 +170,7 @@ def aspirate_and_wait(self, volume: float) -> None: flow_rate=aspirate_props.flow_rate_by_volume.get_for_volume(volume), in_place=True, is_meniscus=None, # TODO: update this once meniscus is implemented + correction_volume=correction_volume, ) self._tip_state.append_liquid(volume) delay_props = aspirate_props.delay @@ -183,6 +185,7 @@ def dispense_and_wait( """Dispense according to dispense properties and wait if enabled.""" # TODO: handle volume correction dispense_props = self._transfer_properties.dispense + correction_volume = dispense_props.correction_by_volume.get_for_volume(volume) self._instrument.dispense( location=self._target_location, well_core=None, @@ -192,6 +195,7 @@ def dispense_and_wait( in_place=True, push_out=push_out_override, is_meniscus=None, + correction_volume=correction_volume, ) if push_out_override: # If a push out was performed, we need to reset the plunger before we can aspirate again @@ -504,12 +508,19 @@ def _add_air_gap(self, air_gap_volume: float) -> None: if air_gap_volume == 0: return aspirate_props = self._transfer_properties.aspirate + correction_volume = aspirate_props.correction_by_volume.get_for_volume( + air_gap_volume + ) # The maximum flow rate should be air_gap_volume per second flow_rate = min( aspirate_props.flow_rate_by_volume.get_for_volume(air_gap_volume), air_gap_volume, ) - self._instrument.air_gap_in_place(volume=air_gap_volume, flow_rate=flow_rate) + self._instrument.air_gap_in_place( + volume=air_gap_volume, + flow_rate=flow_rate, + correction_volume=correction_volume, + ) delay_props = aspirate_props.delay if delay_props.enabled: # Assertion only for mypy purposes @@ -524,6 +535,9 @@ def _remove_air_gap(self, location: Location) -> None: return dispense_props = self._transfer_properties.dispense + correction_volume = dispense_props.correction_by_volume.get_for_volume( + last_air_gap + ) # The maximum flow rate should be air_gap_volume per second flow_rate = min( dispense_props.flow_rate_by_volume.get_for_volume(last_air_gap), @@ -538,6 +552,7 @@ def _remove_air_gap(self, location: Location) -> None: in_place=True, is_meniscus=None, push_out=0, + correction_volume=correction_volume, ) self._tip_state.delete_air_gap(last_air_gap) dispense_delay = dispense_props.delay diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index aa653dccaea..4a60cf8f19e 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -26,11 +26,14 @@ def set_default_speed(self, speed: float) -> None: ... @abstractmethod - def air_gap_in_place(self, volume: float, flow_rate: float) -> None: + def air_gap_in_place( + self, volume: float, flow_rate: float, correction_volume: Optional[float] = None + ) -> None: """Aspirate a given volume of air from the current location of the pipette. Args: volume: The volume of air to aspirate, in microliters. flow_rate: The flow rate of air into the pipette, in microliters. + correction_volume: The correction volume in uL. """ @abstractmethod @@ -43,6 +46,7 @@ def aspirate( flow_rate: float, in_place: bool, is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: """Aspirate a given volume of liquid from the specified location. Args: @@ -52,6 +56,7 @@ def aspirate( rate: The rate for how quickly to aspirate. flow_rate: The flow rate in µL/s to aspirate at. in_place: Whether this is in-place. + correction_volume: The correction volume in uL """ ... @@ -66,6 +71,7 @@ def dispense( in_place: bool, push_out: Optional[float], is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: """Dispense a given volume of liquid into the specified location. Args: @@ -76,6 +82,7 @@ def dispense( flow_rate: The flow rate in µL/s to dispense at. in_place: Whether this is in-place. push_out: The amount to push the plunger below bottom position. + correction_volume: The correction volume in uL """ ... @@ -343,6 +350,23 @@ def distribute_liquid( """ ... + @abstractmethod + def consolidate_liquid( + self, + liquid_class: LiquidClass, + volume: float, + source: List[Tuple[types.Location, WellCoreType]], + dest: Tuple[types.Location, WellCoreType], + new_tip: TransferTipPolicyV2, + tip_racks: List[Tuple[types.Location, LabwareCoreType]], + trash_location: Union[types.Location, TrashBin, WasteChute], + ) -> None: + """ + Consolidate liquid from multiple sources to a single destination + using the specified liquid class properties. + """ + ... + @abstractmethod def is_tip_tracking_available(self) -> bool: """Return whether auto tip tracking is available for the pipette's current nozzle configuration.""" diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index 46cf36de2e9..5572ef440d9 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -74,7 +74,9 @@ def set_default_speed(self, speed: float) -> None: """Sets the speed at which the robot's gantry moves.""" self._default_speed = speed - def air_gap_in_place(self, volume: float, flow_rate: float) -> None: + def air_gap_in_place( + self, volume: float, flow_rate: float, correction_volume: Optional[float] = None + ) -> None: assert False, "Air gap tracking only available in API version 2.22 and later" def aspirate( @@ -86,6 +88,7 @@ def aspirate( flow_rate: float, in_place: bool, is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: """Aspirate a given volume of liquid from the specified location. Args: @@ -95,6 +98,7 @@ def aspirate( rate: The rate in µL/s to aspirate at. flow_rate: Not used in this core. in_place: Whether we should move_to location. + correction_volume: Not used in this core """ if self.get_current_volume() == 0: # Make sure we're at the top of the labware and clear of any @@ -129,6 +133,7 @@ def dispense( in_place: bool, push_out: Optional[float], is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: """Dispense a given volume of liquid into the specified location. Args: @@ -138,6 +143,7 @@ def dispense( rate: The rate in µL/s to dispense at. flow_rate: Not used in this core. in_place: Whether we should move_to location. + correction_volume: Not used in this core. push_out: The amount to push the plunger below bottom position. """ if isinstance(location, (TrashBin, WasteChute)): @@ -586,6 +592,19 @@ def distribute_liquid( """This will never be called because it was added in API 2.23""" assert False, "distribute_liquid is not supported in legacy context" + def consolidate_liquid( + self, + liquid_class: LiquidClass, + volume: float, + source: List[Tuple[types.Location, LegacyWellCore]], + dest: Tuple[types.Location, LegacyWellCore], + new_tip: TransferTipPolicyV2, + tip_racks: List[Tuple[types.Location, LegacyLabwareCore]], + trash_location: Union[types.Location, TrashBin, WasteChute], + ) -> None: + """This will never be called because it was added in API 2.23.""" + assert False, "consolidate_liquid is not supported in legacy context" + def get_active_channels(self) -> int: """This will never be called because it was added in API 2.16.""" assert False, "get_active_channels only supported in API 2.16 & later" diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index 93445f94f05..8c2c570ad1c 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -87,7 +87,9 @@ def get_default_speed(self) -> float: def set_default_speed(self, speed: float) -> None: self._default_speed = speed - def air_gap_in_place(self, volume: float, flow_rate: float) -> None: + def air_gap_in_place( + self, volume: float, flow_rate: float, correction_volume: Optional[float] = None + ) -> None: assert False, "Air gap tracking only available in API version 2.22 and later" def aspirate( @@ -99,6 +101,7 @@ def aspirate( flow_rate: float, in_place: bool, is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: if self.get_current_volume() == 0: # Make sure we're at the top of the labware and clear of any @@ -141,6 +144,7 @@ def dispense( in_place: bool, push_out: Optional[float], is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: if isinstance(location, (TrashBin, WasteChute)): raise APIVersionError( @@ -506,6 +510,19 @@ def distribute_liquid( """This will never be called because it was added in API 2.23.""" assert False, "distribute_liquid is not supported in legacy context" + def consolidate_liquid( + self, + liquid_class: LiquidClass, + volume: float, + source: List[Tuple[types.Location, LegacyWellCore]], + dest: Tuple[types.Location, LegacyWellCore], + new_tip: TransferTipPolicyV2, + tip_racks: List[Tuple[types.Location, LegacyLabwareCore]], + trash_location: Union[types.Location, TrashBin, WasteChute], + ) -> None: + """This will never be called because it was added in API 2.23.""" + assert False, "consolidate_liquid is not supported in legacy context" + def get_active_channels(self) -> int: """This will never be called because it was added in API 2.16.""" assert False, "get_active_channels only supported in API 2.16 & later" diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 11552ef9ec3..6192b35670a 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -1689,6 +1689,94 @@ def distribute_liquid( ) return self + def consolidate_liquid( + self, + liquid_class: LiquidClass, + volume: float, + source: Union[ + labware.Well, Sequence[labware.Well], Sequence[Sequence[labware.Well]] + ], + dest: labware.Well, + new_tip: TransferTipPolicyV2Type = "once", + trash_location: Optional[ + Union[types.Location, labware.Well, TrashBin, WasteChute] + ] = None, + ) -> InstrumentContext: + """ + Consolidate liquid from multiple sources to a single destination + using the specified liquid class properties. + + TODO: Add args description. + """ + if not feature_flags.allow_liquid_classes( + robot_type=RobotTypeEnum.robot_literal_to_enum( + self._protocol_core.robot_type + ) + ): + raise NotImplementedError("This method is not implemented.") + if not isinstance(dest, labware.Well): + raise ValueError( + f"Destination should be a single Well but received {dest}." + ) + flat_sources_list = validation.ensure_valid_flat_wells_list_for_transfer_v2( + source + ) + for well in flat_sources_list + [dest]: + instrument.validate_takes_liquid( + location=well.top(), + reject_module=True, + reject_adapter=True, + ) + + valid_new_tip = validation.ensure_new_tip_policy(new_tip) + if valid_new_tip == TransferTipPolicyV2.NEVER: + if self._last_tip_picked_up_from is None: + raise RuntimeError( + "Pipette has no tip attached to perform transfer." + " Either do a pick_up_tip beforehand or specify a new_tip parameter" + " of 'once' or 'always'." + ) + else: + tip_racks = [self._last_tip_picked_up_from.parent] + else: + tip_racks = self._tip_racks + if self.current_volume != 0: + raise RuntimeError( + "A transfer on a liquid class cannot start with liquid already in the tip." + " Ensure that all previously aspirated liquid is dispensed before starting" + " a new transfer." + ) + + _trash_location: Union[types.Location, labware.Well, TrashBin, WasteChute] + if trash_location is None: + saved_trash = self.trash_container + if isinstance(saved_trash, labware.Labware): + _trash_location = saved_trash.wells()[0] + else: + _trash_location = saved_trash + else: + _trash_location = trash_location + + checked_trash_location = validation.ensure_valid_trash_location_for_transfer_v2( + trash_location=_trash_location + ) + self._core.consolidate_liquid( + liquid_class=liquid_class, + volume=volume, + source=[ + (types.Location(types.Point(), labware=well), well._core) + for well in flat_sources_list + ], + dest=(types.Location(types.Point(), labware=dest), dest._core), + new_tip=valid_new_tip, + tip_racks=[ + (types.Location(types.Point(), labware=rack), rack._core) + for rack in tip_racks + ], + trash_location=checked_trash_location, + ) + return self + @requires_version(2, 0) def delay(self, *args: Any, **kwargs: Any) -> None: """ diff --git a/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py b/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py index 461a446f3e4..bf6bbafc3d5 100644 --- a/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py @@ -14,6 +14,7 @@ FlowRateMixin, BaseLiquidHandlingResult, OverpressureError, + DEFAULT_CORRECTION_VOLUME, ) from .command import ( AbstractCommandImpl, @@ -103,6 +104,7 @@ async def execute(self, params: AirGapInPlaceParams) -> _ExecuteReturn: volume=params.volume, flow_rate=params.flowRate, command_note_adder=self._command_note_adder, + correction_volume=params.correctionVolume or DEFAULT_CORRECTION_VOLUME, ) except PipetteOverpressureError as e: return DefinedErrorData( diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index b07cd522f93..708ee3ecbdb 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -12,6 +12,7 @@ BaseLiquidHandlingResult, aspirate_in_place, prepare_for_aspirate, + DEFAULT_CORRECTION_VOLUME, ) from .movement_common import ( LiquidHandlingWellLocationMixin, @@ -182,6 +183,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: command_note_adder=self._command_note_adder, pipetting=self._pipetting, model_utils=self._model_utils, + correction_volume=params.correctionVolume or DEFAULT_CORRECTION_VOLUME, ) state_update.append(aspirate_result.state_update) if isinstance(aspirate_result, DefinedErrorData): diff --git a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py index 434924928d7..a5e68d7c1f0 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py @@ -13,6 +13,7 @@ BaseLiquidHandlingResult, OverpressureError, aspirate_in_place, + DEFAULT_CORRECTION_VOLUME, ) from .command import ( AbstractCommandImpl, @@ -108,6 +109,7 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn: command_note_adder=self._command_note_adder, pipetting=self._pipetting, model_utils=self._model_utils, + correction_volume=params.correctionVolume or DEFAULT_CORRECTION_VOLUME, ) if isinstance(result, DefinedErrorData): if ( diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index 8ad2365ccb5..d1290190c64 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -16,6 +16,7 @@ BaseLiquidHandlingResult, OverpressureError, dispense_in_place, + DEFAULT_CORRECTION_VOLUME, ) from .movement_common import ( LiquidHandlingWellLocationMixin, @@ -117,6 +118,7 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: }, pipetting=self._pipetting, model_utils=self._model_utils, + correction_volume=params.correctionVolume or DEFAULT_CORRECTION_VOLUME, ) if isinstance(dispense_result, DefinedErrorData): diff --git a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py index 117aa011a84..ff09f5444ee 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py @@ -13,6 +13,7 @@ BaseLiquidHandlingResult, OverpressureError, dispense_in_place, + DEFAULT_CORRECTION_VOLUME, ) from .command import ( AbstractCommandImpl, @@ -95,6 +96,7 @@ async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn: }, pipetting=self._pipetting, model_utils=self._model_utils, + correction_volume=params.correctionVolume or DEFAULT_CORRECTION_VOLUME, ) if isinstance(result, DefinedErrorData): if ( diff --git a/api/src/opentrons/protocol_engine/commands/pipetting_common.py b/api/src/opentrons/protocol_engine/commands/pipetting_common.py index 6740a4babb3..c0bca3c428a 100644 --- a/api/src/opentrons/protocol_engine/commands/pipetting_common.py +++ b/api/src/opentrons/protocol_engine/commands/pipetting_common.py @@ -1,7 +1,7 @@ """Common pipetting command base models.""" from __future__ import annotations -from typing import Literal, Tuple, TYPE_CHECKING +from typing import Literal, Tuple, TYPE_CHECKING, Optional from typing_extensions import TypedDict from pydantic import BaseModel, Field @@ -20,6 +20,10 @@ from ..notes import CommandNoteAdder +DEFAULT_CORRECTION_VOLUME = 0.0 +"""Default correction volume (uL) for any aspirate/ dispense volume.""" + + class PipetteIdMixin(BaseModel): """Mixin for command requests that take a pipette ID.""" @@ -41,6 +45,11 @@ class AspirateVolumeMixin(BaseModel): " There is some tolerance for floating point rounding errors.", ge=0, ) + correctionVolume: Optional[float] = Field( + None, + description="The correction volume in uL.", + ge=0, + ) class DispenseVolumeMixin(BaseModel): @@ -53,6 +62,11 @@ class DispenseVolumeMixin(BaseModel): " There is some tolerance for floating point rounding errors.", ge=0, ) + correctionVolume: Optional[float] = Field( + None, + description="The correction volume in uL.", + ge=0, + ) class FlowRateMixin(BaseModel): @@ -176,18 +190,20 @@ async def aspirate_in_place( pipette_id: str, volume: float, flow_rate: float, + correction_volume: float, location_if_error: ErrorLocationInfo, command_note_adder: CommandNoteAdder, pipetting: PipettingHandler, model_utils: ModelUtils, ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]: - """Execute an aspirate in place microoperation.""" + """Execute an aspirate in place micro-operation.""" try: volume_aspirated = await pipetting.aspirate_in_place( pipette_id=pipette_id, volume=volume, flow_rate=flow_rate, command_note_adder=command_note_adder, + correction_volume=correction_volume, ) except PipetteOverpressureError as e: return DefinedErrorData( @@ -320,17 +336,19 @@ async def dispense_in_place( volume: float, flow_rate: float, push_out: float | None, + correction_volume: float, location_if_error: ErrorLocationInfo, pipetting: PipettingHandler, model_utils: ModelUtils, ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]: - """Dispense-in-place as a microoperation.""" + """Dispense-in-place as a micro-operation.""" try: volume = await pipetting.dispense_in_place( pipette_id=pipette_id, volume=volume, flow_rate=flow_rate, push_out=push_out, + correction_volume=correction_volume, ) except PipetteOverpressureError as e: return DefinedErrorData( diff --git a/api/src/opentrons/protocol_engine/execution/pipetting.py b/api/src/opentrons/protocol_engine/execution/pipetting.py index 0cbca0f9079..7c45479387d 100644 --- a/api/src/opentrons/protocol_engine/execution/pipetting.py +++ b/api/src/opentrons/protocol_engine/execution/pipetting.py @@ -42,6 +42,7 @@ async def aspirate_in_place( volume: float, flow_rate: float, command_note_adder: CommandNoteAdder, + correction_volume: float = 0.0, ) -> float: """Set flow-rate and aspirate.""" @@ -73,6 +74,7 @@ async def dispense_in_place( volume: float, flow_rate: float, push_out: Optional[float], + correction_volume: float = 0.0, ) -> float: """Set flow-rate and dispense.""" @@ -195,6 +197,7 @@ async def aspirate_in_place( volume: float, flow_rate: float, command_note_adder: CommandNoteAdder, + correction_volume: float = 0.0, ) -> float: """Set flow-rate and aspirate. @@ -207,7 +210,9 @@ async def aspirate_in_place( ) with self._set_flow_rate(pipette=hw_pipette, aspirate_flow_rate=flow_rate): await self._hardware_api.aspirate( - mount=hw_pipette.mount, volume=adjusted_volume + mount=hw_pipette.mount, + volume=adjusted_volume, + correction_volume=correction_volume, ) return adjusted_volume @@ -218,6 +223,7 @@ async def dispense_in_place( volume: float, flow_rate: float, push_out: Optional[float], + correction_volume: float = 0.0, ) -> float: """Dispense liquid without moving the pipette.""" hw_pipette, adjusted_volume = self.get_hw_dispense_params(pipette_id, volume) @@ -228,7 +234,10 @@ async def dispense_in_place( ) with self._set_flow_rate(pipette=hw_pipette, dispense_flow_rate=flow_rate): await self._hardware_api.dispense( - mount=hw_pipette.mount, volume=adjusted_volume, push_out=push_out + mount=hw_pipette.mount, + volume=adjusted_volume, + push_out=push_out, + correction_volume=correction_volume, ) return adjusted_volume @@ -326,6 +335,7 @@ async def aspirate_in_place( volume: float, flow_rate: float, command_note_adder: CommandNoteAdder, + correction_volume: float = 0.0, ) -> float: """Virtually aspirate (no-op).""" self._validate_tip_attached(pipette_id=pipette_id, command_name="aspirate") @@ -342,6 +352,7 @@ async def dispense_in_place( volume: float, flow_rate: float, push_out: Optional[float], + correction_volume: float = 0.0, ) -> float: """Virtually dispense (no-op).""" # TODO (tz, 8-23-23): add a check for push_out not larger that the max volume allowed when working on this https://opentrons.atlassian.net/browse/RSS-329 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 9a3ecaad69b..8ac1ffc1dc8 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 @@ -599,6 +599,7 @@ def test_aspirate_from_well( rate=5.6, flow_rate=7.8, in_place=False, + correction_volume=123, ) decoy.verify( @@ -621,6 +622,7 @@ def test_aspirate_from_well( ), volume=12.34, flowRate=7.8, + correctionVolume=123, ) ), mock_protocol_core.set_last_location(location=location, mount=Mount.LEFT), @@ -659,6 +661,7 @@ def test_aspirate_from_coordinates( pipetteId="abc123", volume=12.34, flowRate=7.8, + correctionVolume=None, ) ), mock_protocol_core.set_last_location(location=location, mount=Mount.LEFT), @@ -725,6 +728,7 @@ def test_aspirate_from_meniscus( ), volume=12.34, flowRate=7.8, + correctionVolume=None, ) ), mock_protocol_core.set_last_location(location=location, mount=Mount.LEFT), @@ -754,6 +758,7 @@ def test_aspirate_in_place( pipetteId="abc123", volume=12.34, flowRate=7.8, + correctionVolume=None, ) ), mock_protocol_core.set_last_location(location=location, mount=Mount.LEFT), @@ -896,6 +901,7 @@ def test_dispense_to_well( rate=5.6, flow_rate=6.0, in_place=False, + correction_volume=321, push_out=7, ) @@ -919,6 +925,7 @@ def test_dispense_to_well( ), volume=12.34, flowRate=6.0, + correctionVolume=321, pushOut=7, ) ), @@ -948,7 +955,11 @@ def test_dispense_in_place( decoy.verify( mock_engine_client.execute_command( cmd.DispenseInPlaceParams( - pipetteId="abc123", volume=12.34, flowRate=7.8, pushOut=None + pipetteId="abc123", + volume=12.34, + correctionVolume=None, + flowRate=7.8, + pushOut=None, ) ), ) @@ -985,7 +996,11 @@ def test_dispense_to_coordinates( ), mock_engine_client.execute_command( cmd.DispenseInPlaceParams( - pipetteId="abc123", volume=12.34, flowRate=7.8, pushOut=None + pipetteId="abc123", + volume=12.34, + correctionVolume=None, + flowRate=7.8, + pushOut=None, ) ), ) @@ -1023,7 +1038,11 @@ def test_dispense_conditionally_clamps_volume( decoy.verify( mock_engine_client.execute_command( cmd.DispenseInPlaceParams( - pipetteId="abc123", volume=111.111, flowRate=7.8, pushOut=None + pipetteId="abc123", + volume=111.111, + correctionVolume=None, + flowRate=7.8, + pushOut=None, ) ), ) @@ -1034,6 +1053,7 @@ def test_dispense_conditionally_clamps_volume( pipetteId="abc123", volume=99999999.99999999, flowRate=7.8, + correctionVolume=None, pushOut=None, ) ), diff --git a/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py b/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py index 87870468590..4dadf5b503b 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py @@ -75,6 +75,9 @@ def test_submerge( air_gap_removal_flow_rate = ( sample_transfer_props.dispense.flow_rate_by_volume.get_for_volume(123) ) + air_gap_correction_vol = ( + sample_transfer_props.dispense.correction_by_volume.get_for_volume(123) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, @@ -109,6 +112,7 @@ def test_submerge( in_place=True, is_meniscus=None, push_out=0, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.5), mock_instrument_core.move_to( @@ -132,7 +136,9 @@ def test_aspirate_and_wait( aspirate_flow_rate = ( sample_transfer_props.aspirate.flow_rate_by_volume.get_for_volume(10) ) - + correction_volume = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume(10) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -151,6 +157,7 @@ def test_aspirate_and_wait( flow_rate=aspirate_flow_rate, in_place=True, is_meniscus=None, + correction_volume=correction_volume, ), mock_instrument_core.delay(0.2), ) @@ -190,7 +197,9 @@ def test_dispense_and_wait( dispense_flow_rate = ( sample_transfer_props.dispense.flow_rate_by_volume.get_for_volume(10) ) - + correction_volume = ( + sample_transfer_props.dispense.correction_by_volume.get_for_volume(50) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -210,6 +219,7 @@ def test_dispense_and_wait( in_place=True, push_out=123, is_meniscus=None, + correction_volume=correction_volume, ), mock_instrument_core.delay(0.5), ) @@ -252,6 +262,12 @@ def test_mix( dispense_flow_rate = ( sample_transfer_props.dispense.flow_rate_by_volume.get_for_volume(50) ) + aspirate_correction_volume = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume(50) + ) + dispense_correction_volume = ( + sample_transfer_props.dispense.correction_by_volume.get_for_volume(50) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -274,6 +290,7 @@ def test_mix( flow_rate=aspirate_flow_rate, in_place=True, is_meniscus=None, + correction_volume=aspirate_correction_volume, ), mock_instrument_core.delay(0.2), mock_instrument_core.dispense( @@ -285,6 +302,7 @@ def test_mix( in_place=True, push_out=2.0, is_meniscus=None, + correction_volume=dispense_correction_volume, ), mock_instrument_core.delay(0.5), ) @@ -301,6 +319,9 @@ def test_mix_disabled( aspirate_flow_rate = ( sample_transfer_props.aspirate.flow_rate_by_volume.get_for_volume(50) ) + correction_volume = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume(50) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -322,6 +343,7 @@ def test_mix_disabled( flow_rate=aspirate_flow_rate, in_place=True, is_meniscus=None, + correction_volume=correction_volume, ), times=0, ) @@ -340,6 +362,12 @@ def test_pre_wet( dispense_flow_rate = ( sample_transfer_props.dispense.flow_rate_by_volume.get_for_volume(40) ) + aspirate_correction_volume = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume(50) + ) + dispense_correction_volume = ( + sample_transfer_props.dispense.correction_by_volume.get_for_volume(50) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -359,6 +387,7 @@ def test_pre_wet( flow_rate=aspirate_flow_rate, in_place=True, is_meniscus=None, + correction_volume=aspirate_correction_volume, ), mock_instrument_core.delay(0.2), mock_instrument_core.dispense( @@ -370,6 +399,7 @@ def test_pre_wet( in_place=True, push_out=0, is_meniscus=None, + correction_volume=dispense_correction_volume, ), mock_instrument_core.delay(0.5), ) @@ -386,6 +416,9 @@ def test_pre_wet_disabled( aspirate_flow_rate = ( sample_transfer_props.aspirate.flow_rate_by_volume.get_for_volume(40) ) + aspirate_correction_volume = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume(50) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -405,6 +438,7 @@ def test_pre_wet_disabled( flow_rate=aspirate_flow_rate, in_place=True, is_meniscus=None, + correction_volume=aspirate_correction_volume, ), times=0, ) @@ -423,6 +457,11 @@ def test_retract_after_aspiration( air_gap_volume = ( sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(40) ) + air_gap_correction_vol = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume( + air_gap_volume + ) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, @@ -464,6 +503,7 @@ def test_retract_after_aspiration( mock_instrument_core.air_gap_in_place( volume=air_gap_volume, flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), ) @@ -485,7 +525,11 @@ def test_retract_after_aspiration_without_touch_tip_and_delay( air_gap_volume = ( sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(40) ) - + air_gap_correction_vol = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume( + air_gap_volume + ) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -516,6 +560,7 @@ def test_retract_after_aspiration_without_touch_tip_and_delay( mock_instrument_core.air_gap_in_place( volume=air_gap_volume, flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), ) @@ -570,6 +615,11 @@ def test_retract_after_dispense_with_blowout_in_source( air_gap_volume = ( sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(0) ) + air_gap_correction_vol = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume( + air_gap_volume + ) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -612,7 +662,9 @@ def test_retract_after_dispense_with_blowout_in_source( speed=None, ), mock_instrument_core.air_gap_in_place( - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), mock_instrument_core.set_flow_rate(blow_out=100), @@ -641,7 +693,9 @@ def test_retract_after_dispense_with_blowout_in_source( add_final_air_gap and [ mock_instrument_core.air_gap_in_place( # type: ignore[func-returns-value] - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), # type: ignore[func-returns-value] ] @@ -668,6 +722,11 @@ def test_retract_after_dispense_with_blowout_in_destination( air_gap_volume = ( sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(0) ) + air_gap_correction_vol = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume( + air_gap_volume + ) + ) sample_transfer_props.dispense.retract.blowout.location = ( BlowoutLocation.DESTINATION ) @@ -729,7 +788,9 @@ def test_retract_after_dispense_with_blowout_in_destination( add_final_air_gap and [ mock_instrument_core.air_gap_in_place( # type: ignore[func-returns-value] - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), # type: ignore[func-returns-value] ] @@ -760,6 +821,11 @@ def test_retract_after_dispense_with_blowout_in_trash_well( air_gap_volume = ( sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(0) ) + air_gap_correction_vol = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume( + air_gap_volume + ) + ) sample_transfer_props.dispense.retract.blowout.location = BlowoutLocation.TRASH subject = TransferComponentsExecutor( @@ -804,7 +870,9 @@ def test_retract_after_dispense_with_blowout_in_trash_well( speed=None, ), mock_instrument_core.air_gap_in_place( - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), mock_instrument_core.set_flow_rate(blow_out=100), @@ -832,7 +900,9 @@ def test_retract_after_dispense_with_blowout_in_trash_well( add_final_air_gap and [ mock_instrument_core.air_gap_in_place( # type: ignore[func-returns-value] - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), # type: ignore[func-returns-value] ] @@ -861,6 +931,11 @@ def test_retract_after_dispense_with_blowout_in_disposal_location( air_gap_volume = ( sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(0) ) + air_gap_correction_vol = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume( + air_gap_volume + ) + ) sample_transfer_props.dispense.retract.blowout.location = BlowoutLocation.TRASH subject = TransferComponentsExecutor( @@ -904,7 +979,9 @@ def test_retract_after_dispense_with_blowout_in_disposal_location( speed=None, ), mock_instrument_core.air_gap_in_place( - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), mock_instrument_core.set_flow_rate(blow_out=100), @@ -917,7 +994,9 @@ def test_retract_after_dispense_with_blowout_in_disposal_location( add_final_air_gap and [ mock_instrument_core.air_gap_in_place( # type: ignore[func-returns-value] - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), # type: ignore[func-returns-value] ] diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 2a279ca1ad7..7fad487086a 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -2247,3 +2247,265 @@ def test_distribute_liquid_delegates_to_engine_core( trash_location=trash_location.move(Point(1, 2, 3)), ) ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_raises_for_invalid_locations( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if source or destination is invalid.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([[mock_well]]) + ).then_raise(ValueError("Oh no")) + with pytest.raises(ValueError): + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[[mock_well]], + dest=mock_well, + ) + with pytest.raises(ValueError, match="Destination should be a single Well"): + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest="abc", # type: ignore + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_raises_if_more_than_one_destination( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise error if destination is more than one well.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + with pytest.raises(ValueError, match="Destination should be a single Well"): + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well, mock_well], + dest=[mock_well, mock_well], # type: ignore + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_raises_for_non_liquid_handling_locations( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if sources or destination are not a valid liquid handling target.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when( + mock_instrument_support.validate_takes_liquid( + mock_well.top(), reject_module=True, reject_adapter=True + ) + ).then_raise(ValueError("Uh oh")) + with pytest.raises(ValueError, match="Uh oh"): + subject.consolidate_liquid( + liquid_class=test_liq_class, volume=10, source=[mock_well], dest=mock_well + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_raises_for_bad_tip_policy( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if new_tip is invalid.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("once")).then_raise( + ValueError("Uh oh") + ) + with pytest.raises(ValueError, match="Uh oh"): + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=mock_well, + new_tip="once", + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_raises_for_no_tip( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if there is no tip attached.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("never")).then_return( + TransferTipPolicyV2.NEVER + ) + with pytest.raises(RuntimeError, match="Pipette has no tip"): + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=mock_well, + new_tip="never", + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_raises_if_tip_has_liquid( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if there is liquid in the tip.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + tip_racks = [decoy.mock(cls=Labware)] + + subject.starting_tip = None + subject.tip_racks = tip_racks + + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("never")).then_return( + TransferTipPolicyV2.ONCE + ) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) + decoy.when(mock_instrument_core.get_active_channels()).then_return(2) + decoy.when( + labware.next_available_tip( + starting_tip=None, + tip_racks=tip_racks, + channels=2, + nozzle_map=MOCK_MAP, + ) + ).then_return((decoy.mock(cls=Labware), decoy.mock(cls=Well))) + decoy.when(mock_instrument_core.get_current_volume()).then_return(1000) + with pytest.raises(RuntimeError, match="liquid already in the tip"): + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=mock_well, + new_tip="never", + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_delegates_to_engine_core( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should delegate the execution to core.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + tip_racks = [decoy.mock(cls=Labware)] + trash_location = Location(point=Point(1, 2, 3), labware=mock_well) + next_tiprack = decoy.mock(cls=Labware) + subject.starting_tip = None + subject._tip_racks = tip_racks + + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("never")).then_return( + TransferTipPolicyV2.ONCE + ) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) + decoy.when(mock_instrument_core.get_active_channels()).then_return(2) + decoy.when(mock_instrument_core.get_current_volume()).then_return(0) + decoy.when( + mock_validation.ensure_valid_trash_location_for_transfer_v2(trash_location) + ).then_return(trash_location.move(Point(1, 2, 3))) + decoy.when(next_tiprack.uri).then_return("tiprack-uri") + decoy.when(mock_instrument_core.get_pipette_name()).then_return("pipette-name") + + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=mock_well, + new_tip="never", + trash_location=trash_location, + ) + decoy.verify( + mock_instrument_core.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[(Location(Point(), labware=mock_well), mock_well._core)], + dest=(Location(Point(), labware=mock_well), mock_well._core), + new_tip=TransferTipPolicyV2.ONCE, + tip_racks=[(Location(Point(), labware=tip_racks[0]), tip_racks[0]._core)], + trash_location=trash_location.move(Point(1, 2, 3)), + ) + ) diff --git a/api/tests/opentrons/protocol_api_old/core/simulator/test_instrument_context.py b/api/tests/opentrons/protocol_api_old/core/simulator/test_instrument_context.py index c9bd57c0997..42e3f7aab67 100644 --- a/api/tests/opentrons/protocol_api_old/core/simulator/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api_old/core/simulator/test_instrument_context.py @@ -59,6 +59,7 @@ def test_dispense_no_tip(subject: InstrumentCore) -> None: location=location, well_core=None, in_place=False, + correction_volume=0, push_out=None, ) @@ -106,6 +107,7 @@ def test_pick_up_tip_prep_after( rate=1, flow_rate=1, in_place=False, + correction_volume=0, ) subject.dispense( volume=1, @@ -114,6 +116,7 @@ def test_pick_up_tip_prep_after( location=Location(point=Point(2, 2, 3), labware=None), well_core=labware.get_well_core("A2"), in_place=False, + correction_volume=0, push_out=None, ) @@ -134,6 +137,7 @@ def test_pick_up_tip_prep_after( rate=1, flow_rate=1, in_place=False, + correction_volume=0, ) subject.dispense( volume=1, @@ -142,6 +146,7 @@ def test_pick_up_tip_prep_after( location=Location(point=Point(2, 2, 3), labware=None), well_core=labware.get_well_core("A2"), in_place=False, + correction_volume=0, push_out=None, ) @@ -173,6 +178,7 @@ def test_aspirate_too_much( rate=1, flow_rate=1, in_place=False, + correction_volume=0, ) @@ -224,6 +230,7 @@ def _aspirate(i: InstrumentCore, labware: LabwareCore) -> None: rate=10, flow_rate=10, in_place=False, + correction_volume=0, ) @@ -237,6 +244,7 @@ def _aspirate_dispense(i: InstrumentCore, labware: LabwareCore) -> None: rate=10, flow_rate=10, in_place=False, + correction_volume=0, ) i.dispense( volume=2, @@ -245,6 +253,7 @@ def _aspirate_dispense(i: InstrumentCore, labware: LabwareCore) -> None: location=Location(point=Point(2, 2, 3), labware=None), well_core=labware.get_well_core("A2"), in_place=False, + correction_volume=0, push_out=None, ) @@ -259,6 +268,7 @@ def _aspirate_blowout(i: InstrumentCore, labware: LabwareCore) -> None: rate=13, flow_rate=13, in_place=False, + correction_volume=0, ) i.blow_out( location=Location(point=Point(1, 2, 3), labware=None), diff --git a/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py index b9d110fd9c2..776912b4b18 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py @@ -101,6 +101,7 @@ async def test_air_gap_in_place_implementation( pipetteId="pipette-id-abc", volume=123, flowRate=1.234, + correctionVolume=321, ) decoy.when( @@ -115,6 +116,7 @@ async def test_air_gap_in_place_implementation( volume=123, flow_rate=1.234, command_note_adder=mock_command_note_adder, + correction_volume=321, ) ).then_return(123) @@ -194,6 +196,7 @@ async def test_aspirate_raises_volume_error( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0, ) ).then_raise(AssertionError("blah blah")) @@ -253,6 +256,7 @@ async def test_overpressure_error( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0, ), ).then_raise(PipetteOverpressureError()) diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py index 4a8adbcdc76..ff09f9a966d 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py @@ -126,6 +126,7 @@ async def test_aspirate_implementation_no_prep( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0.0, ), ).then_return(50) @@ -236,6 +237,7 @@ async def test_aspirate_implementation_with_prep( volume=volume, flow_rate=flow_rate, command_note_adder=mock_command_note_adder, + correction_volume=0.0, ), ).then_return(volume) @@ -326,6 +328,7 @@ async def test_aspirate_raises_volume_error( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0.0, ) ).then_raise(AssertionError("blah blah")) @@ -402,6 +405,7 @@ async def test_overpressure_error( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0.0, ), ).then_raise(PipetteOverpressureError()) @@ -502,6 +506,7 @@ async def test_aspirate_implementation_meniscus( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0, ), ).then_return(50) diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py index 5a7ca3ee940..464f8e04a82 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py @@ -129,6 +129,7 @@ async def test_aspirate_in_place_implementation( volume=123, flow_rate=1.234, command_note_adder=mock_command_note_adder, + correction_volume=0.0, ) ).then_return(123) @@ -221,6 +222,7 @@ async def test_aspirate_raises_volume_error( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0, ) ).then_raise(AssertionError("blah blah")) @@ -292,6 +294,7 @@ async def test_overpressure_error( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0, ), ).then_raise(PipetteOverpressureError()) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_dispense.py index 5b60b61d4df..8fe72afe757 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense.py @@ -99,7 +99,11 @@ async def test_dispense_implementation( decoy.when( await pipetting.dispense_in_place( - pipette_id="pipette-id-abc123", volume=50, flow_rate=1.23, push_out=None + pipette_id="pipette-id-abc123", + volume=50, + flow_rate=1.23, + push_out=None, + correction_volume=0, ) ).then_return(42) decoy.when( @@ -193,7 +197,11 @@ async def test_overpressure_error( decoy.when( await pipetting.dispense_in_place( - pipette_id=pipette_id, volume=50, flow_rate=1.23, push_out=None + pipette_id=pipette_id, + volume=50, + flow_rate=1.23, + push_out=None, + correction_volume=0, ), ).then_raise(PipetteOverpressureError()) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py index e9c715223de..15b239e8d46 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py @@ -78,7 +78,11 @@ async def test_dispense_in_place_implementation( decoy.when( await pipetting.dispense_in_place( - pipette_id="pipette-id-abc", volume=123, flow_rate=456, push_out=None + pipette_id="pipette-id-abc", + volume=123, + flow_rate=456, + push_out=None, + correction_volume=0, ) ).then_return(42) @@ -195,6 +199,7 @@ async def test_overpressure_error( volume=50, flow_rate=1.23, push_out=10, + correction_volume=0, ), ).then_raise(PipetteOverpressureError()) diff --git a/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py b/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py index 84a425b88fc..f39853cb894 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py @@ -171,7 +171,9 @@ async def test_hw_dispense_in_place( mock_hardware_api.set_flow_rate( mount=Mount.RIGHT, aspirate=None, dispense=2.5, blow_out=None ), - await mock_hardware_api.dispense(mount=Mount.RIGHT, volume=25, push_out=None), + await mock_hardware_api.dispense( + mount=Mount.RIGHT, volume=25, push_out=None, correction_volume=0 + ), mock_hardware_api.set_flow_rate( mount=Mount.RIGHT, aspirate=1.23, dispense=4.56, blow_out=7.89 ), @@ -263,7 +265,9 @@ async def test_hw_aspirate_in_place( mock_hardware_api.set_flow_rate( mount=Mount.LEFT, aspirate=2.5, dispense=None, blow_out=None ), - await mock_hardware_api.aspirate(mount=Mount.LEFT, volume=25), + await mock_hardware_api.aspirate( + mount=Mount.LEFT, volume=25, correction_volume=0 + ), mock_hardware_api.set_flow_rate( mount=Mount.LEFT, aspirate=1.23, dispense=4.56, blow_out=7.89 ), diff --git a/protocol-designer/cypress/support/SupportModules.ts b/protocol-designer/cypress/support/SupportModules.ts index 427b608d9ab..0fc0262a092 100644 --- a/protocol-designer/cypress/support/SupportModules.ts +++ b/protocol-designer/cypress/support/SupportModules.ts @@ -46,7 +46,7 @@ export const executeModSteps = (action: ModActions): void => { .click({ force: true }) break case ModActions.AddTemperatureStep: - cy.contains('button', 'Temperature').click() + cy.contains('button', 'Temperature').click({ force: true }) break case ModActions.ActivateTempdeck: cy.contains(ModContent.DecativeTempDeck) @@ -72,7 +72,7 @@ export const executeModSteps = (action: ModActions): void => { .click() break case ModActions.SaveButtonTempdeck: - cy.contains(ModContent.Save).click() + cy.contains(ModContent.Save).click({ force: true }) break default: throw new Error(`Unrecognized action: ${action as string}`) diff --git a/protocol-designer/src/assets/images/opentrons_absorbance_plate_reader.png b/protocol-designer/src/assets/images/opentrons_absorbance_plate_reader.png new file mode 100644 index 00000000000..5ac78216aa3 Binary files /dev/null and b/protocol-designer/src/assets/images/opentrons_absorbance_plate_reader.png differ diff --git a/protocol-designer/src/assets/localization/en/application.json b/protocol-designer/src/assets/localization/en/application.json index b6d0505a0d1..256a2fa60db 100644 --- a/protocol-designer/src/assets/localization/en/application.json +++ b/protocol-designer/src/assets/localization/en/application.json @@ -1,5 +1,6 @@ { "are_you_sure": "Are you sure you want to remove liquids from all selected wells?", + "are_you_sure_clear_all_wells": "Are you sure you want to remove liquids from all wells?", "are_you_sure_delete_well": "Are you sure you want to delete well {{well}}?", "blowout": "Blowout", "cancel": "cancel", diff --git a/protocol-designer/src/assets/localization/en/modal.json b/protocol-designer/src/assets/localization/en/modal.json index 8530f61a28a..62d34d00a9d 100644 --- a/protocol-designer/src/assets/localization/en/modal.json +++ b/protocol-designer/src/assets/localization/en/modal.json @@ -57,6 +57,16 @@ "body3": "Add multiple Heater-Shaker Modules and Magnetic Blocks to the deck (Flex only).", "body4": "All protocols now require Opentrons App version 8.2.0+ to run.", "body5": "For more information, see the Protocol Designer Instruction Manual." + }, + "absorbancePlateReaderSupport": { + "heading": "{{version}} Release Notes", + "body1": "Welcome to Protocol Designer {{version}}!", + "body2": "This release includes the following improvements:", + "body3": "Protocol Designer now supports the Absorbance Plate Reader Module", + "body4": "Bug fix for mismatched x- and y-offset values for aspirate and dispense during Mix steps", + "body5": "Bug fix for Move steps not using the gripper by default", + "body6": "All protocols now require Opentrons App version 8.2.0+ to run.", + "body7": "For more information, see the Protocol Designer Instruction Manual." } }, "labware_selection": { diff --git a/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx b/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx index e84930a26c0..2a745cd7d9a 100644 --- a/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx +++ b/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx @@ -9,6 +9,7 @@ import { Link as LinkComponent, SPACING, StyledText, + TEXT_DECORATION_UNDERLINE, } from '@opentrons/components' import magTempCombined from '../../assets/images/modules/magdeck_tempdeck_combined.png' @@ -19,7 +20,8 @@ import heaterShaker from '../../assets/images/modules/heatershaker.png' import thermocyclerGen2 from '../../assets/images/modules/thermocycler_gen2.png' import liquidEnhancements from '../../assets/images/announcements/liquid-enhancements.gif' import opentronsFlex from '../../assets/images/OpentronsFlex.png' -import deckConfigutation from '../../assets/images/deck_configuration.png' +import deckConfiguration from '../../assets/images/deck_configuration.png' +import absorbancePlateReaderImage from '../../assets/images/opentrons_absorbance_plate_reader.png' import { DOC_URL } from '../KnowledgeLink' import type { ReactNode } from 'react' @@ -53,6 +55,8 @@ const batchEditStyles = css` const PD = 'Protocol Designer' const APP = 'Opentrons App' const OPENTRONS_PD = 'Opentrons Protocol Designer' +const OPENTRONS_ABSORBANCE_READER_URL = + 'https://opentrons.com/products/opentrons-flex-absorbance-plate-reader-module-gen1' export const useAnnouncements = (): Announcement[] => { const { t } = useTranslation('modal') @@ -277,7 +281,7 @@ export const useAnnouncements = (): Announcement[] => { announcementKey: 'deckConfigAnd96Channel8.0', image: ( - + ), heading: t('announcements.header', { pd: PD }), @@ -372,5 +376,85 @@ export const useAnnouncements = (): Announcement[] => { ), }, + { + announcementKey: 'absorbancePlateReader', + image: ( + + + + ), + heading: t('announcements.absorbancePlateReaderSupport.heading', { + version: pdVersion, + }), + message: ( + + + {t('announcements.absorbancePlateReaderSupport.body1', { + version: pdVersion, + })} + + + + {t('announcements.absorbancePlateReaderSupport.body2')} + + +
    +
  • + + + ), + }} + i18nKey="announcements.absorbancePlateReaderSupport.body3" + /> + +
  • +
  • + + {t('announcements.absorbancePlateReaderSupport.body4')} + +
  • +
  • + + {t('announcements.absorbancePlateReaderSupport.body5')} + +
  • +
+
+
+ + {t('announcements.absorbancePlateReaderSupport.body6')} + + + + ), + }} + i18nKey="announcements.absorbancePlateReaderSupport.body7" + /> + +
+ ), + }, ] } diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx index 28c032f2373..1606e7a25da 100644 --- a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx +++ b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx @@ -89,6 +89,17 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { wellContentsSelectors.getAllWellContentsForActiveItem ) + const allWellsForActiveItem = + labwareId != null + ? Object.keys(allWellContentsForActiveItem?.[labwareId] ?? {}) + : [] + const activeItemHasLiquids = + labwareId != null + ? Object.values(allWellContentsForActiveItem?.[labwareId] ?? {}).some( + value => value.groupIds.length > 0 + ) + : false + const selectionHasLiquids = Boolean( labwareId != null && liquidLocations[labwareId] != null && @@ -143,6 +154,21 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { reset() } + const handleClearAllWells: () => void = () => { + if (labwareId != null && activeItemHasLiquids) { + if ( + global.confirm(t('application:are_you_sure_clear_all_wells') as string) + ) { + dispatch( + removeWellsContents({ + labwareId, + wells: allWellsForActiveItem, + }) + ) + } + } + } + const handleChangeVolume: (e: ChangeEvent) => void = e => { const value: string | null | undefined = e.currentTarget.value const masked = fieldProcessors.composeMaskers( @@ -247,7 +273,7 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { dispatch(deselectAllWells()) onClose() }} - onCloseClick={handleClearSelectedWells} + onCloseClick={handleClearAllWells} height="100%" width="21.875rem" closeButton={ @@ -255,9 +281,6 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { {t('clear_wells')} } - disableCloseButton={ - !(labwareId != null && selectedWells != null && selectionHasLiquids) - } > {(liquidsInLabware != null && liquidsInLabware.length > 0) || selectedWells.length > 0 ? ( diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx index 400da9a6235..19eb12cb2db 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx @@ -70,6 +70,15 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { subHeader={t('fixtures_replace')} disabled={!hasTrash} goBack={() => { + // Note this is avoid the following case issue. + // https://github.com/Opentrons/opentrons/pull/17344#pullrequestreview-2576591908 + setValue( + 'additionalEquipment', + additionalEquipment.filter( + ae => ae === 'gripper' || ae === 'trashBin' + ) + ) + goBack(1) }} proceed={handleProceed} @@ -135,11 +144,18 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { filterOptions: getNumOptions( numSlotsAvailable >= MAX_SLOTS ? MAX_SLOTS - : numSlotsAvailable + numStagingAreas + : numSlotsAvailable ), onClick: (value: string) => { const inputNum = parseInt(value) - let updatedStagingAreas = [...additionalEquipment] + const currentStagingAreas = additionalEquipment.filter( + additional => additional === 'stagingArea' + ) + const otherEquipment = additionalEquipment.filter( + additional => additional !== 'stagingArea' + ) + let updatedStagingAreas = currentStagingAreas + // let updatedStagingAreas = [...additionalEquipment] if (inputNum > numStagingAreas) { const difference = inputNum - numStagingAreas @@ -148,13 +164,16 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { ...Array(difference).fill(ae), ] } else { - updatedStagingAreas = updatedStagingAreas.slice( + updatedStagingAreas = currentStagingAreas.slice( 0, inputNum ) } - setValue('additionalEquipment', updatedStagingAreas) + setValue('additionalEquipment', [ + ...otherEquipment, + ...updatedStagingAreas, + ]) }, } return ( diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index 6aa5044c2a4..717a6b2b762 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -173,22 +173,24 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { ) : null} - {filteredSupportedModules.map(moduleModel => { - const numSlotsAvailable = getNumSlotsAvailable( - modules, - additionalEquipment, - moduleModel - ) - return ( - 0} - hasGripper={hasGripper} - handleAddModule={handleAddModule} - /> - ) - })} + {filteredSupportedModules + .sort((moduleA, moduleB) => moduleA.localeCompare(moduleB)) + .map(moduleModel => { + const numSlotsAvailable = getNumSlotsAvailable( + modules, + additionalEquipment, + moduleModel + ) + return ( + 0} + hasGripper={hasGripper} + handleAddModule={handleAddModule} + /> + ) + })} {modules != null && Object.keys(modules).length > 0 ? ( {Object.entries(modules) + .sort(([, moduleA], [, moduleB]) => + moduleA.model.localeCompare(moduleB.model) + ) .reduce>( (acc, [key, module]) => { const existingModule = acc.find( @@ -243,7 +248,9 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { }, dropdownType: 'neutral' as DropdownBorder, filterOptions: getNumOptions( - numSlotsAvailable + module.count + module.model !== ABSORBANCE_READER_V1 + ? numSlotsAvailable + module.count + : numSlotsAvailable ), } return ( diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts index bda3d71da18..a167217bf25 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts @@ -1,14 +1,19 @@ import { it, describe, expect } from 'vitest' import { FLEX_ROBOT_TYPE, + ABSORBANCE_READER_V1, + ABSORBANCE_READER_TYPE, HEATERSHAKER_MODULE_TYPE, HEATERSHAKER_MODULE_V1, MAGNETIC_BLOCK_TYPE, MAGNETIC_BLOCK_V1, + MAGNETIC_MODULE_V1, + MAGNETIC_MODULE_V2, TEMPERATURE_MODULE_TYPE, TEMPERATURE_MODULE_V1, TEMPERATURE_MODULE_V2, THERMOCYCLER_MODULE_TYPE, + THERMOCYCLER_MODULE_V1, THERMOCYCLER_MODULE_V2, } from '@opentrons/shared-data' import { getNumSlotsAvailable, getTrashSlot } from '../utils' @@ -36,18 +41,60 @@ describe('getNumSlotsAvailable', () => { const result = getNumSlotsAvailable(null, [], 'gripper') expect(result).toBe(0) }) - it('should return 1 for a non MoaM module', () => { + + it('should return 1 for a non MoaM module - temperature module', () => { const result = getNumSlotsAvailable(null, [], TEMPERATURE_MODULE_V1) expect(result).toBe(1) }) + + it('should return 1 for a non MoaM module - absorbance plate reader', () => { + const result = getNumSlotsAvailable(null, [], ABSORBANCE_READER_V1) + expect(result).toBe(1) + }) + + it('should return 1 for a non MoaM module - thermocycler v1', () => { + const result = getNumSlotsAvailable(null, [], THERMOCYCLER_MODULE_V1) + expect(result).toBe(1) + }) + + it('should return 1 for a non MoaM module - magnetic module v1', () => { + const result = getNumSlotsAvailable(null, [], MAGNETIC_MODULE_V1) + expect(result).toBe(1) + }) + + it('should return 1 for a non MoaM module - magnetic module v2', () => { + const result = getNumSlotsAvailable(null, [], MAGNETIC_MODULE_V2) + expect(result).toBe(1) + }) + it('should return 2 for a thermocycler', () => { const result = getNumSlotsAvailable(null, [], THERMOCYCLER_MODULE_V2) expect(result).toBe(2) }) + it('should return 8 when there are no modules or additional equipment for a heater-shaker', () => { const result = getNumSlotsAvailable(null, [], HEATERSHAKER_MODULE_V1) expect(result).toBe(8) }) + + it('should return 3 when there a plate reader', () => { + const mockModules = { + 0: { + model: ABSORBANCE_READER_V1, + type: ABSORBANCE_READER_TYPE, + slot: 'B3', + }, + } + const mockAdditionalEquipment: AdditionalEquipment[] = ['trashBin'] + const result = getNumSlotsAvailable( + mockModules, + mockAdditionalEquipment, + 'stagingArea' + ) + // Note: the return value is 3 because trashBin can be placed slot1 and plate reader is on B3 + expect(result).toBe(3) + }) + it('should return 0 when there is a TC and 7 modules for a temperature module v2', () => { const mockModules = { 0: { @@ -90,6 +137,7 @@ describe('getNumSlotsAvailable', () => { const result = getNumSlotsAvailable(mockModules, [], TEMPERATURE_MODULE_V2) expect(result).toBe(0) }) + it('should return 1 when there are 9 additional equipment and 1 is a waste chute on the staging area and one is a gripper for a heater-shaker', () => { const mockAdditionalEquipment: AdditionalEquipment[] = [ 'trashBin', @@ -109,6 +157,7 @@ describe('getNumSlotsAvailable', () => { ) expect(result).toBe(1) }) + it('should return 1 when there is a full deck but one staging area for waste chute', () => { const mockModules = { 0: { @@ -148,6 +197,7 @@ describe('getNumSlotsAvailable', () => { ) expect(result).toBe(1) }) + it('should return 1 when there are 7 modules (with one magnetic block) and one trash for staging area', () => { const mockModules = { 0: { @@ -187,8 +237,10 @@ describe('getNumSlotsAvailable', () => { mockAdditionalEquipment, 'stagingArea' ) - expect(result).toBe(1) + // Note: the return value is 2 because trashBin can be placed slot1 + expect(result).toBe(2) }) + it('should return 1 when there are 8 modules with 2 magnetic blocks and one trash for staging area', () => { const mockModules = { 0: { @@ -233,7 +285,7 @@ describe('getNumSlotsAvailable', () => { mockAdditionalEquipment, 'stagingArea' ) - expect(result).toBe(1) + expect(result).toBe(2) }) it('should return 4 when there are 12 magnetic blocks for staging area', () => { const mockModules = { diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts b/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts index 6e762e48f0a..5e1179c58f4 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts @@ -127,7 +127,7 @@ export const DEFAULT_SLOT_MAP_FLEX: { [HEATERSHAKER_MODULE_V1]: 'D1', [MAGNETIC_BLOCK_V1]: 'D2', [TEMPERATURE_MODULE_V2]: 'C1', - [ABSORBANCE_READER_V1]: 'D3', + [ABSORBANCE_READER_V1]: 'B3', } export const DEFAULT_SLOT_MAP_OT2: { [moduleType in ModuleType]?: string } = { diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx index d3451605756..2e29273b9ac 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx @@ -12,6 +12,7 @@ import { useNavigate } from 'react-router-dom' import { FLEX_ROBOT_TYPE, getAreSlotsAdjacent, + ABSORBANCE_READER_MODELS, HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE, MAGNETIC_MODULE_TYPE, @@ -283,11 +284,20 @@ export function CreateNewProtocolWizard(): JSX.Element | null { const stagingAreas = values.additionalEquipment.filter( equipment => equipment === 'stagingArea' ) + if (stagingAreas.length > 0) { + // Note: when plate reader is present, cutoutB3 is not available for StagingArea + const hasPlateReader = modules.some( + module => module.model === ABSORBANCE_READER_MODELS[0] + ) stagingAreas.forEach((_, index) => { - return dispatch( - createDeckFixture('stagingArea', STAGING_AREA_CUTOUTS_ORDERED[index]) - ) + const stagingAreaCutout = hasPlateReader + ? STAGING_AREA_CUTOUTS_ORDERED.filter( + cutout => cutout !== 'cutoutB3' + )[index] + : STAGING_AREA_CUTOUTS_ORDERED[index] + + return dispatch(createDeckFixture('stagingArea', stagingAreaCutout)) }) } diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx index 3ffb60d10f2..69abb7e6ae7 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx @@ -30,10 +30,11 @@ import type { import type { DropdownOption } from '@opentrons/components' import type { AdditionalEquipment, WizardFormState } from './types' -const TOTAL_OUTER_SLOTS = 8 -const MIDDLE_SLOT_NUM = 4 -const MAX_MAGNETIC_BLOCK_SLOTS = 12 -const TOTAL_LEFT_SLOTS = 4 +const NUM_SLOTS_OUTER = 8 +const NUM_SLOTS_MIDDLE = 4 +const NUM_SLOTS_COLUMN3 = 4 +const NUM_SLOTS_MAGNETIC_BLOCK = 12 + export const getNumOptions = (length: number): DropdownOption[] => { return Array.from({ length }, (_, i) => ({ name: `${i + 1}`, @@ -66,12 +67,12 @@ export const getNumSlotsAvailable = ( const magneticBlockCount = magneticBlocks.length const moduleCount = modules != null ? Object.keys(modules).length : 0 let filteredModuleLength = moduleCount - if (magneticBlockCount <= MIDDLE_SLOT_NUM) { + if (magneticBlockCount <= NUM_SLOTS_MIDDLE) { // Subtract magnetic blocks directly if their count is ≤ 4 filteredModuleLength -= magneticBlockCount } else { // Subtract the excess magnetic blocks beyond 4 - const extraMagneticBlocks = magneticBlockCount - MIDDLE_SLOT_NUM + const extraMagneticBlocks = magneticBlockCount - NUM_SLOTS_MIDDLE filteredModuleLength -= extraMagneticBlocks } if (hasTC) { @@ -86,11 +87,9 @@ export const getNumSlotsAvailable = ( case 'gripper': { return 0 } - // TODO: wire up absorbance reader - case ABSORBANCE_READER_V1: { - return 1 - } + // these modules don't support MoaM + case ABSORBANCE_READER_V1: case THERMOCYCLER_MODULE_V1: case TEMPERATURE_MODULE_V1: case MAGNETIC_MODULE_V1: @@ -105,43 +104,45 @@ export const getNumSlotsAvailable = ( return 2 } } + case 'trashBin': case HEATERSHAKER_MODULE_V1: case TEMPERATURE_MODULE_V2: { return ( - TOTAL_OUTER_SLOTS - + NUM_SLOTS_OUTER - (filteredModuleLength + filteredAdditionalEquipmentLength) ) } case 'stagingArea': { - const lengthMinusMagneticBlock = - moduleCount + (hasTC ? 1 : 0) - magneticBlockCount - let adjustedModuleLength = 0 - if (lengthMinusMagneticBlock > TOTAL_LEFT_SLOTS) { - adjustedModuleLength = lengthMinusMagneticBlock - TOTAL_LEFT_SLOTS - } - - const occupiedSlots = - adjustedModuleLength + filteredAdditionalEquipmentLength - - return TOTAL_LEFT_SLOTS - occupiedSlots + const modulesWithColumn3 = + modules !== null + ? Object.values(modules).filter(module => module.slot?.includes('3')) + .length + : 0 + const fixtureSlotsWithColumn3 = + additionalEquipment !== null + ? additionalEquipment.filter(slot => slot.includes('3')).length + : 0 + return NUM_SLOTS_COLUMN3 - modulesWithColumn3 - fixtureSlotsWithColumn3 } + case 'wasteChute': { const adjustmentForStagingArea = numStagingAreas >= 1 ? 1 : 0 return ( - TOTAL_OUTER_SLOTS - + NUM_SLOTS_OUTER - (filteredModuleLength + filteredAdditionalEquipmentLength - adjustmentForStagingArea) ) } + case MAGNETIC_BLOCK_V1: { const filteredAdditionalEquipmentForMagneticBlockLength = additionalEquipment.filter( ae => ae !== 'gripper' && ae !== 'stagingArea' )?.length return ( - MAX_MAGNETIC_BLOCK_SLOTS - + NUM_SLOTS_MAGNETIC_BLOCK - (filteredModuleLength + filteredAdditionalEquipmentForMagneticBlockLength) ) @@ -292,9 +293,21 @@ export const getTrashSlot = (values: WizardFormState): string => { equipment.includes('stagingArea') ) - const cutouts = stagingAreas.map( - (_, index) => STAGING_AREA_CUTOUTS_ORDERED[index] + // when plate reader is present, cutoutB3 is not available for StagingArea + const hasPlateReader = + modules !== null + ? Object.values(modules).some( + module => module.model === ABSORBANCE_READER_V1 + ) + : false + const cutouts = stagingAreas.map((_, index) => + hasPlateReader + ? STAGING_AREA_CUTOUTS_ORDERED.filter(cutout => cutout !== 'cutoutB3')[ + index + ] + : STAGING_AREA_CUTOUTS_ORDERED[index] ) + const hasWasteChute = additionalEquipment.find(equipment => equipment.includes('wasteChute') ) diff --git a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx index 05720f81555..5f219e8ecd1 100644 --- a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx +++ b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx @@ -68,7 +68,7 @@ export function OffDeckDetails(props: OffDeckDetailsProps): JSX.Element { return ( {t(`step_edit_form.field.path.title.${path}`)} - + path animation {subtitle} ) return ( - + {tooltip} { aspirateHelper('A1', 50), aspirateHelper('A2', 50), dispenseHelper('B1', 100), - airGapHelper('B1', 5, { labwareId: 'destPlateId' }), + makeMoveToWellHelper('B1', DEST_LABWARE), + makeAirGapHelper(5), ]) }) @@ -852,24 +843,28 @@ describe('consolidate single-channel', () => { aspirateHelper('A1', 100), ...delayWithOffset('A1', SOURCE_LABWARE), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), delayCommand(12), aspirateHelper('A2', 100), ...delayWithOffset('A2', SOURCE_LABWARE), - airGapHelper('A2', 5), + makeMoveToWellHelper('A2'), + makeAirGapHelper(5), delayCommand(12), dispenseHelper('B1', 210), aspirateHelper('A3', 100), ...delayWithOffset('A3', SOURCE_LABWARE), - airGapHelper('A3', 5), + makeMoveToWellHelper('A3'), + makeAirGapHelper(5), delayCommand(12), aspirateHelper('A4', 100), ...delayWithOffset('A4', SOURCE_LABWARE), - airGapHelper('A4', 5), + makeMoveToWellHelper('A4'), + makeAirGapHelper(5), delayCommand(12), dispenseHelper('B1', 210), @@ -1010,18 +1005,22 @@ describe('consolidate single-channel', () => { pickUpTipHelper('A1'), aspirateHelper('A1', 100), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), aspirateHelper('A2', 100), - airGapHelper('A2', 5), + makeMoveToWellHelper('A2'), + makeAirGapHelper(5), dispenseHelper('B1', 210), aspirateHelper('A3', 100), - airGapHelper('A3', 5), + makeMoveToWellHelper('A3'), + makeAirGapHelper(5), aspirateHelper('A4', 100), - airGapHelper('A4', 5), + makeMoveToWellHelper('A4'), + makeAirGapHelper(5), dispenseHelper('B1', 210), ]) @@ -1044,22 +1043,26 @@ describe('consolidate single-channel', () => { pickUpTipHelper('A1'), aspirateHelper('A1', 150), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseHelper('B1', 155), aspirateHelper('A2', 150), - airGapHelper('A2', 5), + makeMoveToWellHelper('A2'), + makeAirGapHelper(5), dispenseHelper('B1', 155), aspirateHelper('A3', 150), - airGapHelper('A3', 5), + makeMoveToWellHelper('A3'), + makeAirGapHelper(5), dispenseHelper('B1', 155), aspirateHelper('A4', 150), - airGapHelper('A4', 5), + makeMoveToWellHelper('A4'), + makeAirGapHelper(5), dispenseHelper('B1', 155), ]) @@ -1213,12 +1216,10 @@ describe('consolidate single-channel', () => { }, // Air Gap: after aspirating from A1 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { @@ -1229,6 +1230,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -1300,12 +1309,10 @@ describe('consolidate single-channel', () => { }, // Aspirate > air gap: after aspirating from A2 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A2', wellLocation: { @@ -1316,6 +1323,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -1560,12 +1575,10 @@ describe('consolidate single-channel', () => { }, // Aspirate > air gap: after aspirating from A3 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A3', wellLocation: { @@ -1576,6 +1589,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -1703,14 +1724,12 @@ describe('consolidate single-channel', () => { }, // Dispense > air gap in dest well { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.1, wellLocation: { origin: 'bottom', offset: { @@ -1719,7 +1738,15 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', volume: 35, + flowRate: 2.1, }, }, { @@ -1888,12 +1915,10 @@ describe('consolidate single-channel', () => { }, // Air Gap: after aspirating from A1 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { @@ -1904,6 +1929,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -1975,12 +2008,10 @@ describe('consolidate single-channel', () => { }, // Aspirate > air gap: after aspirating from A2 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A2', wellLocation: { @@ -1991,6 +2022,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -2250,12 +2289,10 @@ describe('consolidate single-channel', () => { }, // Aspirate > air gap: after aspirating from A3 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A3', wellLocation: { @@ -2266,6 +2303,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -2408,14 +2453,12 @@ describe('consolidate single-channel', () => { }, // Dispense > air gap in dest well { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.1, wellLocation: { origin: 'bottom', offset: { @@ -2424,7 +2467,15 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', volume: 35, + flowRate: 2.1, }, }, { @@ -2590,12 +2641,10 @@ describe('consolidate single-channel', () => { }, // Air Gap: after aspirating from A1 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { @@ -2606,6 +2655,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -2677,12 +2734,10 @@ describe('consolidate single-channel', () => { }, // Aspirate > air gap: after aspirating from A2 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A2', wellLocation: { @@ -2693,6 +2748,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -2836,8 +2899,7 @@ describe('consolidate single-channel', () => { // Change tip is "always" so we can Dispense > Air Gap here { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -2851,6 +2913,13 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', volume: 35, flowRate: 2.1, }, @@ -2989,12 +3058,10 @@ describe('consolidate single-channel', () => { }, // Aspirate > air gap: after aspirating from A3 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A3', wellLocation: { @@ -3005,6 +3072,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -3147,8 +3222,7 @@ describe('consolidate single-channel', () => { }, // Dispense > air gap in dest well { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -3162,6 +3236,13 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', volume: 35, flowRate: 2.1, }, diff --git a/step-generation/src/__tests__/distribute.test.ts b/step-generation/src/__tests__/distribute.test.ts index b26b24e4d07..12f3676f09a 100644 --- a/step-generation/src/__tests__/distribute.test.ts +++ b/step-generation/src/__tests__/distribute.test.ts @@ -27,6 +27,7 @@ import { pickUpTipHelper, SOURCE_LABWARE, blowoutInPlaceHelper, + makeMoveToWellHelper, } from '../fixtures' import { distribute } from '../commandCreators/compound/distribute' import type { CreateCommand, LabwareDefinition2 } from '@opentrons/shared-data' @@ -36,17 +37,6 @@ import { DEST_WELL_BLOWOUT_DESTINATION, } from '../utils/misc' -// well depth for 96 plate is 10.54, so need to add 1mm to top of well -const airGapHelper = makeAirGapHelper({ - wellLocation: { - origin: 'bottom', - offset: { - x: 0, - y: 0, - z: 11.54, - }, - }, -}) const dispenseAirGapHelper = makeDispenseAirGapHelper({ wellLocation: { origin: 'bottom', @@ -457,8 +447,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', expect(res.commands).toEqual([ aspirateHelper('A1', 200), ...delayWithOffset('A1', SOURCE_LABWARE), - - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), delayCommand(12), dispenseAirGapHelper('A2', 5), @@ -467,8 +457,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', aspirateHelper('A1', 200), ...delayWithOffset('A1', SOURCE_LABWARE), - - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), delayCommand(12), dispenseAirGapHelper('A4', 5), dispenseHelper('A4', 100), @@ -496,13 +486,15 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', const res = getSuccessResult(result) expect(res.commands).toEqual([ aspirateHelper('A1', 200), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('A2', 5), dispenseHelper('A2', 100), dispenseHelper('A3', 100), aspirateHelper('A1', 200), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('A4', 5), dispenseHelper('A4', 100), dispenseHelper('A5', 100), @@ -530,7 +522,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', const res = getSuccessResult(result) expect(res.commands).toEqual([ aspirateHelper('A1', 200), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('A2', 5), delayCommand(12), @@ -540,7 +533,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', ...delayWithOffset('A3', DEST_LABWARE), aspirateHelper('A1', 200), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('A4', 5), delayCommand(12), @@ -930,7 +924,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('B1', 31), @@ -977,7 +972,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1009,7 +1005,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), dispenseAirGapHelper('A4', 31), delayCommand(12), @@ -1020,7 +1017,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', touchTipHelper('A4', { labwareId: DEST_LABWARE }), ...blowoutSingleToTrash, // use the dispense > air gap here before moving to trash - airGapHelper('A4', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A4', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1057,7 +1055,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1073,7 +1072,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', touchTipHelper('A3', { labwareId: DEST_LABWARE }), ...blowoutSingleToTrash, // dispense > air gap since we are about to change the tip - airGapHelper('A3', 3, { labwareId: DEST_LABWARE }), // need to air gap here + makeMoveToWellHelper('A3', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1094,7 +1094,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1105,7 +1106,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', touchTipHelper('A4', { labwareId: DEST_LABWARE }), ...blowoutSingleToTrash, // use the dispense > air gap here before moving to trash - airGapHelper('A4', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A4', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip // skip blowout into trash b/c we're about to drop tip anyway @@ -1143,7 +1145,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1174,7 +1177,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1185,7 +1189,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', touchTipHelper('A4', { labwareId: DEST_LABWARE }), ...blowoutSingleToTrash, // use the dispense > air gap here before moving to trash - airGapHelper('A4', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A4', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1220,7 +1225,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1252,7 +1258,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1265,7 +1272,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', blowoutSingleToSourceA1, // use the dispense > air gap here before moving to trash since it is the final dispense in the step // dispense > air gap from source since blowout location is source - airGapHelper('A1', 3), + makeMoveToWellHelper('A1'), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1302,7 +1310,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1319,7 +1328,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', // blowout location is source so need to blowout blowoutSingleToSourceA1, // dispense > air gap so no liquid drops off the tip as pipette moves from source well to trash - airGapHelper('A1', 3), + makeMoveToWellHelper('A1'), + makeAirGapHelper(3), // delay after aspirating air delayCommand(11), // just drop the tip in the trash @@ -1341,7 +1351,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1353,7 +1364,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', // blowout location is source so need to blowout blowoutSingleToSourceA1, // dispense > air gap so no liquid drops off the tip as pipette moves from source well to trash - airGapHelper('A1', 3), + makeMoveToWellHelper('A1'), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1390,7 +1402,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1422,7 +1435,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1434,7 +1448,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', // use the dispense > air gap here before moving to trash // since it is the final dispense in the step blowoutSingleToSourceA1, - airGapHelper('A1', 3), + makeMoveToWellHelper('A1'), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1469,7 +1484,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1501,7 +1517,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1514,7 +1531,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', blowoutSingleToDestA4, // use the dispense > air gap here before moving to trash // since it is the final dispense in the step - airGapHelper('A4', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A4', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1551,7 +1569,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1568,7 +1587,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', // blowout location is dest so we gotta blowout blowoutSingleToDestA3, // dispense > air gap so no liquid drops off the tip as pipette moves from destination well to trash - airGapHelper('A3', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A3', DEST_LABWARE), + makeAirGapHelper(3), // dispense delay delayCommand(11), // just drop the tip in the trash @@ -1590,7 +1610,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1602,7 +1623,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', // use the dispense > air gap here before moving to trash // since it is the final dispense in the step blowoutSingleToDestA4, - airGapHelper('A4', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A4', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1639,7 +1661,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1671,7 +1694,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1684,7 +1708,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', blowoutSingleToDestA4, // use the dispense > air gap here before moving to trash // since it is the final dispense in the step - airGapHelper('A4', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A4', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index 72c89fc264a..cd22d6a321b 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -25,7 +25,7 @@ import { pickUpTipHelper, SOURCE_LABWARE, makeDispenseAirGapHelper, - AIR_GAP_META, + makeMoveToWellHelper, } from '../fixtures' import { FIXED_TRASH_ID } from '../constants' import { @@ -36,16 +36,6 @@ import { transfer } from '../commandCreators/compound/transfer' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { InvariantContext, RobotState, TransferArgs } from '../types' -const airGapHelper = makeAirGapHelper({ - wellLocation: { - origin: 'bottom', - offset: { - x: 0, - y: 0, - z: 11.54, - }, - }, -}) const dispenseAirGapHelper = makeDispenseAirGapHelper({ wellLocation: { origin: 'bottom', @@ -151,7 +141,8 @@ describe('pick up tip if no tip on pipette', () => { pickUpTipHelper('A1'), aspirateHelper('A1', 30), dispenseHelper('B2', 30), - airGapHelper('B2', 5, { labwareId: 'destPlateId' }), + makeMoveToWellHelper('B2', 'destPlateId'), + makeAirGapHelper(5), ]) }) @@ -843,11 +834,13 @@ describe('advanced options', () => { const res = getSuccessResult(result) expect(res.commands).toEqual([ aspirateHelper('A1', 295), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('B1', 5), dispenseHelper('B1', 295), aspirateHelper('A1', 55), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('B1', 5), dispenseHelper('B1', 55), ]) @@ -863,12 +856,13 @@ describe('advanced options', () => { const res = getSuccessResult(result) expect(res.commands).toEqual([ aspirateHelper('A1', 150), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('B1', 5), dispenseHelper('B1', 150), - aspirateHelper('A1', 150), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('B1', 5), dispenseHelper('B1', 150), ]) @@ -886,8 +880,8 @@ describe('advanced options', () => { expect(res.commands).toEqual([ aspirateHelper('A1', 295), ...delayWithOffset('A1', SOURCE_LABWARE), - - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), delayCommand(12), dispenseAirGapHelper('B1', 5), @@ -895,8 +889,8 @@ describe('advanced options', () => { aspirateHelper('A1', 55), ...delayWithOffset('A1', SOURCE_LABWARE), - - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), delayCommand(12), dispenseAirGapHelper('B1', 5), @@ -915,7 +909,8 @@ describe('advanced options', () => { const res = getSuccessResult(result) expect(res.commands).toEqual([ aspirateHelper('A1', 295), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('B1', 5), delayCommand(12), @@ -924,7 +919,8 @@ describe('advanced options', () => { ...delayWithOffset('B1', DEST_LABWARE), aspirateHelper('A1', 55), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('B1', 5), delayCommand(12), @@ -1288,12 +1284,10 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { @@ -1304,6 +1298,14 @@ describe('advanced options', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -1317,7 +1319,7 @@ describe('advanced options', () => { // dispense the aspirate > air gap { commandType: 'dispense', - meta: AIR_GAP_META, + key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -1592,12 +1594,10 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { @@ -1608,6 +1608,14 @@ describe('advanced options', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -1621,7 +1629,6 @@ describe('advanced options', () => { // dispense aspirate > air gap then liquid { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -1777,22 +1784,28 @@ describe('advanced options', () => { }, // use the dispense > air gap here before moving to trash { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 3, labwareId: 'destPlateId', wellName: 'B1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 3, flowRate: 2.1, }, }, @@ -1847,6 +1860,7 @@ describe('advanced options', () => { }, { commandType: 'dispense', + key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -1900,6 +1914,7 @@ describe('advanced options', () => { }, { commandType: 'dispense', + key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -1986,22 +2001,28 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -2015,7 +2036,6 @@ describe('advanced options', () => { // dispense the aspirate > air gap { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -2288,23 +2308,29 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { - flowRate: 2.1, + pipetteId: 'p300SingleId', labwareId: 'sourcePlateId', + wellName: 'A1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { pipetteId: 'p300SingleId', volume: 31, - wellName: 'A1', + flowRate: 2.1, }, }, { @@ -2316,7 +2342,6 @@ describe('advanced options', () => { }, { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { flowRate: 2.2, @@ -2472,25 +2497,31 @@ describe('advanced options', () => { }, // dispense > air gap on the way to trash { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - volume: 3, - flowRate: 2.1, wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, }, }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 3, + flowRate: 2.1, + }, + }, { commandType: 'waitForDuration', key: expect.any(String), @@ -2710,22 +2741,28 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -2739,7 +2776,6 @@ describe('advanced options', () => { // dispense { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -3012,22 +3048,28 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -3041,7 +3083,6 @@ describe('advanced options', () => { // dispense "aspirate > air gap" then dispense liquid { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -3197,25 +3238,31 @@ describe('advanced options', () => { }, // dispense > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - volume: 3, - flowRate: 2.1, wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, }, }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 3, + flowRate: 2.1, + }, + }, { commandType: 'waitForDuration', key: expect.any(String), @@ -3433,22 +3480,28 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -3462,7 +3515,6 @@ describe('advanced options', () => { // dispense { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -3618,23 +3670,29 @@ describe('advanced options', () => { }, // dispense > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'sourcePlateId', wellName: 'A1', - flowRate: 2.1, wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', volume: 3, + flowRate: 2.1, }, }, { @@ -3786,22 +3844,28 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', + commandType: 'moveToWell', key: expect.any(String), - meta: AIR_GAP_META, params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -3815,7 +3879,6 @@ describe('advanced options', () => { // dispense "aspirate > air gap" then dispense liquid { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -3971,25 +4034,31 @@ describe('advanced options', () => { }, // dispense > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'sourcePlateId', wellName: 'A1', - volume: 3, - flowRate: 2.1, wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, }, }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 3, + flowRate: 2.1, + }, + }, { commandType: 'waitForDuration', key: expect.any(String), diff --git a/step-generation/src/commandCreators/atomic/airGapInPlace.ts b/step-generation/src/commandCreators/atomic/airGapInPlace.ts new file mode 100644 index 00000000000..1a7656a3cda --- /dev/null +++ b/step-generation/src/commandCreators/atomic/airGapInPlace.ts @@ -0,0 +1,37 @@ +import { uuid } from '../../utils' +import { pipetteDoesNotExist } from '../../errorCreators' +import type { AirGapInPlaceParams } from '@opentrons/shared-data' +import type { CommandCreator, CommandCreatorError } from '../../types' + +export const airGapInPlace: CommandCreator = ( + args, + invariantContext, + prevRobotState +) => { + const { flowRate, pipetteId, volume } = args + const errors: CommandCreatorError[] = [] + const pipetteSpec = invariantContext.pipetteEntities[pipetteId]?.spec + + if (!pipetteSpec) { + errors.push( + pipetteDoesNotExist({ + pipette: pipetteId, + }) + ) + } + + const commands = [ + { + commandType: 'airGapInPlace' as const, + key: uuid(), + params: { + flowRate, + pipetteId, + volume, + }, + }, + ] + return { + commands, + } +} diff --git a/step-generation/src/commandCreators/atomic/aspirate.ts b/step-generation/src/commandCreators/atomic/aspirate.ts index 07b95f3459d..6d64cf00e7d 100644 --- a/step-generation/src/commandCreators/atomic/aspirate.ts +++ b/step-generation/src/commandCreators/atomic/aspirate.ts @@ -40,7 +40,6 @@ export const aspirate: CommandCreator = ( labwareId, wellName, flowRate, - isAirGap, tipRack, wellLocation, nozzles, @@ -257,7 +256,6 @@ export const aspirate: CommandCreator = ( wellLocation, flowRate, }, - ...(isAirGap && { meta: { isAirGap } }), }, ] return { diff --git a/step-generation/src/commandCreators/atomic/dispense.ts b/step-generation/src/commandCreators/atomic/dispense.ts index 554ebbce840..2ca1c737d08 100644 --- a/step-generation/src/commandCreators/atomic/dispense.ts +++ b/step-generation/src/commandCreators/atomic/dispense.ts @@ -26,7 +26,6 @@ import type { CommandCreator, CommandCreatorError } from '../../types' export interface DispenseAtomicCommandParams extends DispenseParams { nozzles: NozzleConfigurationStyle | null tipRack: string - isAirGap?: boolean } /** Dispense with given args. Requires tip. */ export const dispense: CommandCreator = ( @@ -40,7 +39,6 @@ export const dispense: CommandCreator = ( labwareId, wellName, flowRate, - isAirGap, wellLocation, nozzles, tipRack, @@ -226,7 +224,6 @@ export const dispense: CommandCreator = ( // pushOut will always be undefined in step-generation for now // since there is no easy way to allow users to for it in PD }, - ...(isAirGap && { meta: { isAirGap } }), }, ] return { diff --git a/step-generation/src/commandCreators/atomic/index.ts b/step-generation/src/commandCreators/atomic/index.ts index 5390c77d017..5e2dc2e7bf8 100644 --- a/step-generation/src/commandCreators/atomic/index.ts +++ b/step-generation/src/commandCreators/atomic/index.ts @@ -2,13 +2,14 @@ import { absorbanceReaderCloseLid } from './absorbanceReaderCloseLid' import { absorbanceReaderInitialize } from './absorbanceReaderInitialize' import { absorbanceReaderOpenLid } from './absorbanceReaderOpenLid' import { absorbanceReaderRead } from './absorbanceReaderRead' +import { airGapInPlace } from './airGapInPlace' import { aspirate } from './aspirate' import { aspirateInPlace } from './aspirateInPlace' import { blowout } from './blowout' import { blowOutInPlace } from './blowOutInPlace' +import { comment } from './comment' import { configureForVolume } from './configureForVolume' import { configureNozzleLayout } from './configureNozzleLayout' -import { comment } from './comment' import { deactivateTemperature } from './deactivateTemperature' import { delay } from './delay' import { disengageMagnet } from './disengageMagnet' @@ -21,12 +22,13 @@ import { moveLabware } from './moveLabware' import { moveToAddressableArea } from './moveToAddressableArea' import { moveToAddressableAreaForDropTip } from './moveToAddressableAreaForDropTip' import { moveToWell } from './moveToWell' +import { pickUpTip } from './pickUpTip' import { setTemperature } from './setTemperature' import { touchTip } from './touchTip' import { waitForTemperature } from './waitForTemperature' -import { pickUpTip } from './pickUpTip' export { + airGapInPlace, absorbanceReaderCloseLid, absorbanceReaderInitialize, absorbanceReaderOpenLid, diff --git a/step-generation/src/commandCreators/compound/consolidate.ts b/step-generation/src/commandCreators/compound/consolidate.ts index fb57d522f4d..579d3799161 100644 --- a/step-generation/src/commandCreators/compound/consolidate.ts +++ b/step-generation/src/commandCreators/compound/consolidate.ts @@ -2,11 +2,10 @@ import chunk from 'lodash/chunk' import flatMap from 'lodash/flatMap' import { COLUMN, - getWellDepth, LOW_VOLUME_PIPETTES, GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA, + getWellDepth, } from '@opentrons/shared-data' -import { AIR_GAP_OFFSET_FROM_TOP } from '../../constants' import * as errorCreators from '../../errorCreators' import { getPipetteWithTipMaxVol } from '../../robotStateSelectors' import { movableTrashCommandsUtil } from '../../utils/movableTrashCommandsUtil' @@ -15,15 +14,16 @@ import { curryCommandCreator, reduceCommandCreators, wasteChuteCommandsUtil, - getTrashOrLabware, airGapHelper, dispenseLocationHelper, moveHelper, getIsSafePipetteMovement, getWasteChuteAddressableAreaNamePip, getHasWasteChute, + getTrashOrLabware, } from '../../utils' import { + airGapInPlace, aspirate, configureForVolume, delay, @@ -39,6 +39,7 @@ import type { CommandCreator, CurriedCommandCreator, } from '../../types' +import { AIR_GAP_OFFSET_FROM_TOP } from '../../constants' export const consolidate: CommandCreator = ( args, @@ -178,7 +179,6 @@ export const consolidate: CommandCreator = ( ) const destinationWell = args.destWell - const destLabwareDef = trashOrLabware === 'labware' ? invariantContext.labwareEntities[args.destLabware].def @@ -221,12 +221,10 @@ export const consolidate: CommandCreator = ( getWellDepth(sourceLabwareDef, sourceWell) + AIR_GAP_OFFSET_FROM_TOP const airGapAfterAspirateCommands = aspirateAirGapVolume ? [ - curryCommandCreator(aspirate, { + curryCommandCreator(moveToWell, { pipetteId: args.pipette, - volume: aspirateAirGapVolume, labwareId: args.sourceLabware, wellName: sourceWell, - flowRate: aspirateFlowRateUlSec, wellLocation: { origin: 'bottom', offset: { @@ -235,9 +233,11 @@ export const consolidate: CommandCreator = ( y: 0, }, }, - isAirGap: true, - tipRack: args.tipRack, - nozzles, + }), + curryCommandCreator(airGapInPlace, { + pipetteId: args.pipette, + volume: aspirateAirGapVolume, + flowRate: aspirateFlowRateUlSec, }), ...(aspirateDelay != null ? [ @@ -467,8 +467,6 @@ export const consolidate: CommandCreator = ( destWell: destinationWell, flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: airGapOffsetDestWell, - tipRack: args.tipRack, - nozzles, }), ...(aspirateDelay != null ? [ diff --git a/step-generation/src/commandCreators/compound/distribute.ts b/step-generation/src/commandCreators/compound/distribute.ts index d5f3e63c1cb..de87cffd29d 100644 --- a/step-generation/src/commandCreators/compound/distribute.ts +++ b/step-generation/src/commandCreators/compound/distribute.ts @@ -16,12 +16,13 @@ import { reduceCommandCreators, blowoutUtil, wasteChuteCommandsUtil, - getDispenseAirGapLocation, getIsSafePipetteMovement, getWasteChuteAddressableAreaNamePip, getHasWasteChute, + getDispenseAirGapLocation, } from '../../utils' import { + airGapInPlace, aspirate, configureForVolume, delay, @@ -215,12 +216,10 @@ export const distribute: CommandCreator = ( getWellDepth(destLabwareDef, firstDestWell) + AIR_GAP_OFFSET_FROM_TOP const airGapAfterAspirateCommands = aspirateAirGapVolume ? [ - curryCommandCreator(aspirate, { + curryCommandCreator(moveToWell, { pipetteId: args.pipette, - volume: aspirateAirGapVolume, labwareId: args.sourceLabware, wellName: args.sourceWell, - flowRate: aspirateFlowRateUlSec, wellLocation: { origin: 'bottom', offset: { @@ -229,9 +228,11 @@ export const distribute: CommandCreator = ( y: 0, }, }, - isAirGap: true, - tipRack: args.tipRack, - nozzles, + }), + curryCommandCreator(airGapInPlace, { + pipetteId: args.pipette, + volume: aspirateAirGapVolume, + flowRate: aspirateFlowRateUlSec, }), ...(aspirateDelay != null ? [ @@ -254,7 +255,6 @@ export const distribute: CommandCreator = ( y: 0, }, }, - isAirGap: true, nozzles, tipRack: args.tipRack, }), @@ -345,7 +345,6 @@ export const distribute: CommandCreator = ( }), ] } - const { dispenseAirGapLabware, dispenseAirGapWell, @@ -362,12 +361,10 @@ export const distribute: CommandCreator = ( const airGapAfterDispenseCommands = dispenseAirGapVolume && !willReuseTip ? [ - curryCommandCreator(aspirate, { + curryCommandCreator(moveToWell, { pipetteId: args.pipette, - volume: dispenseAirGapVolume, labwareId: dispenseAirGapLabware, wellName: dispenseAirGapWell, - flowRate: aspirateFlowRateUlSec, wellLocation: { origin: 'bottom', offset: { @@ -376,10 +373,11 @@ export const distribute: CommandCreator = ( y: 0, }, }, - isAirGap: true, - tipRack: args.tipRack, - - nozzles, + }), + curryCommandCreator(airGapInPlace, { + pipetteId: args.pipette, + volume: dispenseAirGapVolume, + flowRate: aspirateFlowRateUlSec, }), ...(aspirateDelay != null ? [ diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index cd62cbacfbc..adfb9c43887 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -22,6 +22,7 @@ import { getHasWasteChute, } from '../../utils' import { + airGapInPlace, aspirate, configureForVolume, delay, @@ -394,12 +395,10 @@ export const transfer: CommandCreator = ( const airGapAfterAspirateCommands = aspirateAirGapVolume && destinationWell != null ? [ - curryCommandCreator(aspirate, { + curryCommandCreator(moveToWell, { pipetteId: args.pipette, - volume: aspirateAirGapVolume, labwareId: args.sourceLabware, wellName: sourceWell, - flowRate: aspirateFlowRateUlSec, wellLocation: { origin: 'bottom', offset: { @@ -408,9 +407,11 @@ export const transfer: CommandCreator = ( y: 0, }, }, - isAirGap: true, - tipRack, - nozzles: args.nozzles, + }), + curryCommandCreator(airGapInPlace, { + pipetteId: args.pipette, + volume: aspirateAirGapVolume, + flowRate: aspirateFlowRateUlSec, }), ...(aspirateDelay != null ? [ @@ -433,7 +434,6 @@ export const transfer: CommandCreator = ( y: 0, }, }, - isAirGap: true, tipRack: args.tipRack, nozzles: args.nozzles, }), @@ -539,8 +539,6 @@ export const transfer: CommandCreator = ( destWell: destinationWell, flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: airGapOffsetDestWell, - tipRack, - nozzles: args.nozzles, }), ...(aspirateDelay != null ? [ diff --git a/step-generation/src/fixtures/commandFixtures.ts b/step-generation/src/fixtures/commandFixtures.ts index 86b6e9ea030..aac4662fea0 100644 --- a/step-generation/src/fixtures/commandFixtures.ts +++ b/step-generation/src/fixtures/commandFixtures.ts @@ -3,7 +3,6 @@ import { tiprackWellNamesFlat, DEFAULT_PIPETTE, SOURCE_LABWARE, - AIR_GAP_META, DEFAULT_BLOWOUT_WELL, DEST_LABWARE, } from './data' @@ -139,21 +138,30 @@ export const makeAspirateHelper: MakeAspDispHelper = bakedP ...params, }, }) -export const makeAirGapHelper: MakeAirGapHelper = bakedParams => ( - wellName, - volume, - params -) => ({ - commandType: 'aspirate', - meta: AIR_GAP_META, +export const makeMoveToWellHelper = (wellName: string, labwareId?: string) => ({ + commandType: 'moveToWell', key: expect.any(String), params: { - ..._defaultAspirateParams, - ...bakedParams, + pipetteId: DEFAULT_PIPETTE, + labwareId: labwareId ?? SOURCE_LABWARE, wellName, + wellLocation: { + origin: 'bottom', + offset: { + x: 0, + y: 0, + z: 11.54, + }, + }, + }, +}) +export const makeAirGapHelper = (volume: number) => ({ + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: DEFAULT_PIPETTE, volume, flowRate: ASPIRATE_FLOW_RATE, - ...params, }, }) export const blowoutHelper = ( @@ -238,7 +246,6 @@ export const makeDispenseAirGapHelper: MakeDispenseAirGapHelper = ( flowRate, offsetFromBottomMm, pipetteId, - tipRack, sourceId, sourceWell, volume, - nozzles, } = args const trashOrLabware = getTrashOrLabware( @@ -674,12 +670,10 @@ export const airGapHelper: CommandCreator = ( }) commands = [ - curryCommandCreator(aspirate, { - pipetteId: pipetteId, - volume, + curryCommandCreator(moveToWell, { + pipetteId, labwareId: dispenseAirGapLabware, wellName: dispenseAirGapWell, - flowRate, wellLocation: { origin: 'bottom', offset: { @@ -688,20 +682,20 @@ export const airGapHelper: CommandCreator = ( y: 0, }, }, - isAirGap: true, - tipRack, - nozzles, + }), + curryCommandCreator(airGapInPlace, { + pipetteId, + volume, + flowRate, }), ] // when aspirating out of multi wells for consolidate } else { commands = [ - curryCommandCreator(aspirate, { - pipetteId: pipetteId, - volume, + curryCommandCreator(moveToWell, { + pipetteId, labwareId: destinationId, wellName: destWell, - flowRate, wellLocation: { origin: 'bottom', offset: { @@ -710,9 +704,11 @@ export const airGapHelper: CommandCreator = ( y: 0, }, }, - isAirGap: true, - tipRack, - nozzles, + }), + curryCommandCreator(airGapInPlace, { + pipetteId, + volume, + flowRate, }), ] }