Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'edge' into chore_pd-rm-absreader-ff
Browse files Browse the repository at this point in the history
ncdiehl11 committed Jan 28, 2025
2 parents b10e319 + 8f8b055 commit 67c0dd5
Showing 55 changed files with 7,942 additions and 385 deletions.
2 changes: 2 additions & 0 deletions api/src/opentrons/hardware_control/api.py
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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"
13 changes: 11 additions & 2 deletions api/src/opentrons/hardware_control/ot3api.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
"""
...

31 changes: 28 additions & 3 deletions api/src/opentrons/protocol_api/core/engine/instrument.py
Original file line number Diff line number Diff line change
@@ -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,
Original file line number Diff line number Diff line change
@@ -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
26 changes: 25 additions & 1 deletion api/src/opentrons/protocol_api/core/instrument.py
Original file line number Diff line number Diff line change
@@ -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."""
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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"
88 changes: 88 additions & 0 deletions api/src/opentrons/protocol_api/instrument_context.py
Original file line number Diff line number Diff line change
@@ -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:
"""
Original file line number Diff line number Diff line change
@@ -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(
2 changes: 2 additions & 0 deletions api/src/opentrons/protocol_engine/commands/aspirate.py
Original file line number Diff line number Diff line change
@@ -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):
Original file line number Diff line number Diff line change
@@ -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 (
2 changes: 2 additions & 0 deletions api/src/opentrons/protocol_engine/commands/dispense.py
Original file line number Diff line number Diff line change
@@ -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):
Original file line number Diff line number Diff line change
@@ -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 (
24 changes: 21 additions & 3 deletions api/src/opentrons/protocol_engine/commands/pipetting_common.py
Original file line number Diff line number Diff line change
@@ -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(
15 changes: 13 additions & 2 deletions api/src/opentrons/protocol_engine/execution/pipetting.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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,
)
),
Original file line number Diff line number Diff line change
@@ -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]
]
262 changes: 262 additions & 0 deletions api/tests/opentrons/protocol_api/test_instrument_context.py
Original file line number Diff line number Diff line change
@@ -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)),
)
)
Original file line number Diff line number Diff line change
@@ -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),
Original file line number Diff line number Diff line change
@@ -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())

5 changes: 5 additions & 0 deletions api/tests/opentrons/protocol_engine/commands/test_aspirate.py
Original file line number Diff line number Diff line change
@@ -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)

Original file line number Diff line number Diff line change
@@ -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())

12 changes: 10 additions & 2 deletions api/tests/opentrons/protocol_engine/commands/test_dispense.py
Original file line number Diff line number Diff line change
@@ -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())

Original file line number Diff line number Diff line change
@@ -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())

Original file line number Diff line number Diff line change
@@ -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
),
4 changes: 2 additions & 2 deletions protocol-designer/cypress/support/SupportModules.ts
Original file line number Diff line number Diff line change
@@ -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}`)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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",
10 changes: 10 additions & 0 deletions protocol-designer/src/assets/localization/en/modal.json
Original file line number Diff line number Diff line change
@@ -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 <strong>8.2.0+</strong> to run.",
"body5": "For more information, see the <link1>Protocol Designer Instruction Manual</link1>."
},
"absorbancePlateReaderSupport": {
"heading": "{{version}} Release Notes",
"body1": "Welcome to Protocol Designer {{version}}!",
"body2": "This release includes the following improvements:",
"body3": "Protocol Designer now supports the <link1>Absorbance Plate Reader Module</link1>",
"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 <link1>Protocol Designer Instruction Manual</link1>."
}
},
"labware_selection": {
Original file line number Diff line number Diff line change
@@ -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: (
<Flex justifyContent={JUSTIFY_CENTER} paddingTop={SPACING.spacing8}>
<img width="340" src={deckConfigutation} />
<img width="340" src={deckConfiguration} />
</Flex>
),
heading: t('announcements.header', { pd: PD }),
@@ -372,5 +376,85 @@ export const useAnnouncements = (): Announcement[] => {
</Flex>
),
},
{
announcementKey: 'absorbancePlateReader',
image: (
<Flex
justifyContent={JUSTIFY_CENTER}
paddingTop={SPACING.spacing8}
backgroundColor={COLORS.blue10}
>
<img width="100%" src={absorbancePlateReaderImage} />
</Flex>
),
heading: t('announcements.absorbancePlateReaderSupport.heading', {
version: pdVersion,
}),
message: (
<Flex gridGap={SPACING.spacing4} flexDirection={DIRECTION_COLUMN}>
<StyledText desktopStyle="bodyDefaultSemiBold">
{t('announcements.absorbancePlateReaderSupport.body1', {
version: pdVersion,
})}
</StyledText>
<Flex flexDirection={DIRECTION_COLUMN}>
<StyledText desktopStyle="bodyDefaultRegular">
{t('announcements.absorbancePlateReaderSupport.body2')}
</StyledText>
<Flex marginLeft={SPACING.spacing16}>
<ul>
<li>
<StyledText desktopStyle="bodyDefaultRegular">
<Trans
t={t}
components={{
link1: (
<LinkComponent
external
href={OPENTRONS_ABSORBANCE_READER_URL}
textDecoration={TEXT_DECORATION_UNDERLINE}
color={COLORS.black90}
/>
),
}}
i18nKey="announcements.absorbancePlateReaderSupport.body3"
/>
</StyledText>
</li>
<li>
<StyledText desktopStyle="bodyDefaultRegular">
{t('announcements.absorbancePlateReaderSupport.body4')}
</StyledText>
</li>
<li>
<StyledText desktopStyle="bodyDefaultRegular">
{t('announcements.absorbancePlateReaderSupport.body5')}
</StyledText>
</li>
</ul>
</Flex>
</Flex>
<StyledText desktopStyle="bodyDefaultRegular">
{t('announcements.absorbancePlateReaderSupport.body6')}
</StyledText>
<StyledText desktopStyle="bodyDefaultRegular">
<Trans
t={t}
components={{
link1: (
<LinkComponent
external
href={DOC_URL}
textDecoration={TEXT_DECORATION_UNDERLINE}
color={COLORS.black90}
/>
),
}}
i18nKey="announcements.absorbancePlateReaderSupport.body7"
/>
</StyledText>
</Flex>
),
},
]
}
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => void = e => {
const value: string | null | undefined = e.currentTarget.value
const masked = fieldProcessors.composeMaskers(
@@ -247,17 +273,14 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element {
dispatch(deselectAllWells())
onClose()
}}
onCloseClick={handleClearSelectedWells}
onCloseClick={handleClearAllWells}
height="100%"
width="21.875rem"
closeButton={
<StyledText desktopStyle="bodyDefaultRegular">
{t('clear_wells')}
</StyledText>
}
disableCloseButton={
!(labwareId != null && selectedWells != null && selectionHasLiquids)
}
>
{(liquidsInLabware != null && liquidsInLabware.length > 0) ||
selectedWells.length > 0 ? (
Original file line number Diff line number Diff line change
@@ -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 (
Original file line number Diff line number Diff line change
@@ -173,22 +173,24 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null {
</StyledText>
) : null}
<Flex gridGap={SPACING.spacing4} flexWrap={WRAP}>
{filteredSupportedModules.map(moduleModel => {
const numSlotsAvailable = getNumSlotsAvailable(
modules,
additionalEquipment,
moduleModel
)
return (
<AddModuleEmptySelectorButton
key={moduleModel}
moduleModel={moduleModel}
areSlotsAvailable={numSlotsAvailable > 0}
hasGripper={hasGripper}
handleAddModule={handleAddModule}
/>
)
})}
{filteredSupportedModules
.sort((moduleA, moduleB) => moduleA.localeCompare(moduleB))
.map(moduleModel => {
const numSlotsAvailable = getNumSlotsAvailable(
modules,
additionalEquipment,
moduleModel
)
return (
<AddModuleEmptySelectorButton
key={moduleModel}
moduleModel={moduleModel}
areSlotsAvailable={numSlotsAvailable > 0}
hasGripper={hasGripper}
handleAddModule={handleAddModule}
/>
)
})}
</Flex>
{modules != null && Object.keys(modules).length > 0 ? (
<Flex
@@ -209,6 +211,9 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null {
gridGap={SPACING.spacing4}
>
{Object.entries(modules)
.sort(([, moduleA], [, moduleB]) =>
moduleA.model.localeCompare(moduleB.model)
)
.reduce<Array<FormModule & { count: number; key: string }>>(
(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 (
Original file line number Diff line number Diff line change
@@ -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 = {
Original file line number Diff line number Diff line change
@@ -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 } = {
16 changes: 13 additions & 3 deletions protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx
Original file line number Diff line number Diff line change
@@ -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))
})
}

65 changes: 39 additions & 26 deletions protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx
Original file line number Diff line number Diff line change
@@ -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')
)
Original file line number Diff line number Diff line change
@@ -68,7 +68,7 @@ export function OffDeckDetails(props: OffDeckDetailsProps): JSX.Element {
return (
<Flex
backgroundColor={COLORS.white}
borderRadius={BORDERS.borderRadius8}
borderRadius={BORDERS.borderRadius12}
width="100%"
height="65vh"
padding={padding}
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ interface PathButtonProps {
}

function PathButton(props: PathButtonProps): JSX.Element {
const { disabled, onClick, id, path, selected, subtitle } = props
const { disabled, onClick, path, selected, subtitle } = props
const [targetProps, tooltipProps] = useHoverTooltip({
placement: TOOLTIP_TOP_START,
})
@@ -79,14 +79,18 @@ function PathButton(props: PathButtonProps): JSX.Element {
<Tooltip tooltipProps={tooltipProps} maxWidth="24.5rem">
<Flex gridGap={SPACING.spacing8} flexDirection={DIRECTION_COLUMN}>
<Box>{t(`step_edit_form.field.path.title.${path}`)}</Box>
<img src={PATH_ANIMATION_IMAGES[path]} width="361px" />
<img
src={PATH_ANIMATION_IMAGES[path]}
width="361px"
alt="path animation"
/>
<Box>{subtitle}</Box>
</Flex>
</Tooltip>
)

return (
<Flex {...targetProps} key={id}>
<Flex {...targetProps} flexDirection={DIRECTION_COLUMN}>
{tooltip}
<RadioButton
width="100%"
6,468 changes: 6,468 additions & 0 deletions shared-data/command/schemas/12.json

Large diffs are not rendered by default.

203 changes: 142 additions & 61 deletions step-generation/src/__tests__/consolidate.test.ts

Large diffs are not rendered by default.

125 changes: 75 additions & 50 deletions step-generation/src/__tests__/distribute.test.ts
Original file line number Diff line number Diff line change
@@ -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(),
Loading

0 comments on commit 67c0dd5

Please sign in to comment.