Skip to content

Commit

Permalink
added tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sanni-t committed Jan 2, 2025
1 parent 9b2de59 commit 279a913
Show file tree
Hide file tree
Showing 5 changed files with 447 additions and 66 deletions.
6 changes: 3 additions & 3 deletions api/src/opentrons/protocol_api/core/engine/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -1013,7 +1013,7 @@ def aspirate_liquid_class(
liquid=0,
air_gap=0,
)
components_executer = tx_comps_executor.TransferComponentsExecutor(
components_executor = tx_comps_executor.TransferComponentsExecutor(
instrument_core=self,
transfer_properties=transfer_properties,
target_location=aspirate_location,
Expand All @@ -1040,10 +1040,10 @@ def dispense_liquid_class(
self,
volume: float,
dest: Tuple[Location, WellCore],
source: Optional[Tuple[Location, WellCore]],
transfer_properties: TransferProperties,
transfer_type: tx_comps_executor.TransferType,
tip_contents: List[tx_comps_executor.LiquidAndAirGapPair],
source: Optional[Tuple[Location, WellCore]],
trash_location: Union[Location, TrashBin, WasteChute],
) -> tx_comps_executor.LiquidAndAirGapPair:
"""Execute single-dispense steps.
Expand Down Expand Up @@ -1092,7 +1092,7 @@ def dispense_liquid_class(
liquid=0,
air_gap=0,
)
components_executor = tx_comps_executor.get_transfer_components_executor(
components_executor = tx_comps_executor.TransferComponentsExecutor(
instrument_core=self,
transfer_properties=transfer_properties,
target_location=dispense_location,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class TipState:
"""

ready_to_aspirate: bool = True
# TODO: maybe use the tip contents from engine state instead.
last_liquid_and_air_gap_in_tip: LiquidAndAirGapPair = LiquidAndAirGapPair(
liquid=0,
air_gap=0,
Expand Down Expand Up @@ -84,20 +85,6 @@ def remove_air_gap(self, volume: float) -> None:
), "Last air gap volume doe not match the volume being removed"
self.last_liquid_and_air_gap_in_tip.air_gap = 0

def update(
self,
liquid: Optional[float] = None,
air_gap: Optional[float] = None,
ready_to_aspirate: Optional[bool] = None,
) -> None:
"""Update the tip state contents with given values."""
if liquid is not None:
self.last_liquid_and_air_gap_in_tip.liquid = liquid
if air_gap is not None:
self.last_liquid_and_air_gap_in_tip.air_gap = air_gap
if ready_to_aspirate is not None:
self.ready_to_aspirate = ready_to_aspirate


class TransferType(Enum):
ONE_TO_ONE = "one_to_one"
Expand Down Expand Up @@ -333,7 +320,7 @@ def retract_after_aspiration(self, volume: float) -> None:
)
)

