Skip to content

Commit

Permalink
add trash location validation and update transfer_liquid structure
Browse files Browse the repository at this point in the history
  • Loading branch information
sanni-t committed Nov 25, 2024
1 parent 4bc6960 commit a682ea5
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 14 deletions.
17 changes: 15 additions & 2 deletions api/src/opentrons/protocol_api/core/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,14 +307,27 @@ def configure_nozzle_layout(
...

@abstractmethod
def transfer_liquid(
def load_liquid_class(
self,
liquid_class: LiquidClass,
pipette_load_name: str,
tiprack_uri: str,
) -> str:
"""Load the liquid class properties of given pipette and tiprack into the engine.
Returns: ID of the liquid class record
"""
...

@abstractmethod
def transfer_liquid(
self,
liquid_class_id: str,
volume: float,
source: List[WellCoreType],
dest: List[WellCoreType],
new_tip: TransferTipPolicyV2,
trash_location: Union[types.Location, TrashBin, WasteChute],
trash_location: Union[WellCoreType, types.Location, TrashBin, WasteChute],
) -> None:
"""Transfer a liquid from source to dest according to liquid class properties."""
...
Expand Down
42 changes: 31 additions & 11 deletions api/src/opentrons/protocol_api/instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -1516,7 +1516,9 @@ def transfer_liquid(
labware.Well, Sequence[labware.Well], Sequence[Sequence[labware.Well]]
],
new_tip: Literal["once", "always", "never"] = "once",
trash_location: Optional[Union[types.Location, TrashBin, WasteChute]] = None,
trash_location: Optional[
Union[types.Location, labware.Well, TrashBin, WasteChute]
] = None,
) -> InstrumentContext:
"""Transfer liquid from source to dest using the specified liquid class properties.
Expand Down Expand Up @@ -1569,18 +1571,36 @@ def transfer_liquid(
" Ensure that all previously aspirated liquid is dispensed before starting"
" a new transfer."
)
liquid_class_props = liquid_class.get_for(
pipette=self.name, tiprack=tiprack.name
)
checked_trash_location: Union[
types.Location, labware.Labware, TrashBin, WasteChute
]

_trash_location: Union[types.Location, labware.Well, TrashBin, WasteChute]
if trash_location is None:
checked_trash_location = (
self.trash_container
) # Could be a labware or a trash fixture
saved_trash = self.trash_container
if isinstance(saved_trash, labware.Labware):
_trash_location = saved_trash.wells()[0]
else:
_trash_location = saved_trash
else:
checked_trash_location = trash_location
_trash_location = trash_location

checked_trash_location = validation.ensure_valid_trash_location_for_transfer_v2(
trash_location=_trash_location
)
liquid_class_id = self._core.load_liquid_class(
liquid_class=liquid_class,
pipette_load_name=self.name,
tiprack_uri=tiprack.uri,
)

self._core.transfer_liquid(
liquid_class_id=liquid_class_id,
volume=volume,
source=[well._core for well in flat_sources_list],
dest=[well._core for well in flat_dest_list],
new_tip=valid_new_tip,
trash_location=checked_trash_location._core
if isinstance(checked_trash_location, labware.Well)
else checked_trash_location,
)

return self

Expand Down
32 changes: 31 additions & 1 deletion api/src/opentrons/protocol_api/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@

from .disposal_locations import TrashBin, WasteChute


if TYPE_CHECKING:
from .labware import Well

Expand Down Expand Up @@ -683,3 +682,34 @@ def ensure_valid_flat_wells_list_for_transfer_v2(
f" Location should be a well instance, or a 1-dimensional or"
f" 2-dimensional sequence of well instances."
)


def ensure_valid_trash_location_for_transfer_v2(
trash_location: Union[Location, Well, TrashBin, WasteChute]
) -> Union[Location, Well, TrashBin, WasteChute]:
"""Ensure that the trash location is valid for v2 transfer."""
if (
isinstance(trash_location, Well)
or isinstance(trash_location, TrashBin)
or isinstance(trash_location, WasteChute)
):
return trash_location
elif isinstance(trash_location, Location):
_, maybe_well = trash_location.labware.get_parent_labware_and_well()

if maybe_well is None:
raise TypeError(
"If a location is specified as a `types.Location`"
" (for instance, as the result of a call to `Well.top()`),"
" it must be a location relative to a well,"
" since that is where a tip is dropped."
f" However, the given location refers to {trash_location.labware}"
)
return trash_location
else:
raise TypeError(
f"If specified, location should be an instance of"
f" `types.Location` (e.g. the return value from `Well.top()`)"
f" or `Well` (e.g. `tiprack.wells()[0]`) or an instance of `TrashBin` or `WasteChute`."
f" However, it is {trash_location}."
)
73 changes: 73 additions & 0 deletions api/tests/opentrons/protocol_api/test_instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -1863,3 +1863,76 @@ def test_transfer_liquid_raises_if_tip_has_liquid(
dest=[mock_well],
new_tip="never",
)


@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"])
def test_transfer_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 load liquid class into engine and delegate the transfer 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(
labware.next_available_tip(
starting_tip=None,
tip_racks=tip_racks,
channels=2,
nozzle_map=MOCK_MAP,
)
).then_return((next_tiprack, decoy.mock(cls=Well)))
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")
decoy.when(
mock_instrument_core.load_liquid_class(
liquid_class=test_liq_class,
pipette_load_name="pipette-name",
tiprack_uri="tiprack-uri",
)
).then_return("liq-class-id")

subject.transfer_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.transfer_liquid(
liquid_class_id="liq-class-id",
volume=10,
source=[mock_well._core],
dest=[mock_well._core],
new_tip=TransferTipPolicyV2.ONCE,
trash_location=trash_location.move(Point(1, 2, 3)),
)
)

0 comments on commit a682ea5

Please sign in to comment.