Skip to content

Commit

Permalink
implement tip state handling
Browse files Browse the repository at this point in the history
  • Loading branch information
sfoster1 committed Apr 19, 2024
1 parent cac1158 commit 3e78dd3
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 14 deletions.
41 changes: 31 additions & 10 deletions api/src/opentrons/protocol_engine/state/tips.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from enum import Enum
from typing import Dict, Optional, List, Union

from opentrons_shared_data.labware.labware_definition import LabwareDefinition

from opentrons.hardware_control.nozzle_manager import NozzleMap

from .abstract_store import HasState, HandlesActions
from ..actions import (
Action,
Expand All @@ -13,6 +17,7 @@
from ..commands import (
Command,
LoadLabwareResult,
ReloadLabwareResult,
PickUpTip,
PickUpTipResult,
DropTipResult,
Expand All @@ -24,8 +29,6 @@
)
from ..error_recovery_policy import ErrorRecoveryType

from opentrons.hardware_control.nozzle_manager import NozzleMap


class TipRackWellState(Enum):
"""The state of a single tip in a tip rack's well."""
Expand Down Expand Up @@ -100,21 +103,24 @@ def handle_action(self, action: Action) -> None:
well_name
] = TipRackWellState.CLEAN

def _add_new_tiprack(self, labware_id: str, definition: LabwareDefinition) -> None:
self._state.tips_by_labware_id[labware_id] = {
well_name: TipRackWellState.CLEAN
for column in definition.ordering
for well_name in column
}
self._state.column_by_labware_id[labware_id] = [
column for column in definition.ordering
]

def _handle_succeeded_command(self, command: Command) -> None:
if (
isinstance(command.result, LoadLabwareResult)
and command.result.definition.parameters.isTiprack
):
labware_id = command.result.labwareId
definition = command.result.definition
self._state.tips_by_labware_id[labware_id] = {
well_name: TipRackWellState.CLEAN
for column in definition.ordering
for well_name in column
}
self._state.column_by_labware_id[labware_id] = [
column for column in definition.ordering
]
self._add_new_tiprack(labware_id, definition)

elif isinstance(command.result, PickUpTipResult):
labware_id = command.params.labwareId
Expand All @@ -130,6 +136,21 @@ def _handle_succeeded_command(self, command: Command) -> None:
pipette_id = command.params.pipetteId
self._state.length_by_pipette_id.pop(pipette_id, None)

elif isinstance(command.result, ReloadLabwareResult):
if (
command.result.definition.parameters.isTiprack
and command.result.labwareId not in self._state.tips_by_labware_id
):
self._add_new_tiprack(
command.result.labwareId, command.result.definition
)
elif (
not command.result.definition.parameters.isTiprack
and command.result.labwareId in self._state.tips_by_labware_id
):
self._state.tips_by_labware_id.pop(command.result.labwareId)
self._state.column_by_labware_id.pop(command.result.labwareId)

def _handle_failed_command(
self,
action: FailCommandAction,
Expand Down
186 changes: 182 additions & 4 deletions api/tests/opentrons/protocol_engine/state/test_tip_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ def subject() -> TipStore:
return TipStore()


@pytest.fixture
def labware_definition() -> LabwareDefinition:
"""Get a labware definition value object."""
def _default_labware_def(
parameters: Optional[LabwareParameters] = None,
) -> LabwareDefinition:
return LabwareDefinition.construct( # type: ignore[call-arg]
ordering=[
["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"],
Expand All @@ -54,10 +54,22 @@ def labware_definition() -> LabwareDefinition:
["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"],
["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"],
],
parameters=_tip_rack_parameters,
parameters=parameters or _tip_rack_parameters,
)


@pytest.fixture
def labware_definition() -> LabwareDefinition:
"""Get a labware definition value object."""
return _default_labware_def()


@pytest.fixture
def reload_labware_definition() -> LabwareDefinition:
"""Get a labware definition for reloading."""
return _default_labware_def()


@pytest.fixture
def load_labware_command(labware_definition: LabwareDefinition) -> commands.LoadLabware:
"""Get a load labware command value object."""
Expand All @@ -69,6 +81,18 @@ def load_labware_command(labware_definition: LabwareDefinition) -> commands.Load
)


@pytest.fixture
def reload_labware_command(
reload_labware_definition: LabwareDefinition,
) -> commands.ReloadLabware:
"""Get a reload labware command."""
return commands.ReloadLabware.construct( # type: ignore[call-arg]
result=commands.ReloadLabwareResult.construct(
labwareId="cool-labware", definition=reload_labware_definition
)
)