def retract_after_dispensing( # noqa: C901
def retract_after_dispensing(
self,
trash_location: Union[Location, TrashBin, WasteChute],
source_location: Optional[Location],
Expand Down Expand Up @@ -385,42 +372,6 @@ def retract_after_dispensing( # noqa: C901
assert retract_delay.duration is not None
self._instrument.delay(retract_delay.duration)

def _do_touch_tip_and_air_gap() -> None:
touch_tip_props = retract_props.touch_tip
if touch_tip_props.enabled:
assert (
touch_tip_props.speed is not None
and touch_tip_props.z_offset is not None
and touch_tip_props.mm_to_edge is not None
)
# TODO: update this once touch tip has mmToEdge
self._instrument.touch_tip(
location=retract_location,
well_core=self._target_well,
radius=1,
z_offset=touch_tip_props.z_offset,
speed=touch_tip_props.speed,
)
self._instrument.move_to(
location=retract_location,
well_core=self._target_well,
force_direct=True,
minimum_z_height=None,
# Full speed because the tip will already be out of the liquid
speed=None,
)

if self._transfer_type != TransferType.ONE_TO_MANY:
# TODO: check if it is okay to just do `prepare_to_aspirate` unconditionally
if not self._tip_state.ready_to_aspirate:
self._instrument.prepare_to_aspirate()
self._tip_state.ready_to_aspirate = True
self._add_air_gap(
air_gap_volume=self._transfer_properties.aspirate.retract.air_gap_by_volume.get_for_volume(
0
)
)

blowout_props = retract_props.blowout
if (
blowout_props.enabled
Expand All @@ -434,32 +385,97 @@ def _do_touch_tip_and_air_gap() -> None:
in_place=True,
)
self._tip_state.ready_to_aspirate = False
_do_touch_tip_and_air_gap()
self._do_touch_tip_and_air_gap(
location=retract_location, well=self._target_well
)

if (
blowout_props.enabled
and not blowout_props.location == BlowoutLocation.DESTINATION
and blowout_props.location != BlowoutLocation.DESTINATION
):
assert blowout_props.flow_rate is not None
self._instrument.set_flow_rate(blow_out=blowout_props.flow_rate)
touch_tip_and_air_gap_location: Optional[Location]
if blowout_props.location == BlowoutLocation.SOURCE:
if source_location is None:
raise RuntimeError(
"Blowout location is source but source location is not provided."
"Blowout location is 'source' but source location is not provided."
)
self._instrument.blow_out(
location=source_location,
well_core=source_well,
in_place=False,
)
touch_tip_and_air_gap_location = source_location
touch_tip_and_air_gap_well = source_well
else:
self._instrument.blow_out(
location=trash_location,
well_core=None, # Update this to correct well core
well_core=None, # TODO: Update this to correct well core
in_place=False,
)
touch_tip_and_air_gap_location = (
trash_location if isinstance(trash_location, Location) else None
)
touch_tip_and_air_gap_well = (
None # TODO: Update this to correct well core
)
last_air_gap = self._tip_state.last_liquid_and_air_gap_in_tip.air_gap
self._tip_state.remove_air_gap(last_air_gap)
self._tip_state.ready_to_aspirate = False
_do_touch_tip_and_air_gap()
self._do_touch_tip_and_air_gap(
location=touch_tip_and_air_gap_location,
well=touch_tip_and_air_gap_well,
)

def _do_touch_tip_and_air_gap(
self,
location: Optional[Location],
well: Optional[WellCore],
) -> None:
"""Perform touch tip and air gap as part of post-dispense retract."""
touch_tip_props = self._transfer_properties.dispense.retract.touch_tip
if touch_tip_props.enabled:
assert (
touch_tip_props.speed is not None
and touch_tip_props.z_offset is not None
and touch_tip_props.mm_to_edge is not None
)
# TODO: update this once touch tip has mmToEdge
# Also, check that when blow out is a non-dest-well,
# whether the touch tip params from transfer props should be used for
# both dest-well touch tip and non-dest-well touch tip.
if well is not None and location is not None:
self._instrument.touch_tip(
location=location,
well_core=well,
radius=1,
z_offset=touch_tip_props.z_offset,
speed=touch_tip_props.speed,
)
else:
raise RuntimeError(
"Invalid touch tip location for post-dispense retraction."
)
self._instrument.move_to(
location=location,
well_core=well,
force_direct=True,
minimum_z_height=None,
# Full speed because the tip will already be out of the liquid
speed=None,
)

if self._transfer_type != TransferType.ONE_TO_MANY:
# TODO: check if it is okay to just do `prepare_to_aspirate` unconditionally
if not self._tip_state.ready_to_aspirate:
self._instrument.prepare_to_aspirate()
self._tip_state.ready_to_aspirate = True
self._add_air_gap(
air_gap_volume=self._transfer_properties.aspirate.retract.air_gap_by_volume.get_for_volume(
0
)
)

def _add_air_gap(self, air_gap_volume: float) -> None:
"""Add an air gap."""
Expand Down
2 changes: 1 addition & 1 deletion api/tests/opentrons/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,7 @@ def maximal_liquid_class_def() -> LiquidClassSchemaV1:
),
),
delay=DelayProperties(
enable=False, params=DelayParams(duration=0)
enable=True, params=DelayParams(duration=10)
),
),
positionReference=PositionReference.WELL_BOTTOM,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
TransferComponentsExecutor,
TransferType,
TipState,
LiquidAndAirGapPair,
)
from opentrons.protocol_engine import (
DeckPoint,
Expand Down Expand Up @@ -1627,9 +1628,9 @@ def test_aspirate_liquid_class(
)
).then_return(mock_transfer_components_executor)
decoy.when(
mock_engine_client.state.pipettes.get_aspirated_volume("abc123")
).then_return(111)
subject.aspirate_liquid_class(
mock_transfer_components_executor.tip_state.last_liquid_and_air_gap_in_tip
).then_return(LiquidAndAirGapPair(liquid=111, air_gap=222))
result = subject.aspirate_liquid_class(
volume=123,
source=(source_location, source_well),
transfer_properties=test_transfer_properties,
Expand All @@ -1648,3 +1649,73 @@ def test_aspirate_liquid_class(
mock_transfer_components_executor.aspirate_and_wait(volume=123),
mock_transfer_components_executor.retract_after_aspiration(volume=123),
)
assert result == LiquidAndAirGapPair(air_gap=222, liquid=111)


def test_dispense_liquid_class(
decoy: Decoy,
mock_engine_client: EngineClient,
subject: InstrumentCore,
minimal_liquid_class_def2: LiquidClassSchemaV1,
mock_transfer_components_executor: TransferComponentsExecutor,
) -> None:
"""It should call dispense sub-steps execution based on liquid class."""
source_well = decoy.mock(cls=WellCore)
source_location = Location(Point(1, 2, 3), labware=None)
dest_well = decoy.mock(cls=WellCore)
dest_location = Location(Point(3, 2, 1), labware=None)
test_liquid_class = LiquidClass.create(minimal_liquid_class_def2)
test_transfer_properties = test_liquid_class.get_for(
"flex_1channel_50", "opentrons_flex_96_tiprack_50ul"
)
push_out_vol = test_transfer_properties.dispense.push_out_by_volume.get_for_volume(
123
)
decoy.when(
transfer_components_executor.absolute_point_from_position_reference_and_offset(
well=dest_well,
position_reference=PositionReference.WELL_BOTTOM,
offset=Coordinate(x=0, y=0, z=-5),
)
).then_return(Point(1, 2, 3))
decoy.when(
transfer_components_executor.TransferComponentsExecutor(
instrument_core=subject,
transfer_properties=test_transfer_properties,
target_location=Location(Point(1, 2, 3), labware=None),
target_well=dest_well,
transfer_type=TransferType.ONE_TO_ONE,
tip_state=TipState(),
)
).then_return(mock_transfer_components_executor)
decoy.when(
mock_transfer_components_executor.tip_state.last_liquid_and_air_gap_in_tip
).then_return(LiquidAndAirGapPair(liquid=333, air_gap=444))
result = subject.dispense_liquid_class(
volume=123,
dest=(dest_location, dest_well),
source=(source_location, source_well),
transfer_properties=test_transfer_properties,
transfer_type=TransferType.ONE_TO_ONE,
tip_contents=[],
trash_location=Location(Point(1, 2, 3), labware=None),
)
decoy.verify(
mock_transfer_components_executor.submerge(
submerge_properties=test_transfer_properties.dispense.submerge,
),
mock_transfer_components_executor.dispense_and_wait(
volume=123,
push_out_override=push_out_vol,
),
mock_transfer_components_executor.mix(
mix_properties=test_transfer_properties.dispense.mix,
last_dispense_push_out=True,
),
mock_transfer_components_executor.retract_after_dispensing(
trash_location=Location(Point(1, 2, 3), labware=None),
source_location=source_location,
source_well=source_well,
),
)
assert result == LiquidAndAirGapPair(air_gap=444, liquid=333)
Loading

0 comments on commit 279a913

Please sign in to comment.