Skip to content

Commit 0c40f7d

Browse files
authored
fix(api): Automatic tip tracking index out of range fix (#15135)
RQA-2700 Ensure automatic tip tracking for partial configurations does not exceed the limits of the tiprack it is iterating over
1 parent ed09d54 commit 0c40f7d

File tree

2 files changed

+128
-5
lines changed

2 files changed

+128
-5
lines changed

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ def _cluster_search_A1(active_columns: int, active_rows: int) -> Optional[str]:
297297
critical_column = active_columns - 1
298298
critical_row = active_rows - 1
299299

300-
while critical_column <= len(columns):
300+
while critical_column < len(columns):
301301
tip_cluster = _identify_tip_cluster(
302302
active_columns, active_rows, critical_column, critical_row, "A1"
303303
)
@@ -312,7 +312,7 @@ def _cluster_search_A1(active_columns: int, active_rows: int) -> Optional[str]:
312312
if critical_row + active_rows < len(columns[0]):
313313
critical_row = critical_row + active_rows
314314
else:
315-
critical_column = critical_column + 1
315+
critical_column += 1
316316
critical_row = active_rows - 1
317317
return None
318318

@@ -336,7 +336,7 @@ def _cluster_search_A12(active_columns: int, active_rows: int) -> Optional[str]:
336336
if critical_row + active_rows < len(columns[0]):
337337
critical_row = critical_row + active_rows
338338
else:
339-
critical_column = critical_column - 1
339+
critical_column -= 1
340340
critical_row = active_rows - 1
341341
return None
342342

@@ -360,7 +360,9 @@ def _cluster_search_H1(active_columns: int, active_rows: int) -> Optional[str]:
360360
if critical_row - active_rows >= 0:
361361
critical_row = critical_row - active_rows
362362
else:
363-
critical_column = critical_column + 1
363+
critical_column += 1
364+
if critical_column >= len(columns):
365+
return None
364366
critical_row = len(columns[critical_column]) - active_rows
365367
return None
366368

@@ -384,7 +386,9 @@ def _cluster_search_H12(active_columns: int, active_rows: int) -> Optional[str]:
384386
if critical_row - active_rows >= 0:
385387
critical_row = critical_row - active_rows
386388
else:
387-
critical_column = critical_column - 1
389+
critical_column -= 1
390+
if critical_column < 0:
391+
return None
388392
critical_row = len(columns[critical_column]) - active_rows
389393
return None
390394

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

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,3 +1135,122 @@ def _reconfigure_nozzle_layout(start: str, back_l: str, front_r: str) -> NozzleM
11351135
_assert_and_pickup("H1", map)
11361136
map = _reconfigure_nozzle_layout("A1", "A1", "A1")
11371137
_assert_and_pickup("B9", map)
1138+
1139+
1140+
def test_next_tip_automatic_tip_tracking_tiprack_limits(
1141+
subject: TipStore,
1142+
supported_tip_fixture: pipette_definition.SupportedTipsDefinition,
1143+
load_labware_command: commands.LoadLabware,
1144+
pick_up_tip_command: commands.PickUpTip,
1145+
) -> None:
1146+
"""Test tip tracking logic to ensure once a tiprack is consumed it returns None when consuming tips using multiple pipette configurations."""
1147+
# Load labware
1148+
subject.handle_action(
1149+
actions.SucceedCommandAction(private_result=None, command=load_labware_command)
1150+
)
1151+
1152+
# Load pipette
1153+
load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg]
1154+
result=commands.LoadPipetteResult(pipetteId="pipette-id")
1155+
)
1156+
load_pipette_private_result = commands.LoadPipettePrivateResult(
1157+
pipette_id="pipette-id",
1158+
serial_number="pipette-serial",
1159+
config=LoadedStaticPipetteData(
1160+
channels=96,
1161+
max_volume=15,
1162+
min_volume=3,
1163+
model="gen a",
1164+
display_name="display name",
1165+
flow_rates=FlowRates(
1166+
default_aspirate={},
1167+
default_dispense={},
1168+
default_blow_out={},
1169+
),
1170+
tip_configuration_lookup_table={15: supported_tip_fixture},
1171+
nominal_tip_overlap={},
1172+
nozzle_offset_z=1.23,
1173+
home_position=4.56,
1174+
nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96),
1175+
back_left_corner_offset=Point(x=1, y=2, z=3),
1176+
front_right_corner_offset=Point(x=4, y=5, z=6),
1177+
),
1178+
)
1179+
subject.handle_action(
1180+
actions.SucceedCommandAction(
1181+
private_result=load_pipette_private_result, command=load_pipette_command
1182+
)
1183+
)
1184+
1185+
def _get_next_and_pickup(nozzle_map: NozzleMap) -> str | None:
1186+
result = TipView(subject.state).get_next_tip(
1187+
labware_id="cool-labware",
1188+
num_tips=0,
1189+
starting_tip_name=None,
1190+
nozzle_map=nozzle_map,
1191+
)
1192+
if result is not None:
1193+
pick_up_tip = commands.PickUpTip.construct( # type: ignore[call-arg]
1194+
params=commands.PickUpTipParams.construct(
1195+
pipetteId="pipette-id",
1196+
labwareId="cool-labware",
1197+
wellName=result,
1198+
),
1199+
result=commands.PickUpTipResult.construct(
1200+
position=DeckPoint(x=0, y=0, z=0), tipLength=1.23
1201+
),
1202+
)
1203+
1204+
subject.handle_action(
1205+
actions.SucceedCommandAction(private_result=None, command=pick_up_tip)
1206+
)
1207+
1208+
return result
1209+
1210+
# Configure nozzle for partial configurations
1211+
configure_nozzle_layout_cmd = commands.ConfigureNozzleLayout.construct( # type: ignore[call-arg]
1212+
result=commands.ConfigureNozzleLayoutResult()
1213+
)
1214+
1215+
def _reconfigure_nozzle_layout(start: str, back_l: str, front_r: str) -> NozzleMap:
1216+
configure_nozzle_private_result = commands.ConfigureNozzleLayoutPrivateResult(
1217+
pipette_id="pipette-id",
1218+
nozzle_map=NozzleMap.build(
1219+
physical_nozzles=NINETY_SIX_MAP,
1220+
physical_rows=NINETY_SIX_ROWS,
1221+
physical_columns=NINETY_SIX_COLS,
1222+
starting_nozzle=start,
1223+
back_left_nozzle=back_l,
1224+
front_right_nozzle=front_r,
1225+
),
1226+
)
1227+
subject.handle_action(
1228+
actions.SucceedCommandAction(
1229+
private_result=configure_nozzle_private_result,
1230+
command=configure_nozzle_layout_cmd,
1231+
)
1232+
)
1233+
return configure_nozzle_private_result.nozzle_map
1234+
1235+
map = _reconfigure_nozzle_layout("A1", "A1", "A1")
1236+
for x in range(96):
1237+
_get_next_and_pickup(map)
1238+
assert _get_next_and_pickup(map) is None
1239+
1240+
subject.handle_action(actions.ResetTipsAction(labware_id="cool-labware"))
1241+
map = _reconfigure_nozzle_layout("A12", "A12", "A12")
1242+
for x in range(96):
1243+
_get_next_and_pickup(map)
1244+
assert _get_next_and_pickup(map) is None
1245+
1246+
subject.handle_action(actions.ResetTipsAction(labware_id="cool-labware"))
1247+
map = _reconfigure_nozzle_layout("H1", "H1", "H1")
1248+
for x in range(96):
1249+
_get_next_and_pickup(map)
1250+
assert _get_next_and_pickup(map) is None
1251+
1252+
subject.handle_action(actions.ResetTipsAction(labware_id="cool-labware"))
1253+
map = _reconfigure_nozzle_layout("H12", "H12", "H12")
1254+
for x in range(96):
1255+
_get_next_and_pickup(map)
1256+
assert _get_next_and_pickup(map) is None

0 commit comments

Comments
 (0)