Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(api): Automatic tip tracking index out of range fix #15135

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions api/src/opentrons/protocol_engine/state/tips.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ def _cluster_search_A1(active_columns: int, active_rows: int) -> Optional[str]:
critical_column = active_columns - 1
critical_row = active_rows - 1

while critical_column <= len(columns):
while critical_column < len(columns):
tip_cluster = _identify_tip_cluster(
active_columns, active_rows, critical_column, critical_row, "A1"
)
Expand All @@ -312,7 +312,7 @@ def _cluster_search_A1(active_columns: int, active_rows: int) -> Optional[str]:
if critical_row + active_rows < len(columns[0]):
critical_row = critical_row + active_rows
else:
critical_column = critical_column + 1
critical_column += 1
critical_row = active_rows - 1
return None

Expand All @@ -336,7 +336,7 @@ def _cluster_search_A12(active_columns: int, active_rows: int) -> Optional[str]:
if critical_row + active_rows < len(columns[0]):
critical_row = critical_row + active_rows
else:
critical_column = critical_column - 1
critical_column -= 1
critical_row = active_rows - 1
return None

Expand All @@ -360,7 +360,9 @@ def _cluster_search_H1(active_columns: int, active_rows: int) -> Optional[str]:
if critical_row - active_rows >= 0:
critical_row = critical_row - active_rows
else:
critical_column = critical_column + 1
critical_column += 1
if critical_column >= len(columns):
return None
critical_row = len(columns[critical_column]) - active_rows
return None

Expand All @@ -384,7 +386,9 @@ def _cluster_search_H12(active_columns: int, active_rows: int) -> Optional[str]:
if critical_row - active_rows >= 0:
critical_row = critical_row - active_rows
else:
critical_column = critical_column - 1
critical_column -= 1
if critical_column < 0:
return None
critical_row = len(columns[critical_column]) - active_rows
return None

Expand Down
119 changes: 119 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 @@ -1135,3 +1135,122 @@ def _reconfigure_nozzle_layout(start: str, back_l: str, front_r: str) -> NozzleM
_assert_and_pickup("H1", map)
map = _reconfigure_nozzle_layout("A1", "A1", "A1")
_assert_and_pickup("B9", map)


def test_next_tip_automatic_tip_tracking_tiprack_limits(
subject: TipStore,
supported_tip_fixture: pipette_definition.SupportedTipsDefinition,
load_labware_command: commands.LoadLabware,
pick_up_tip_command: commands.PickUpTip,
) -> None:
"""Test tip tracking logic to ensure once a tiprack is consumed it returns None when consuming tips using multiple pipette configurations."""
# Load labware
subject.handle_action(
actions.SucceedCommandAction(private_result=None, command=load_labware_command)
)

# Load pipette
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=96,
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.P1000_96),
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
)
)

def _get_next_and_pickup(nozzle_map: NozzleMap) -> str | None:
result = TipView(subject.state).get_next_tip(
labware_id="cool-labware",
num_tips=0,
starting_tip_name=None,
nozzle_map=nozzle_map,
)
if result is not None:
pick_up_tip = commands.PickUpTip.construct( # type: ignore[call-arg]
params=commands.PickUpTipParams.construct(
pipetteId="pipette-id",
labwareId="cool-labware",
wellName=result,
),
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_tip)
)

return result

# Configure nozzle for partial configurations
configure_nozzle_layout_cmd = commands.ConfigureNozzleLayout.construct( # type: ignore[call-arg]
result=commands.ConfigureNozzleLayoutResult()
)

def _reconfigure_nozzle_layout(start: str, back_l: str, front_r: str) -> NozzleMap:
configure_nozzle_private_result = commands.ConfigureNozzleLayoutPrivateResult(
pipette_id="pipette-id",
nozzle_map=NozzleMap.build(
physical_nozzles=NINETY_SIX_MAP,
physical_rows=NINETY_SIX_ROWS,
physical_columns=NINETY_SIX_COLS,
starting_nozzle=start,
back_left_nozzle=back_l,
front_right_nozzle=front_r,
),
)
subject.handle_action(
actions.SucceedCommandAction(
private_result=configure_nozzle_private_result,
command=configure_nozzle_layout_cmd,
)
)
return configure_nozzle_private_result.nozzle_map

map = _reconfigure_nozzle_layout("A1", "A1", "A1")
for x in range(96):
_get_next_and_pickup(map)
assert _get_next_and_pickup(map) is None

subject.handle_action(actions.ResetTipsAction(labware_id="cool-labware"))
map = _reconfigure_nozzle_layout("A12", "A12", "A12")
for x in range(96):
_get_next_and_pickup(map)
assert _get_next_and_pickup(map) is None

subject.handle_action(actions.ResetTipsAction(labware_id="cool-labware"))
map = _reconfigure_nozzle_layout("H1", "H1", "H1")
for x in range(96):
_get_next_and_pickup(map)
assert _get_next_and_pickup(map) is None

subject.handle_action(actions.ResetTipsAction(labware_id="cool-labware"))
map = _reconfigure_nozzle_layout("H12", "H12", "H12")
for x in range(96):
_get_next_and_pickup(map)
assert _get_next_and_pickup(map) is None
Loading