Skip to content

Commit 2afa859

Browse files
authored
fix(api): validation casing for mixed tip use between single and eight 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.
1 parent 21572f0 commit 2afa859

File tree

2 files changed

+114
-0
lines changed

2 files changed

+114
-0
lines changed

api/src/opentrons/protocol_engine/state/tips.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,14 @@ def _validate_tip_cluster(
267267
elif all(wells[well] == TipRackWellState.USED for well in tip_cluster):
268268
return None
269269
else:
270+
# 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
271+
if (
272+
nozzle_map is not None
273+
and len(nozzle_map.full_instrument_map_store) == 8
274+
):
275+
return None
276+
277+
# In the case of a 96ch we can attempt to index in by singular rows and columns assuming that indexed direction is safe
270278
# The tip cluster list is ordered: Each row from a column in order by columns
271279
tip_cluster_final_column = []
272280
for i in range(active_rows):

api/tests/opentrons/protocol_engine/state/test_tip_state.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,112 @@ def test_get_next_tip_with_starting_tip_8_channel(
519519
assert result == "A3"
520520

521521

522+
def test_get_next_tip_with_1_channel_followed_by_8_channel(
523+
subject: TipStore,
524+
load_labware_command: commands.LoadLabware,
525+
supported_tip_fixture: pipette_definition.SupportedTipsDefinition,
526+
) -> None:
527+
"""It should return the first tip of column 2 for the 8 channel after performing a single tip pickup on column 1."""
528+
subject.handle_action(
529+
actions.SucceedCommandAction(private_result=None, command=load_labware_command)
530+
)
531+
load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg]
532+
result=commands.LoadPipetteResult(pipetteId="pipette-id")
533+
)
534+
load_pipette_private_result = commands.LoadPipettePrivateResult(
535+
pipette_id="pipette-id",
536+
serial_number="pipette-serial",
537+
config=LoadedStaticPipetteData(
538+
channels=1,
539+
max_volume=15,
540+
min_volume=3,
541+
model="gen a",
542+
display_name="display name",
543+
flow_rates=FlowRates(
544+
default_aspirate={},
545+
default_dispense={},
546+
default_blow_out={},
547+
),
548+
tip_configuration_lookup_table={15: supported_tip_fixture},
549+
nominal_tip_overlap={},
550+
nozzle_offset_z=1.23,
551+
home_position=4.56,
552+
nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE_GEN2),
553+
back_left_corner_offset=Point(0, 0, 0),
554+
front_right_corner_offset=Point(0, 0, 0),
555+
),
556+
)
557+
subject.handle_action(
558+
actions.SucceedCommandAction(
559+
private_result=load_pipette_private_result, command=load_pipette_command
560+
)
561+
)
562+
load_pipette_command2 = commands.LoadPipette.construct( # type: ignore[call-arg]
563+
result=commands.LoadPipetteResult(pipetteId="pipette-id2")
564+
)
565+
load_pipette_private_result2 = commands.LoadPipettePrivateResult(
566+
pipette_id="pipette-id2",
567+
serial_number="pipette-serial2",
568+
config=LoadedStaticPipetteData(
569+
channels=8,
570+
max_volume=15,
571+
min_volume=3,
572+
model="gen a",
573+
display_name="display name2",
574+
flow_rates=FlowRates(
575+
default_aspirate={},
576+
default_dispense={},
577+
default_blow_out={},
578+
),
579+
tip_configuration_lookup_table={15: supported_tip_fixture},
580+
nominal_tip_overlap={},
581+
nozzle_offset_z=1.23,
582+
home_position=4.56,
583+
nozzle_map=get_default_nozzle_map(PipetteNameType.P300_MULTI_GEN2),
584+
back_left_corner_offset=Point(0, 0, 0),
585+
front_right_corner_offset=Point(0, 0, 0),
586+
),
587+
)
588+
subject.handle_action(
589+
actions.SucceedCommandAction(
590+
private_result=load_pipette_private_result2, command=load_pipette_command2
591+
)
592+
)
593+
594+
result = TipView(subject.state).get_next_tip(
595+
labware_id="cool-labware",
596+
num_tips=1,
597+
starting_tip_name=None,
598+
nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE_GEN2),
599+
)
600+
601+
assert result == "A1"
602+
603+
pick_up_tip2 = commands.PickUpTip.construct( # type: ignore[call-arg]
604+
params=commands.PickUpTipParams.construct(
605+
pipetteId="pipette-id2",
606+
labwareId="cool-labware",
607+
wellName="A1",
608+
),
609+
result=commands.PickUpTipResult.construct(
610+
position=DeckPoint(x=0, y=0, z=0), tipLength=1.23
611+
),
612+
)
613+
614+
subject.handle_action(
615+
actions.SucceedCommandAction(private_result=None, command=pick_up_tip2)
616+
)
617+
618+
result = TipView(subject.state).get_next_tip(
619+
labware_id="cool-labware",
620+
num_tips=8,
621+
starting_tip_name=None,
622+
nozzle_map=get_default_nozzle_map(PipetteNameType.P300_MULTI_GEN2),
623+
)
624+
625+
assert result == "A2"
626+
627+
522628
def test_get_next_tip_with_starting_tip_out_of_tips(
523629
subject: TipStore,
524630
load_labware_command: commands.LoadLabware,

0 commit comments

Comments
 (0)