Skip to content

Commit

Permalink
fix(api): validation casing for mixed tip use between single and eigh…
Browse files Browse the repository at this point in the history
…t channels (#15259)

Closes RQA-2780
Ensures the engine correctly identifies the next valid column for an 8ch pickup after a
sequence of single channel pickups.
  • Loading branch information
CaseyBatten authored May 23, 2024
1 parent 21572f0 commit 2afa859
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 0 deletions.
8 changes: 8 additions & 0 deletions api/src/opentrons/protocol_engine/state/tips.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,14 @@ def _validate_tip_cluster(
elif all(wells[well] == TipRackWellState.USED for well in tip_cluster):
return None
else:
# In the case of an 8ch pipette where a column has mixed state tips we may simply progress to the next column in our search
if (
nozzle_map is not None
and len(nozzle_map.full_instrument_map_store) == 8
):
return None

# In the case of a 96ch we can attempt to index in by singular rows and columns assuming that indexed direction is safe
# The tip cluster list is ordered: Each row from a column in order by columns
tip_cluster_final_column = []
for i in range(active_rows):
Expand Down
106 changes: 106 additions & 0 deletions api/tests/opentrons/protocol_engine/state/test_tip_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,112 @@ def test_get_next_tip_with_starting_tip_8_channel(
assert result == "A3"


def test_get_next_tip_with_1_channel_followed_by_8_channel(
subject: TipStore,
load_labware_command: commands.LoadLabware,
supported_tip_fixture: pipette_definition.SupportedTipsDefinition,
) -> None:
"""It should return the first tip of column 2 for the 8 channel after performing a single tip pickup on column 1."""
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(0, 0, 0),
front_right_corner_offset=Point(0, 0, 0),
),
)
subject.handle_action(
actions.SucceedCommandAction(
private_result=load_pipette_private_result, command=load_pipette_command
)
)
load_pipette_command2 = commands.LoadPipette.construct( # type: ignore[call-arg]
result=commands.LoadPipetteResult(pipetteId="pipette-id2")
)
load_pipette_private_result2 = commands.LoadPipettePrivateResult(
pipette_id="pipette-id2",
serial_number="pipette-serial2",
config=LoadedStaticPipetteData(
channels=8,
max_volume=15,
min_volume=3,
model="gen a",
display_name="display name2",
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_MULTI_GEN2),
back_left_corner_offset=Point(0, 0, 0),
front_right_corner_offset=Point(0, 0, 0),
),
)
subject.handle_action(
actions.SucceedCommandAction(
private_result=load_pipette_private_result2, command=load_pipette_command2
)
)

result = TipView(subject.state).get_next_tip(
labware_id="cool-labware",
num_tips=1,
starting_tip_name=None,
nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE_GEN2),
)

assert result == "A1"

pick_up_tip2 = commands.PickUpTip.construct( # type: ignore[call-arg]
params=commands.PickUpTipParams.construct(
pipetteId="pipette-id2",
labwareId="cool-labware",
wellName="A1",
),
result=commands.PickUpTipResult.construct(
position=DeckPoint(x=0, y=0, z=0), tipLength=1.23
),
)

subject.handle_action(
actions.SucceedCommandAction(private_result=None, command=pick_up_tip2)
)

result = TipView(subject.state).get_next_tip(
labware_id="cool-labware",
num_tips=8,
starting_tip_name=None,
nozzle_map=get_default_nozzle_map(PipetteNameType.P300_MULTI_GEN2),
)

assert result == "A2"


def test_get_next_tip_with_starting_tip_out_of_tips(
subject: TipStore,
load_labware_command: commands.LoadLabware,
Expand Down

0 comments on commit 2afa859

Please sign in to comment.