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}`)}
-
+
{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,
}),
]
}