Skip to content

Commit

Permalink
add instr context tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sanni-t committed Nov 15, 2024
1 parent 298218d commit 1ce8da6
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 40 deletions.
4 changes: 2 additions & 2 deletions api/src/opentrons/protocol_api/core/engine/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import contextlib
from typing import Optional, TYPE_CHECKING, cast, Union
from typing import Optional, TYPE_CHECKING, cast, Union, Iterator
from opentrons.protocols.api_support.types import APIVersion

from opentrons.types import Location, Mount, NozzleConfigurationType, NozzleMapInterface
Expand Down Expand Up @@ -945,7 +945,7 @@ def nozzle_configuration_valid_for_lld(self) -> bool:
)

@contextlib.contextmanager
def load_liquid_class(self, liquid_class: LiquidClass):
def load_liquid_class(self, liquid_class: LiquidClass) -> Iterator[None]:
"""Load a liquid class into the engine."""
try:
# TODO: issue a loadLiquidClass command
Expand Down
19 changes: 11 additions & 8 deletions api/src/opentrons/protocol_api/instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -1222,7 +1222,6 @@ def home_plunger(self) -> InstrumentContext:
self._core.home_plunger()
return self

# TODO (spp, 2024-03-08): verify if ok to & change source & dest types to AdvancedLiquidHandling
@publisher.publish(command=cmds.distribute)
@requires_version(2, 0)
def distribute(
Expand Down Expand Up @@ -1262,7 +1261,6 @@ def distribute(

return self.transfer(volume, source, dest, **kwargs)

# TODO (spp, 2024-03-08): verify if ok to & change source & dest types to AdvancedLiquidHandling
@publisher.publish(command=cmds.consolidate)
@requires_version(2, 0)
def consolidate(
Expand Down Expand Up @@ -1511,11 +1509,11 @@ def _execute_transfer(self, plan: v1_transfer.TransferPlan) -> None:
def transfer_liquid(
self,
liquid_class: LiquidClass,
volume: Union[float, Sequence[float]],
volume: float,
source: AdvancedLiquidHandling,
dest: AdvancedLiquidHandling,
trash_location: Optional[Union[types.Location, TrashBin, WasteChute]],
new_tip: Literal["once", "always", "never"] = "once",
trash_location: Optional[Union[types.Location, TrashBin, WasteChute]] = None,
) -> InstrumentContext:
"""Transfer liquid from source to dest using the specified liquid class properties."""
if not feature_flags.allow_liquid_classes(
Expand Down Expand Up @@ -1561,21 +1559,26 @@ def transfer_liquid(
liquid_class_props = liquid_class.get_for(
pipette=self.name, tiprack=tiprack.name
)

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

transfer_steps = v2_transfer.get_transfer_steps(
v2_transfer.get_transfer_steps(
aspirate_properties=liquid_class_props.aspirate,
single_dispense_properties=liquid_class_props.dispense,
volume=volume,
source=flat_sources_list,
dest=flat_dest_list,
trash_location=trash_location,
trash_location=checked_trash_location,
new_tip=valid_new_tip,
)
return self

@requires_version(2, 0)
def delay(self, *args: Any, **kwargs: Any) -> None:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Common functions between v1 transfer and liquid-class-based transfer."""
import enum
from typing import Iterable, Generator, Tuple, TypeVar, Union, Sequence, TYPE_CHECKING
from typing import Iterable, Generator, Tuple, TypeVar


class TransferTipPolicyV2(enum.Enum):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,32 @@
"""Steps builder for transfer, consolidate and distribute using liquid class."""
import dataclasses
from typing import (
TYPE_CHECKING,
Union,
Sequence,
Optional,
Dict,
Any,
Generator,
Literal,
Sequence,
Union,
TYPE_CHECKING,
)

from opentrons.protocol_api._liquid_properties import (
AspirateProperties,
SingleDispenseProperties,
MultiDispenseProperties,
)
from opentrons import types
from .common import expand_for_volume_constraints, TransferTipPolicyV2
from opentrons.protocol_api.labware import Labware, Well
from .common import TransferTipPolicyV2

# from opentrons.protocol_api.labware import Labware, Well
#
if TYPE_CHECKING:
from opentrons.protocol_api import LiquidClass, TrashBin, WasteChute

AdvancedLiquidHandling = Union[
Well,
types.Location,
Sequence[Union[Well, types.Location]],
Sequence[Sequence[Well]],
]
from opentrons.protocol_api import TrashBin, WasteChute, Well, Labware
#
# AdvancedLiquidHandling = Union[
# Well,
# types.Location,
# Sequence[Union[Well, types.Location]],
# Sequence[Sequence[Well]],
# ]


@dataclasses.dataclass
Expand All @@ -41,10 +39,10 @@ def get_transfer_steps(
aspirate_properties: AspirateProperties,
single_dispense_properties: SingleDispenseProperties,
volume: float,
source: AdvancedLiquidHandling,
dest: AdvancedLiquidHandling,
trash_location: Union[types.Location, TrashBin, WasteChute],
source: Sequence[Union[Well, types.Location]],
dest: Sequence[Union[Well, types.Location]],
trash_location: Union[Labware, types.Location, TrashBin, WasteChute],
new_tip: TransferTipPolicyV2,
) -> Generator[TransferStep, None, None]:
) -> None:
"""Return the PAPI function steps to perform for this transfer."""
# TODO: check for valid volume params of disposal vol, air gap and max volume
183 changes: 182 additions & 1 deletion api/tests/opentrons/protocol_api/test_instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
from decoy import Decoy
from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped]

from opentrons.config import feature_flags as ff
from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError
from opentrons.protocol_engine.errors.error_occurrence import (
ProtocolCommandFailedError,
)

from opentrons.legacy_broker import LegacyBroker
from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2

from tests.opentrons.protocol_api.partial_tip_configurations import (
PipetteReliantNozzleConfigSpec,
Expand All @@ -42,6 +44,7 @@
Well,
labware,
validation as mock_validation,
LiquidClass,
)
from opentrons.protocol_api.validation import WellTarget, PointTarget
from opentrons.protocol_api.core.common import InstrumentCore, ProtocolCore
Expand All @@ -51,12 +54,16 @@

from opentrons.hardware_control.nozzle_manager import NozzleMap
from opentrons.protocol_api.disposal_locations import TrashBin, WasteChute
from opentrons_shared_data.pipette.pipette_definition import ValidNozzleMaps
from opentrons.types import Location, Mount, Point

from opentrons_shared_data.pipette.pipette_definition import ValidNozzleMaps
from opentrons_shared_data.errors.exceptions import (
CommandPreconditionViolated,
)
from opentrons_shared_data.liquid_classes.liquid_class_definition import (
LiquidClassSchemaV1,
)
from opentrons_shared_data.robot.types import RobotTypeEnum, RobotType
from . import versions_at_or_above, versions_between


Expand Down Expand Up @@ -1649,3 +1656,177 @@ def test_air_gap_uses_air_gap(

decoy.verify(mock_move_to(top_location, publish=False))
decoy.verify(mock_instrument_core.air_gap_in_place(10, 11))


@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"])
def test_transfer_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([mock_well])).then_raise(
ValueError("Oh no")
)
with pytest.raises(ValueError):
subject.transfer_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_transfer_liquid_raises_for_unequal_source_and_dest(
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 and destination are not of same length."""
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(mock_well)).then_return(
[mock_well, mock_well]
)
decoy.when(mock_validation.ensure_valid_flat_wells_list([mock_well])).then_return(
[mock_well]
)
with pytest.raises(
ValueError, match="Sources and destinations should be of the same length"
):
subject.transfer_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_transfer_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([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.transfer_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_transfer_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([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.transfer_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_transfer_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 no tip attached."""
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([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.transfer_liquid(
liquid_class=test_liq_class,
volume=10,
source=[mock_well],
dest=[mock_well],
new_tip="never",
)
8 changes: 0 additions & 8 deletions api/tests/opentrons/protocol_api/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
from opentrons_shared_data.pipette.types import PipetteNameType
from opentrons_shared_data.robot.types import RobotType

from opentrons.protocols.advanced_control.transfers.transfer_liquid import (
AdvancedLiquidHandling,
)
from opentrons.types import (
Mount,
DeckSlotName,
Expand All @@ -41,11 +38,6 @@
from opentrons.protocol_api import validation as subject, Well, Labware


def get_mock_well() -> Well:
"""Return a mocked out Well object."""
return decoy.mock(cls=Well)


@pytest.mark.parametrize(
["input_mount", "input_pipette", "expected"],
[
Expand Down

0 comments on commit 1ce8da6

Please sign in to comment.