diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index a8f28ffaab7..c1d2f554918 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -1083,7 +1083,7 @@ def select_tiprack_from_list( elif starting_point: first_well = starting_point else: - first_well = first.wells()[0] + first_well = None next_tip = first.next_tip(num_channels, first_well, nozzle_map) if next_tip: diff --git a/api/src/opentrons/protocol_engine/state/tips.py b/api/src/opentrons/protocol_engine/state/tips.py index 7da05b45116..2de48fa27c3 100644 --- a/api/src/opentrons/protocol_engine/state/tips.py +++ b/api/src/opentrons/protocol_engine/state/tips.py @@ -176,7 +176,7 @@ def get_next_tip( # noqa: C901 starting_tip_name: Optional[str], nozzle_map: Optional[NozzleMap], ) -> Optional[str]: - """Get the next available clean tip.""" + """Get the next available clean tip. Does not support use of a starting tip if the pipette used is in a partial configuration.""" wells = self._state.tips_by_labware_id.get(labware_id, {}) columns = self._state.column_by_labware_id.get(labware_id, []) @@ -215,10 +215,10 @@ def _identify_tip_cluster( else: return None tip_cluster.append(well) + if any(well not in [*wells] for well in tip_cluster): - # If wells from this cluster have been dropped, return None to search for the next cluster return None - # Return the list of tips to be analyzed + return tip_cluster def _validate_tip_cluster( @@ -269,7 +269,6 @@ def _cluster_search_A1(active_columns: int, active_rows: int) -> Optional[str]: active_columns, active_rows, tip_cluster ) if isinstance(result, str): - # The result is the critical tip to target return result if critical_row + active_rows < len(columns[0]): critical_row = critical_row + active_rows @@ -292,7 +291,6 @@ def _cluster_search_A12(active_columns: int, active_rows: int) -> Optional[str]: active_columns, active_rows, tip_cluster ) if isinstance(result, str): - # The result is the critical tip to target return result if critical_row + active_rows < len(columns[0]): critical_row = critical_row + active_rows @@ -315,7 +313,6 @@ def _cluster_search_H1(active_columns: int, active_rows: int) -> Optional[str]: active_columns, active_rows, tip_cluster ) if isinstance(result, str): - # The result is the critical tip to target return result if critical_row - active_rows > 0: critical_row = critical_row - active_rows @@ -338,7 +335,6 @@ def _cluster_search_H12(active_columns: int, active_rows: int) -> Optional[str]: active_columns, active_rows, tip_cluster ) if isinstance(result, str): - # The result is the critical tip to target return result if critical_row - active_rows > 0: critical_row = critical_row - active_rows @@ -351,29 +347,28 @@ def _cluster_search_H12(active_columns: int, active_rows: int) -> Optional[str]: num_channels = len(nozzle_map.full_instrument_map_store) num_nozzle_cols = len(nozzle_map.columns) num_nozzle_rows = len(nozzle_map.rows) + # Each pipette's cluster search is determined by the point of entry for a given pipette/configuration: + # - Single channel pipettes always search a tiprack top to bottom, left to right + # - Eight channel pipettes will begin at the top if the primary nozzle is H1 and at the bottom if + # it is A1. The eight channel will always progress across the columns left to right. + # - 96 Channel pipettes will begin in the corner opposite their primary/starting nozzle (if starting nozzle = A1, enter tiprack at H12) + # The 96 channel will then progress towards the opposite corner, either going up or down, left or right depending on configuration. + if num_channels == 1: - # for a single channel pipette, always begin at A1 on the tiprack return _cluster_search_A1(num_nozzle_cols, num_nozzle_rows) elif num_channels == 8: - # perform 8 channel logic beginning from either H1 or A1 of the tiprack if nozzle_map.starting_nozzle == "A1": - # Define the critical well by the position of the well relative to Tip Rack entry point H1 return _cluster_search_H1(num_nozzle_cols, num_nozzle_rows) elif nozzle_map.starting_nozzle == "H1": - # Define the critical well by the position of the well relative to Tip Rack entry point A1 return _cluster_search_A1(num_nozzle_cols, num_nozzle_rows) elif num_channels == 96: if nozzle_map.starting_nozzle == "A1": - # Define the critical well by the position of the well relative to Tip Rack entry point H12 return _cluster_search_H12(num_nozzle_cols, num_nozzle_rows) elif nozzle_map.starting_nozzle == "A12": - # Define the critical well by the position of the well relative to Tip Rack entry point H1 return _cluster_search_H1(num_nozzle_cols, num_nozzle_rows) elif nozzle_map.starting_nozzle == "H1": - # Define the critical well by the position of the well relative to Tip Rack entry point A12 return _cluster_search_A12(num_nozzle_cols, num_nozzle_rows) elif nozzle_map.starting_nozzle == "H12": - # Define the critical well by the position of the well relative to Tip Rack entry point A1 return _cluster_search_A1(num_nozzle_cols, num_nozzle_rows) else: raise ValueError( diff --git a/api/tests/opentrons/protocol_engine/state/test_tip_state.py b/api/tests/opentrons/protocol_engine/state/test_tip_state.py index ced1e92d2ff..0c94f3806e8 100644 --- a/api/tests/opentrons/protocol_engine/state/test_tip_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_tip_state.py @@ -145,6 +145,8 @@ def test_get_next_tip_returns_none( nozzle_offset_z=1.23, home_position=4.56, nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), ), ) subject.handle_action( @@ -204,6 +206,8 @@ def test_get_next_tip_returns_first_tip( nozzle_offset_z=1.23, home_position=4.56, nozzle_map=get_default_nozzle_map(pipette_name_type), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), ), ) subject.handle_action( @@ -259,6 +263,8 @@ def test_get_next_tip_used_starting_tip( 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( @@ -308,6 +314,7 @@ def test_get_next_tip_skips_picked_up_tip( load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] result=commands.LoadPipetteResult(pipetteId="pipette-id") ) + channels_num = input_tip_amount if input_starting_tip is not None: pipette_name_type = PipetteNameType.P1000_96 if input_tip_amount == 1: @@ -316,28 +323,8 @@ def test_get_next_tip_skips_picked_up_tip( pipette_name_type = PipetteNameType.P300_MULTI_GEN2 else: pipette_name_type = PipetteNameType.P1000_96 - load_pipette_private_result = commands.LoadPipettePrivateResult( - pipette_id="pipette-id", - serial_number="pipette-serial", - config=LoadedStaticPipetteData( - channels=input_tip_amount, - 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(pipette_name_type), - ), - ) else: + channels_num = get_next_tip_tips pipette_name_type = PipetteNameType.P1000_96 if get_next_tip_tips == 1: pipette_name_type = PipetteNameType.P300_SINGLE_GEN2 @@ -345,27 +332,29 @@ def test_get_next_tip_skips_picked_up_tip( pipette_name_type = PipetteNameType.P300_MULTI_GEN2 else: pipette_name_type = PipetteNameType.P1000_96 - load_pipette_private_result = commands.LoadPipettePrivateResult( - pipette_id="pipette-id", - serial_number="pipette-serial", - config=LoadedStaticPipetteData( - channels=get_next_tip_tips, - 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(pipette_name_type), + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=channels_num, + 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(pipette_name_type), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), + ), + ) subject.handle_action( actions.UpdateCommandAction( private_result=load_pipette_private_result, command=load_pipette_command @@ -490,6 +479,8 @@ def test_get_next_tip_with_starting_tip_8_channel( 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( @@ -563,6 +554,8 @@ def test_get_next_tip_with_starting_tip_out_of_tips( 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( @@ -636,6 +629,8 @@ def test_get_next_tip_with_column_and_starting_tip( 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(