@pytest.fixture
def pick_up_tip_command() -> commands.PickUpTip:
"""Get a pick-up tip command value object."""
Expand Down Expand Up @@ -1135,3 +1159,157 @@ def _reconfigure_nozzle_layout(start: str, back_l: str, front_r: str) -> NozzleM
_assert_and_pickup("B1", map)
map = _reconfigure_nozzle_layout("A1", "A1", "A1")
_assert_and_pickup("B2", map)


def test_reload_tiprack_does_not_alter_tip_state(
subject: TipStore,
load_labware_command: commands.LoadLabware,
pick_up_tip_command: commands.PickUpTip,
reload_labware_command: commands.ReloadLabware,
supported_tip_fixture: pipette_definition.SupportedTipsDefinition,
) -> None:
"""Reloading a tiprack is not the same as resetting the tips."""
subject.handle_action(
actions.SucceedCommandAction(private_result=None, command=load_labware_command)
)
load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg]
result=commands.LoadPipetteResult(pipetteId="pipette-id")
)
load_pipette_private_result = commands.LoadPipettePrivateResult(
pipette_id="pipette-id",
serial_number="pipette-serial",
config=LoadedStaticPipetteData(
channels=1,
max_volume=15,
min_volume=3,
model="gen a",
display_name="display name",
flow_rates=FlowRates(
default_aspirate={},
default_dispense={},
default_blow_out={},
),
tip_configuration_lookup_table={15: supported_tip_fixture},
nominal_tip_overlap={},
nozzle_offset_z=1.23,
home_position=4.56,
nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE_GEN2),
back_left_corner_offset=Point(x=1, y=2, z=3),
front_right_corner_offset=Point(x=4, y=5, z=6),
),
)

subject.handle_action(
actions.SucceedCommandAction(
private_result=load_pipette_private_result, command=load_pipette_command
)
)

subject.handle_action(
actions.SucceedCommandAction(private_result=None, command=pick_up_tip_command)
)
subject.handle_action(
actions.SucceedCommandAction(
private_result=None, command=reload_labware_command
)
)
result = TipView(subject.state).get_next_tip(
labware_id="cool-labware",
num_tips=1,
starting_tip_name=None,
nozzle_map=None,
)

assert result == "B1"


@pytest.mark.parametrize(
"labware_definition",
[
_default_labware_def(
parameters=LabwareParameters.construct(isTiprack=False) # type: ignore[call-arg]
)
],
)
def test_reload_to_tiprack_loads(
subject: TipStore,
load_labware_command: commands.LoadLabware,
reload_labware_command: commands.ReloadLabware,
) -> None:
"""If you have a loaded non-tiprack and reload with a tiprack, state should appear."""
subject.handle_action(
actions.SucceedCommandAction(private_result=None, command=load_labware_command)
)
assert load_labware_command.result
assert (
TipView(subject.state).get_next_tip(
labware_id=load_labware_command.result.labwareId,
num_tips=1,
starting_tip_name=None,
nozzle_map=None,
)
is None
)

subject.handle_action(
actions.SucceedCommandAction(
private_result=None, command=reload_labware_command
)
)
result = TipView(subject.state).get_next_tip(
labware_id="cool-labware",
num_tips=1,
starting_tip_name=None,
nozzle_map=None,
)

assert result == "A1"


@pytest.mark.parametrize(
"reload_labware_definition",
[
_default_labware_def(
parameters=LabwareParameters.construct(isTiprack=False) # type: ignore[call-arg]
)
],
)
def test_reload_to_non_tiprack_unloads(
subject: TipStore,
load_labware_command: commands.LoadLabware,
reload_labware_command: commands.ReloadLabware,
) -> None:
"""If you have a loaded tiprack and reload with a non-tiprack def, state should go away."""
subject.handle_action(
actions.SucceedCommandAction(private_result=None, command=load_labware_command)
)
result = TipView(subject.state).get_next_tip(
labware_id="cool-labware",
num_tips=1,
starting_tip_name=None,
nozzle_map=None,
)
assert result == "A1"

subject.handle_action(
actions.SucceedCommandAction(
private_result=None, command=reload_labware_command
)
)
result = TipView(subject.state).get_next_tip(
labware_id="cool-labware",
num_tips=1,
starting_tip_name=None,
nozzle_map=None,
)
assert load_labware_command.result

assert (
TipView(subject.state).get_next_tip(
labware_id=load_labware_command.result.labwareId,
num_tips=1,
starting_tip_name=None,
nozzle_map=None,
)
is None
)

0 comments on commit 3e78dd3

Please sign in to comment.