opentrons 8.7.0a7__py3-none-any.whl → 8.7.0a9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of opentrons might be problematic. Click here for more details.
- opentrons/_version.py +2 -2
- opentrons/drivers/asyncio/communication/serial_connection.py +55 -129
- opentrons/drivers/flex_stacker/driver.py +6 -1
- opentrons/drivers/heater_shaker/abstract.py +0 -5
- opentrons/drivers/heater_shaker/driver.py +0 -10
- opentrons/drivers/heater_shaker/simulator.py +0 -4
- opentrons/drivers/thermocycler/abstract.py +0 -6
- opentrons/drivers/thermocycler/driver.py +10 -61
- opentrons/drivers/thermocycler/simulator.py +0 -6
- opentrons/hardware_control/api.py +5 -24
- opentrons/hardware_control/backends/controller.py +2 -8
- opentrons/hardware_control/backends/flex_protocol.py +1 -0
- opentrons/hardware_control/backends/ot3controller.py +3 -3
- opentrons/hardware_control/backends/ot3simulator.py +2 -2
- opentrons/hardware_control/backends/simulator.py +1 -2
- opentrons/hardware_control/backends/subsystem_manager.py +2 -5
- opentrons/hardware_control/emulation/abstract_emulator.py +4 -6
- opentrons/hardware_control/emulation/connection_handler.py +5 -8
- opentrons/hardware_control/emulation/heater_shaker.py +3 -12
- opentrons/hardware_control/emulation/settings.py +1 -1
- opentrons/hardware_control/emulation/thermocycler.py +15 -67
- opentrons/hardware_control/module_control.py +8 -82
- opentrons/hardware_control/modules/__init__.py +0 -3
- opentrons/hardware_control/modules/absorbance_reader.py +4 -11
- opentrons/hardware_control/modules/flex_stacker.py +9 -38
- opentrons/hardware_control/modules/heater_shaker.py +5 -42
- opentrons/hardware_control/modules/magdeck.py +4 -8
- opentrons/hardware_control/modules/mod_abc.py +5 -13
- opentrons/hardware_control/modules/tempdeck.py +5 -25
- opentrons/hardware_control/modules/thermocycler.py +11 -68
- opentrons/hardware_control/modules/types.py +1 -20
- opentrons/hardware_control/modules/utils.py +4 -11
- opentrons/hardware_control/nozzle_manager.py +0 -3
- opentrons/hardware_control/ot3api.py +7 -26
- opentrons/hardware_control/poller.py +8 -22
- opentrons/hardware_control/protocols/gripper_controller.py +1 -0
- opentrons/hardware_control/scripts/update_module_fw.py +0 -5
- opentrons/hardware_control/types.py +2 -31
- opentrons/legacy_commands/module_commands.py +0 -23
- opentrons/legacy_commands/protocol_commands.py +0 -20
- opentrons/legacy_commands/types.py +0 -80
- opentrons/motion_planning/deck_conflict.py +12 -17
- opentrons/motion_planning/waypoints.py +29 -15
- opentrons/protocol_api/__init__.py +1 -5
- opentrons/protocol_api/_types.py +1 -6
- opentrons/protocol_api/core/common.py +1 -3
- opentrons/protocol_api/core/engine/_default_labware_versions.py +11 -32
- opentrons/protocol_api/core/engine/labware.py +1 -8
- opentrons/protocol_api/core/engine/module_core.py +8 -75
- opentrons/protocol_api/core/engine/protocol.py +1 -18
- opentrons/protocol_api/core/engine/well.py +0 -8
- opentrons/protocol_api/core/legacy/legacy_module_core.py +4 -24
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -11
- opentrons/protocol_api/core/legacy/legacy_well_core.py +0 -4
- opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +2 -14
- opentrons/protocol_api/core/module.py +4 -37
- opentrons/protocol_api/core/protocol.py +2 -11
- opentrons/protocol_api/core/well.py +0 -4
- opentrons/protocol_api/labware.py +0 -5
- opentrons/protocol_api/module_contexts.py +61 -122
- opentrons/protocol_api/protocol_context.py +4 -26
- opentrons/protocol_api/robot_context.py +21 -38
- opentrons/protocol_api/validation.py +1 -6
- opentrons/protocol_engine/actions/__init__.py +2 -4
- opentrons/protocol_engine/actions/actions.py +9 -22
- opentrons/protocol_engine/clients/sync_client.py +7 -42
- opentrons/protocol_engine/commands/__init__.py +0 -42
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +15 -2
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +15 -2
- opentrons/protocol_engine/commands/aspirate.py +0 -1
- opentrons/protocol_engine/commands/command.py +0 -1
- opentrons/protocol_engine/commands/command_unions.py +0 -49
- opentrons/protocol_engine/commands/dispense.py +0 -1
- opentrons/protocol_engine/commands/drop_tip.py +8 -32
- opentrons/protocol_engine/commands/heater_shaker/__init__.py +0 -14
- opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +4 -5
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +5 -31
- opentrons/protocol_engine/commands/movement_common.py +0 -2
- opentrons/protocol_engine/commands/pick_up_tip.py +11 -21
- opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +7 -38
- opentrons/protocol_engine/commands/thermocycler/__init__.py +0 -16
- opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +0 -6
- opentrons/protocol_engine/commands/thermocycler/run_profile.py +0 -8
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +6 -40
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +5 -29
- opentrons/protocol_engine/commands/touch_tip.py +1 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +22 -6
- opentrons/protocol_engine/errors/__init__.py +0 -4
- opentrons/protocol_engine/errors/exceptions.py +0 -55
- opentrons/protocol_engine/execution/__init__.py +0 -2
- opentrons/protocol_engine/execution/command_executor.py +0 -8
- opentrons/protocol_engine/execution/create_queue_worker.py +1 -5
- opentrons/protocol_engine/execution/labware_movement.py +21 -10
- opentrons/protocol_engine/execution/movement.py +0 -2
- opentrons/protocol_engine/execution/queue_worker.py +0 -4
- opentrons/protocol_engine/execution/run_control.py +0 -8
- opentrons/protocol_engine/protocol_engine.py +34 -75
- opentrons/protocol_engine/resources/__init__.py +0 -2
- opentrons/protocol_engine/resources/deck_configuration_provider.py +0 -7
- opentrons/protocol_engine/resources/labware_validation.py +6 -10
- opentrons/protocol_engine/state/_labware_origin_math.py +636 -0
- opentrons/protocol_engine/state/_well_math.py +18 -60
- opentrons/protocol_engine/state/addressable_areas.py +0 -2
- opentrons/protocol_engine/state/commands.py +11 -14
- opentrons/protocol_engine/state/geometry.py +374 -213
- opentrons/protocol_engine/state/labware.py +102 -52
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +0 -37
- opentrons/protocol_engine/state/modules.py +8 -21
- opentrons/protocol_engine/state/motion.py +0 -44
- opentrons/protocol_engine/state/state.py +0 -14
- opentrons/protocol_engine/state/state_summary.py +0 -2
- opentrons/protocol_engine/state/tips.py +258 -177
- opentrons/protocol_engine/state/update_types.py +9 -16
- opentrons/protocol_engine/types/__init__.py +3 -9
- opentrons/protocol_engine/types/deck_configuration.py +1 -5
- opentrons/protocol_engine/types/instrument.py +1 -8
- opentrons/protocol_engine/types/labware.py +13 -1
- opentrons/protocol_engine/types/module.py +0 -10
- opentrons/protocol_engine/types/tip.py +0 -9
- opentrons/protocol_runner/create_simulating_orchestrator.py +2 -29
- opentrons/protocol_runner/run_orchestrator.py +2 -18
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +1 -2
- opentrons/simulate.py +15 -48
- opentrons/system/camera.py +1 -1
- {opentrons-8.7.0a7.dist-info → opentrons-8.7.0a9.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a7.dist-info → opentrons-8.7.0a9.dist-info}/RECORD +130 -146
- opentrons/protocol_api/core/engine/tasks.py +0 -48
- opentrons/protocol_api/core/legacy/tasks.py +0 -19
- opentrons/protocol_api/core/legacy_simulator/tasks.py +0 -19
- opentrons/protocol_api/core/tasks.py +0 -31
- opentrons/protocol_api/tasks.py +0 -48
- opentrons/protocol_engine/commands/create_timer.py +0 -83
- opentrons/protocol_engine/commands/heater_shaker/common.py +0 -20
- opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +0 -136
- opentrons/protocol_engine/commands/set_tip_state.py +0 -97
- opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +0 -191
- opentrons/protocol_engine/commands/wait_for_tasks.py +0 -98
- opentrons/protocol_engine/execution/task_handler.py +0 -157
- opentrons/protocol_engine/resources/concurrency_provider.py +0 -27
- opentrons/protocol_engine/state/labware_origin_math/errors.py +0 -94
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +0 -1331
- opentrons/protocol_engine/state/tasks.py +0 -139
- opentrons/protocol_engine/types/tasks.py +0 -38
- {opentrons-8.7.0a7.dist-info → opentrons-8.7.0a9.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a7.dist-info → opentrons-8.7.0a9.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a7.dist-info → opentrons-8.7.0a9.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
"""Tip state tracking."""
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Dict, Iterable, Optional, List, Union
|
|
5
6
|
|
|
6
|
-
from opentrons.types import NozzleMapInterface
|
|
7
|
+
from opentrons.types import NozzleMapInterface
|
|
7
8
|
from opentrons.protocol_engine.state import update_types
|
|
8
|
-
from opentrons.protocol_engine.types import TipRackWellState
|
|
9
9
|
|
|
10
10
|
from ._abstract_store import HasState, HandlesActions
|
|
11
|
-
from ._well_math import
|
|
12
|
-
|
|
13
|
-
wells_covered_by_pipette_configuration,
|
|
14
|
-
wells_covered_by_physical_pipette,
|
|
15
|
-
)
|
|
16
|
-
from ..actions import Action, get_state_updates
|
|
11
|
+
from ._well_math import wells_covered_dense
|
|
12
|
+
from ..actions import Action, ResetTipsAction, get_state_updates
|
|
17
13
|
|
|
18
14
|
from opentrons.hardware_control.nozzle_manager import NozzleMap
|
|
19
15
|
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
class _TipRackWellState(Enum):
|
|
18
|
+
"""The state of a single tip in a tip rack's well."""
|
|
19
|
+
|
|
20
|
+
CLEAN = "clean"
|
|
21
|
+
USED = "used"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_TipRackStateByWellName = Dict[str, _TipRackWellState]
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
@dataclass
|
|
@@ -46,12 +49,19 @@ class TipStore(HasState[TipState], HandlesActions):
|
|
|
46
49
|
for state_update in get_state_updates(action):
|
|
47
50
|
self._handle_state_update(state_update)
|
|
48
51
|
|
|
52
|
+
if isinstance(action, ResetTipsAction):
|
|
53
|
+
labware_id = action.labware_id
|
|
54
|
+
|
|
55
|
+
for well_name in self._state.tips_by_labware_id[labware_id].keys():
|
|
56
|
+
self._state.tips_by_labware_id[labware_id][
|
|
57
|
+
well_name
|
|
58
|
+
] = _TipRackWellState.CLEAN
|
|
59
|
+
|
|
49
60
|
def _handle_state_update(self, state_update: update_types.StateUpdate) -> None:
|
|
50
|
-
if state_update.
|
|
51
|
-
self.
|
|
52
|
-
labware_id=state_update.
|
|
53
|
-
well_names=state_update.
|
|
54
|
-
tip_state=state_update.tips_state.tip_state,
|
|
61
|
+
if state_update.tips_used != update_types.NO_CHANGE:
|
|
62
|
+
self._set_used_tips(
|
|
63
|
+
labware_id=state_update.tips_used.labware_id,
|
|
64
|
+
well_names=state_update.tips_used.well_names,
|
|
55
65
|
)
|
|
56
66
|
|
|
57
67
|
if state_update.loaded_labware != update_types.NO_CHANGE:
|
|
@@ -59,7 +69,7 @@ class TipStore(HasState[TipState], HandlesActions):
|
|
|
59
69
|
definition = state_update.loaded_labware.definition
|
|
60
70
|
if definition.parameters.isTiprack:
|
|
61
71
|
self._state.tips_by_labware_id[labware_id] = {
|
|
62
|
-
well_name:
|
|
72
|
+
well_name: _TipRackWellState.CLEAN
|
|
63
73
|
for column in definition.ordering
|
|
64
74
|
for well_name in column
|
|
65
75
|
}
|
|
@@ -73,7 +83,7 @@ class TipStore(HasState[TipState], HandlesActions):
|
|
|
73
83
|
]
|
|
74
84
|
if definition.parameters.isTiprack:
|
|
75
85
|
self._state.tips_by_labware_id[labware_id] = {
|
|
76
|
-
well_name:
|
|
86
|
+
well_name: _TipRackWellState.CLEAN
|
|
77
87
|
for column in definition.ordering
|
|
78
88
|
for well_name in column
|
|
79
89
|
}
|
|
@@ -81,12 +91,10 @@ class TipStore(HasState[TipState], HandlesActions):
|
|
|
81
91
|
column for column in definition.ordering
|
|
82
92
|
]
|
|
83
93
|
|
|
84
|
-
def
|
|
85
|
-
self, labware_id: str, well_names: Iterable[str], tip_state: TipRackWellState
|
|
86
|
-
) -> None:
|
|
94
|
+
def _set_used_tips(self, labware_id: str, well_names: Iterable[str]) -> None:
|
|
87
95
|
well_states = self._state.tips_by_labware_id.get(labware_id, {})
|
|
88
96
|
for well_name in well_names:
|
|
89
|
-
well_states[well_name] =
|
|
97
|
+
well_states[well_name] = _TipRackWellState.USED
|
|
90
98
|
|
|
91
99
|
|
|
92
100
|
class TipView:
|
|
@@ -109,15 +117,226 @@ class TipView:
|
|
|
109
117
|
starting_tip_name: Optional[str],
|
|
110
118
|
nozzle_map: Optional[NozzleMapInterface],
|
|
111
119
|
) -> Optional[str]:
|
|
112
|
-
"""
|
|
120
|
+
"""Get the next available clean tip. Does not support use of a starting tip if the pipette used is in a partial configuration."""
|
|
121
|
+
wells = self._state.tips_by_labware_id.get(labware_id, {})
|
|
122
|
+
columns = self._state.columns_by_labware_id.get(labware_id, [])
|
|
113
123
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
# TODO(sf): I'm pretty sure this can be replaced with wells_covered_96 but I'm not quite sure how
|
|
125
|
+
def _identify_tip_cluster(
|
|
126
|
+
active_columns: int,
|
|
127
|
+
active_rows: int,
|
|
128
|
+
critical_column: int,
|
|
129
|
+
critical_row: int,
|
|
130
|
+
entry_well: str,
|
|
131
|
+
) -> Optional[List[str]]:
|
|
132
|
+
tip_cluster: list[str] = []
|
|
133
|
+
for i in range(active_columns):
|
|
134
|
+
if entry_well == "A1" or entry_well == "H1":
|
|
135
|
+
if critical_column - i >= 0:
|
|
136
|
+
column = columns[critical_column - i]
|
|
137
|
+
else:
|
|
138
|
+
return None
|
|
139
|
+
elif entry_well == "A12" or entry_well == "H12":
|
|
140
|
+
if critical_column + i < len(columns):
|
|
141
|
+
column = columns[critical_column + i]
|
|
142
|
+
else:
|
|
143
|
+
return None
|
|
144
|
+
else:
|
|
145
|
+
raise ValueError(
|
|
146
|
+
f"Invalid entry well {entry_well} for tip cluster identification."
|
|
147
|
+
)
|
|
148
|
+
for j in range(active_rows):
|
|
149
|
+
if entry_well == "A1" or entry_well == "A12":
|
|
150
|
+
if critical_row - j >= 0:
|
|
151
|
+
well = column[critical_row - j]
|
|
152
|
+
else:
|
|
153
|
+
return None
|
|
154
|
+
elif entry_well == "H1" or entry_well == "H12":
|
|
155
|
+
if critical_row + j < len(column):
|
|
156
|
+
well = column[critical_row + j]
|
|
157
|
+
else:
|
|
158
|
+
return None
|
|
159
|
+
tip_cluster.append(well)
|
|
160
|
+
|
|
161
|
+
if any(well not in [*wells] for well in tip_cluster):
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
return tip_cluster
|
|
165
|
+
|
|
166
|
+
def _validate_tip_cluster(
|
|
167
|
+
active_columns: int, active_rows: int, tip_cluster: List[str]
|
|
168
|
+
) -> Union[str, int, None]:
|
|
169
|
+
if not any(wells[well] == _TipRackWellState.USED for well in tip_cluster):
|
|
170
|
+
return tip_cluster[0]
|
|
171
|
+
elif all(wells[well] == _TipRackWellState.USED for well in tip_cluster):
|
|
172
|
+
return None
|
|
173
|
+
else:
|
|
174
|
+
# 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
|
|
175
|
+
if nozzle_map is not None and nozzle_map.physical_nozzle_count == 8:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
# In the case of a 96ch we can attempt to index in by singular rows and columns assuming that indexed direction is safe
|
|
179
|
+
# The tip cluster list is ordered: Each row from a column in order by columns
|
|
180
|
+
tip_cluster_final_column: list[str] = []
|
|
181
|
+
for i in range(active_rows):
|
|
182
|
+
tip_cluster_final_column.append(
|
|
183
|
+
tip_cluster[((active_columns * active_rows) - 1) - i]
|
|
184
|
+
)
|
|
185
|
+
tip_cluster_final_row: list[str] = []
|
|
186
|
+
for i in range(active_columns):
|
|
187
|
+
tip_cluster_final_row.append(
|
|
188
|
+
tip_cluster[(active_rows - 1) + (i * active_rows)]
|
|
189
|
+
)
|
|
190
|
+
if all(
|
|
191
|
+
wells[well] == _TipRackWellState.USED
|
|
192
|
+
for well in tip_cluster_final_column
|
|
193
|
+
):
|
|
194
|
+
return None
|
|
195
|
+
elif all(
|
|
196
|
+
wells[well] == _TipRackWellState.USED
|
|
197
|
+
for well in tip_cluster_final_row
|
|
198
|
+
):
|
|
199
|
+
return None
|
|
200
|
+
else:
|
|
201
|
+
# Tiprack has no valid tip selection, cannot progress
|
|
202
|
+
return -1
|
|
203
|
+
|
|
204
|
+
# Search through the tiprack beginning at A1
|
|
205
|
+
def _cluster_search_A1(active_columns: int, active_rows: int) -> Optional[str]:
|
|
206
|
+
critical_column = active_columns - 1
|
|
207
|
+
critical_row = active_rows - 1
|
|
208
|
+
|
|
209
|
+
while critical_column < len(columns):
|
|
210
|
+
tip_cluster = _identify_tip_cluster(
|
|
211
|
+
active_columns, active_rows, critical_column, critical_row, "A1"
|
|
212
|
+
)
|
|
213
|
+
if tip_cluster is not None:
|
|
214
|
+
result = _validate_tip_cluster(
|
|
215
|
+
active_columns, active_rows, tip_cluster
|
|
216
|
+
)
|
|
217
|
+
if isinstance(result, str):
|
|
218
|
+
return result
|
|
219
|
+
elif isinstance(result, int) and result == -1:
|
|
220
|
+
return None
|
|
221
|
+
if critical_row + 1 < len(columns[0]):
|
|
222
|
+
critical_row = critical_row + 1
|
|
223
|
+
else:
|
|
224
|
+
critical_column += 1
|
|
225
|
+
critical_row = active_rows - 1
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
# Search through the tiprack beginning at A12
|
|
229
|
+
def _cluster_search_A12(active_columns: int, active_rows: int) -> Optional[str]:
|
|
230
|
+
critical_column = len(columns) - active_columns
|
|
231
|
+
critical_row = active_rows - 1
|
|
232
|
+
|
|
233
|
+
while critical_column >= 0:
|
|
234
|
+
tip_cluster = _identify_tip_cluster(
|
|
235
|
+
active_columns, active_rows, critical_column, critical_row, "A12"
|
|
236
|
+
)
|
|
237
|
+
if tip_cluster is not None:
|
|
238
|
+
result = _validate_tip_cluster(
|
|
239
|
+
active_columns, active_rows, tip_cluster
|
|
240
|
+
)
|
|
241
|
+
if isinstance(result, str):
|
|
242
|
+
return result
|
|
243
|
+
elif isinstance(result, int) and result == -1:
|
|
244
|
+
return None
|
|
245
|
+
if critical_row + 1 < len(columns[0]):
|
|
246
|
+
critical_row = critical_row + 1
|
|
247
|
+
else:
|
|
248
|
+
critical_column -= 1
|
|
249
|
+
critical_row = active_rows - 1
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
# Search through the tiprack beginning at H1
|
|
253
|
+
def _cluster_search_H1(active_columns: int, active_rows: int) -> Optional[str]:
|
|
254
|
+
critical_column = active_columns - 1
|
|
255
|
+
critical_row = len(columns[critical_column]) - active_rows
|
|
256
|
+
|
|
257
|
+
while critical_column <= len(columns): # change to max size of labware
|
|
258
|
+
tip_cluster = _identify_tip_cluster(
|
|
259
|
+
active_columns, active_rows, critical_column, critical_row, "H1"
|
|
260
|
+
)
|
|
261
|
+
if tip_cluster is not None:
|
|
262
|
+
result = _validate_tip_cluster(
|
|
263
|
+
active_columns, active_rows, tip_cluster
|
|
264
|
+
)
|
|
265
|
+
if isinstance(result, str):
|
|
266
|
+
return result
|
|
267
|
+
elif isinstance(result, int) and result == -1:
|
|
268
|
+
return None
|
|
269
|
+
if critical_row - 1 >= 0:
|
|
270
|
+
critical_row = critical_row - 1
|
|
271
|
+
else:
|
|
272
|
+
critical_column += 1
|
|
273
|
+
if critical_column >= len(columns):
|
|
274
|
+
return None
|
|
275
|
+
critical_row = len(columns[critical_column]) - active_rows
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
# Search through the tiprack beginning at H12
|
|
279
|
+
def _cluster_search_H12(active_columns: int, active_rows: int) -> Optional[str]:
|
|
280
|
+
critical_column = len(columns) - active_columns
|
|
281
|
+
critical_row = len(columns[critical_column]) - active_rows
|
|
282
|
+
|
|
283
|
+
while critical_column >= 0:
|
|
284
|
+
tip_cluster = _identify_tip_cluster(
|
|
285
|
+
active_columns, active_rows, critical_column, critical_row, "H12"
|
|
286
|
+
)
|
|
287
|
+
if tip_cluster is not None:
|
|
288
|
+
result = _validate_tip_cluster(
|
|
289
|
+
active_columns, active_rows, tip_cluster
|
|
290
|
+
)
|
|
291
|
+
if isinstance(result, str):
|
|
292
|
+
return result
|
|
293
|
+
elif isinstance(result, int) and result == -1:
|
|
294
|
+
return None
|
|
295
|
+
if critical_row - 1 >= 0:
|
|
296
|
+
critical_row = critical_row - 1
|
|
297
|
+
else:
|
|
298
|
+
critical_column -= 1
|
|
299
|
+
if critical_column < 0:
|
|
300
|
+
return None
|
|
301
|
+
critical_row = len(columns[critical_column]) - active_rows
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
if starting_tip_name is None and nozzle_map is not None and columns:
|
|
305
|
+
num_channels = nozzle_map.physical_nozzle_count
|
|
306
|
+
num_nozzle_cols = len(nozzle_map.columns)
|
|
307
|
+
num_nozzle_rows = len(nozzle_map.rows)
|
|
308
|
+
# Each pipette's cluster search is determined by the point of entry for a given pipette/configuration:
|
|
309
|
+
# - Single channel pipettes always search a tiprack top to bottom, left to right
|
|
310
|
+
# - Eight channel pipettes will begin at the top if the primary nozzle is H1 and at the bottom if
|
|
311
|
+
# it is A1. The eight channel will always progress across the columns left to right.
|
|
312
|
+
# - 96 Channel pipettes will begin in the corner opposite their primary/starting nozzle (if starting nozzle = A1, enter tiprack at H12)
|
|
313
|
+
# The 96 channel will then progress towards the opposite corner, either going up or down, left or right depending on configuration.
|
|
314
|
+
|
|
315
|
+
if num_channels == 1:
|
|
316
|
+
return _cluster_search_A1(num_nozzle_cols, num_nozzle_rows)
|
|
317
|
+
elif num_channels == 8:
|
|
318
|
+
if nozzle_map.starting_nozzle == "A1":
|
|
319
|
+
return _cluster_search_H1(num_nozzle_cols, num_nozzle_rows)
|
|
320
|
+
elif nozzle_map.starting_nozzle == "H1":
|
|
321
|
+
return _cluster_search_A1(num_nozzle_cols, num_nozzle_rows)
|
|
322
|
+
elif num_channels == 96:
|
|
323
|
+
if nozzle_map.starting_nozzle == "A1":
|
|
324
|
+
return _cluster_search_H12(num_nozzle_cols, num_nozzle_rows)
|
|
325
|
+
elif nozzle_map.starting_nozzle == "A12":
|
|
326
|
+
return _cluster_search_H1(num_nozzle_cols, num_nozzle_rows)
|
|
327
|
+
elif nozzle_map.starting_nozzle == "H1":
|
|
328
|
+
return _cluster_search_A12(num_nozzle_cols, num_nozzle_rows)
|
|
329
|
+
elif nozzle_map.starting_nozzle == "H12":
|
|
330
|
+
return _cluster_search_A1(num_nozzle_cols, num_nozzle_rows)
|
|
331
|
+
else:
|
|
332
|
+
raise ValueError(
|
|
333
|
+
f"Nozzle {nozzle_map.starting_nozzle} is an invalid starting tip for automatic tip pickup."
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
raise RuntimeError(
|
|
337
|
+
"Invalid number of channels for automatic tip tracking."
|
|
338
|
+
)
|
|
118
339
|
else:
|
|
119
|
-
wells = self._state.tips_by_labware_id.get(labware_id, {})
|
|
120
|
-
columns = self._state.columns_by_labware_id.get(labware_id, [])
|
|
121
340
|
if columns and num_tips == len(columns[0]): # Get next tips for 8-channel
|
|
122
341
|
column_head = [column[0] for column in columns]
|
|
123
342
|
starting_column_index = 0
|
|
@@ -131,15 +350,17 @@ class TipView:
|
|
|
131
350
|
starting_column_index = idx
|
|
132
351
|
|
|
133
352
|
for column in columns[starting_column_index:]:
|
|
134
|
-
if
|
|
353
|
+
if not any(
|
|
354
|
+
wells[well] == _TipRackWellState.USED for well in column
|
|
355
|
+
):
|
|
135
356
|
return column[0]
|
|
136
357
|
|
|
137
358
|
elif num_tips == len(wells.keys()): # Get next tips for 96 channel
|
|
138
359
|
if starting_tip_name and starting_tip_name != columns[0][0]:
|
|
139
360
|
return None
|
|
140
361
|
|
|
141
|
-
if
|
|
142
|
-
tip_state ==
|
|
362
|
+
if not any(
|
|
363
|
+
tip_state == _TipRackWellState.USED for tip_state in wells.values()
|
|
143
364
|
):
|
|
144
365
|
return next(iter(wells))
|
|
145
366
|
|
|
@@ -148,74 +369,10 @@ class TipView:
|
|
|
148
369
|
wells = _drop_wells_before_starting_tip(wells, starting_tip_name)
|
|
149
370
|
|
|
150
371
|
for well_name, tip_state in wells.items():
|
|
151
|
-
if tip_state ==
|
|
372
|
+
if tip_state == _TipRackWellState.CLEAN:
|
|
152
373
|
return well_name
|
|
153
374
|
return None
|
|
154
375
|
|
|
155
|
-
def _get_next_tip_with_nozzle_map(
|
|
156
|
-
self,
|
|
157
|
-
labware_id: str,
|
|
158
|
-
nozzle_map: NozzleMapInterface,
|
|
159
|
-
) -> Optional[str]:
|
|
160
|
-
"""Get the next available clean tip for given nozzle configuration if one can be found."""
|
|
161
|
-
tip_well_states = self._state.tips_by_labware_id.get(labware_id, {})
|
|
162
|
-
wells_by_columns = self._state.columns_by_labware_id.get(labware_id, [])
|
|
163
|
-
|
|
164
|
-
def _validate_wells(well_list: Set[str], target_well: str) -> bool:
|
|
165
|
-
# If we are not picking up the correct number of tips it's not valid
|
|
166
|
-
if len(well_list) != nozzle_map.tip_count:
|
|
167
|
-
return False
|
|
168
|
-
# If not all the tips we'll be picking up are clean it's not valid
|
|
169
|
-
target_well_states = [tip_well_states[well_name] for well_name in well_list]
|
|
170
|
-
if not all(state == TipRackWellState.CLEAN for state in target_well_states):
|
|
171
|
-
return False
|
|
172
|
-
# Since we know a full configuration will always produce zero non-active overlapping wells
|
|
173
|
-
# we can skip the following checks if it is a full configuration.
|
|
174
|
-
if nozzle_map.configuration != NozzleConfigurationType.FULL:
|
|
175
|
-
# If we have a partial configuration we need to ensure that any wells in the way are NOT present
|
|
176
|
-
wells_covered_physically = set(
|
|
177
|
-
wells_covered_by_physical_pipette(
|
|
178
|
-
nozzle_map=nozzle_map, # type: ignore[arg-type]
|
|
179
|
-
target_well=target_well,
|
|
180
|
-
labware_wells_by_column=wells_by_columns,
|
|
181
|
-
)
|
|
182
|
-
)
|
|
183
|
-
wells_in_way_well_state = [
|
|
184
|
-
tip_well_states[well_name]
|
|
185
|
-
for well_name in wells_covered_physically.difference(well_list)
|
|
186
|
-
]
|
|
187
|
-
# TODO(jbl 2025-08-25) this should be changed to ensure all these extra wells are EMPTY when further
|
|
188
|
-
# tip return work occurs
|
|
189
|
-
if any(
|
|
190
|
-
well_state == TipRackWellState.CLEAN
|
|
191
|
-
for well_state in wells_in_way_well_state
|
|
192
|
-
):
|
|
193
|
-
return False
|
|
194
|
-
|
|
195
|
-
return True
|
|
196
|
-
|
|
197
|
-
# Get an ordered list of wells to most efficiently search, depending on pipette configuration
|
|
198
|
-
target_well_list = _resolve_well_order(wells_by_columns, nozzle_map)
|
|
199
|
-
|
|
200
|
-
for well in target_well_list:
|
|
201
|
-
# If the target well/tip isn't clean, skip to the next one. This will be checked
|
|
202
|
-
# again in _validate_wells, but we can short circuit the following checks if this is False
|
|
203
|
-
if tip_well_states[well] != TipRackWellState.CLEAN:
|
|
204
|
-
continue
|
|
205
|
-
# Get list of all wells (i.e. tips) that would be covered by the active nozzles
|
|
206
|
-
targeted_wells = set(
|
|
207
|
-
wells_covered_by_pipette_configuration(
|
|
208
|
-
nozzle_map=nozzle_map, # type: ignore[arg-type]
|
|
209
|
-
target_well=well,
|
|
210
|
-
labware_wells_by_column=wells_by_columns,
|
|
211
|
-
)
|
|
212
|
-
)
|
|
213
|
-
# If we are picking up the correct number of tips, return that target well
|
|
214
|
-
if _validate_wells(targeted_wells, target_well=well):
|
|
215
|
-
return well
|
|
216
|
-
|
|
217
|
-
return None
|
|
218
|
-
|
|
219
376
|
def has_clean_tip(self, labware_id: str, well_name: str) -> bool:
|
|
220
377
|
"""Get whether a well in a labware has a clean tip.
|
|
221
378
|
|
|
@@ -230,12 +387,12 @@ class TipView:
|
|
|
230
387
|
tip_rack = self._state.tips_by_labware_id.get(labware_id)
|
|
231
388
|
well_state = tip_rack.get(well_name) if tip_rack else None
|
|
232
389
|
|
|
233
|
-
return well_state ==
|
|
390
|
+
return well_state == _TipRackWellState.CLEAN
|
|
234
391
|
|
|
235
|
-
def
|
|
392
|
+
def compute_tips_to_mark_as_used(
|
|
236
393
|
self, labware_id: str, well_name: str, nozzle_map: NozzleMap
|
|
237
394
|
) -> list[str]:
|
|
238
|
-
"""Compute which tips a hypothetical tip pickup
|
|
395
|
+
"""Compute which tips a hypothetical tip pickup should mark as "used".
|
|
239
396
|
|
|
240
397
|
Params:
|
|
241
398
|
labware_id: The labware ID of the tip rack.
|
|
@@ -246,15 +403,7 @@ class TipView:
|
|
|
246
403
|
The well names of all the tips that the operation will use.
|
|
247
404
|
"""
|
|
248
405
|
columns = self._state.columns_by_labware_id.get(labware_id, [])
|
|
249
|
-
return list(
|
|
250
|
-
wells_covered_dense(
|
|
251
|
-
nozzle_map.columns,
|
|
252
|
-
nozzle_map.rows,
|
|
253
|
-
nozzle_map.starting_nozzle,
|
|
254
|
-
well_name,
|
|
255
|
-
columns,
|
|
256
|
-
)
|
|
257
|
-
)
|
|
406
|
+
return list(wells_covered_dense(nozzle_map, well_name, columns))
|
|
258
407
|
|
|
259
408
|
|
|
260
409
|
def _drop_wells_before_starting_tip(
|
|
@@ -262,78 +411,10 @@ def _drop_wells_before_starting_tip(
|
|
|
262
411
|
) -> _TipRackStateByWellName:
|
|
263
412
|
"""Drop any wells that come before the starting tip and return the remaining ones after."""
|
|
264
413
|
seen_starting_well = False
|
|
265
|
-
remaining_wells: dict[str,
|
|
414
|
+
remaining_wells: dict[str, _TipRackWellState] = {}
|
|
266
415
|
for well_name, tip_state in wells.items():
|
|
267
416
|
if well_name == starting_tip_name:
|
|
268
417
|
seen_starting_well = True
|
|
269
418
|
if seen_starting_well:
|
|
270
419
|
remaining_wells[well_name] = tip_state
|
|
271
420
|
return remaining_wells
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def _resolve_well_order( # noqa: C901
|
|
275
|
-
well_list: List[List[str]], nozzle_map: NozzleMapInterface
|
|
276
|
-
) -> List[str]:
|
|
277
|
-
"""Given a list of ordered columns and pipette information, returns a flat list of wells ordered for tip pick up.
|
|
278
|
-
|
|
279
|
-
Wells can be ordered in four different ways:
|
|
280
|
-
- Top to bottom, left to right (A1, B1, ... A2, B2, ... G12, H12)
|
|
281
|
-
- Top to bottom, right to left (A12, B12, ... A11, B11, ... G1, H1)
|
|
282
|
-
- Bottom to top, left to right (H1, G1, ... H2, G2, ... B12, A12)
|
|
283
|
-
- Bottom to top, right to left (A12, B12, ... A11, B11, ... G1, H1)
|
|
284
|
-
|
|
285
|
-
- Full configurations (which will always cover a single channel) will go top to bottom, left to right.
|
|
286
|
-
- A partial 8-channel pipette configuration will always search left to right, starting at either top to bottom for
|
|
287
|
-
starting nozzle H1 or bottom to top for starting nozzle A1
|
|
288
|
-
- A partial 96-channel pipette configuration will always begin in the opposite corner of the starting nozzle
|
|
289
|
-
"""
|
|
290
|
-
if nozzle_map.configuration == NozzleConfigurationType.FULL:
|
|
291
|
-
return _get_top_to_bottom_left_to_right(well_list)
|
|
292
|
-
elif nozzle_map.physical_nozzle_count == 8:
|
|
293
|
-
if nozzle_map.starting_nozzle == "A1":
|
|
294
|
-
return _get_bottom_to_top_left_to_right(well_list)
|
|
295
|
-
elif nozzle_map.starting_nozzle == "H1":
|
|
296
|
-
return _get_top_to_bottom_left_to_right(well_list)
|
|
297
|
-
else:
|
|
298
|
-
raise ValueError(
|
|
299
|
-
f"Nozzle {nozzle_map.starting_nozzle} is an invalid starting tip for"
|
|
300
|
-
" 8-channel pipette automatic tip pickup."
|
|
301
|
-
)
|
|
302
|
-
elif nozzle_map.physical_nozzle_count == 96:
|
|
303
|
-
if nozzle_map.starting_nozzle == "A1":
|
|
304
|
-
return _get_bottom_to_top_right_to_left(well_list)
|
|
305
|
-
elif nozzle_map.starting_nozzle == "A12":
|
|
306
|
-
return _get_bottom_to_top_left_to_right(well_list)
|
|
307
|
-
elif nozzle_map.starting_nozzle == "H1":
|
|
308
|
-
return _get_top_to_bottom_right_to_left(well_list)
|
|
309
|
-
elif nozzle_map.starting_nozzle == "H12":
|
|
310
|
-
return _get_top_to_bottom_left_to_right(well_list)
|
|
311
|
-
else:
|
|
312
|
-
raise ValueError(
|
|
313
|
-
f"Nozzle {nozzle_map.starting_nozzle} is an invalid starting tip for 96-channel automatic tip pickup."
|
|
314
|
-
)
|
|
315
|
-
else:
|
|
316
|
-
raise ValueError(
|
|
317
|
-
f"Automatic tip pickup does not support {nozzle_map.physical_nozzle_count}-channel pipettes"
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
def _get_top_to_bottom_left_to_right(well_list: List[List[str]]) -> List[str]:
|
|
322
|
-
return [well for column in well_list for well in column]
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def _get_bottom_to_top_left_to_right(well_list: List[List[str]]) -> List[str]:
|
|
326
|
-
reverse_column_ordering = [list(reversed(column)) for column in well_list]
|
|
327
|
-
return [well for column in reverse_column_ordering for well in column]
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def _get_top_to_bottom_right_to_left(well_list: List[List[str]]) -> List[str]:
|
|
331
|
-
reverse_row_ordering = list(reversed(well_list))
|
|
332
|
-
return [well for column in reverse_row_ordering for well in column]
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
def _get_bottom_to_top_right_to_left(well_list: List[List[str]]) -> List[str]:
|
|
336
|
-
reverse_row_column_ordering = [
|
|
337
|
-
list(reversed(column)) for column in reversed(well_list)
|
|
338
|
-
]
|
|
339
|
-
return [well for column in reverse_row_column_ordering for well in column]
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import dataclasses
|
|
6
6
|
import enum
|
|
7
7
|
import typing
|
|
8
|
-
from typing_extensions import Self
|
|
8
|
+
from typing_extensions import Self, Optional
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
|
|
11
11
|
from opentrons.hardware_control.nozzle_manager import NozzleMap
|
|
@@ -15,7 +15,6 @@ from opentrons.protocol_engine.types import (
|
|
|
15
15
|
LabwareLocation,
|
|
16
16
|
OnLabwareLocation,
|
|
17
17
|
TipGeometry,
|
|
18
|
-
TipRackWellState,
|
|
19
18
|
AspiratedFluid,
|
|
20
19
|
LiquidClassRecord,
|
|
21
20
|
ABSMeasureMode,
|
|
@@ -236,10 +235,8 @@ class PipetteAspirateReadyUpdate:
|
|
|
236
235
|
|
|
237
236
|
|
|
238
237
|
@dataclasses.dataclass
|
|
239
|
-
class
|
|
240
|
-
"""Represents an update that marks tips in a tip rack as
|
|
241
|
-
|
|
242
|
-
tip_state: TipRackWellState
|
|
238
|
+
class TipsUsedUpdate:
|
|
239
|
+
"""Represents an update that marks tips in a tip rack as used."""
|
|
243
240
|
|
|
244
241
|
labware_id: str
|
|
245
242
|
"""The labware ID of the tip rack."""
|
|
@@ -416,7 +413,7 @@ class LoadModuleUpdate:
|
|
|
416
413
|
definition: ModuleDefinition
|
|
417
414
|
slot_name: DeckSlotName
|
|
418
415
|
requested_model: ModuleModel
|
|
419
|
-
serial_number:
|
|
416
|
+
serial_number: Optional[str]
|
|
420
417
|
|
|
421
418
|
|
|
422
419
|
@dataclasses.dataclass
|
|
@@ -455,7 +452,7 @@ class StateUpdate:
|
|
|
455
452
|
|
|
456
453
|
labware_lid: LabwareLidUpdate | NoChangeType = NO_CHANGE
|
|
457
454
|
|
|
458
|
-
|
|
455
|
+
tips_used: TipsUsedUpdate | NoChangeType = NO_CHANGE
|
|
459
456
|
|
|
460
457
|
liquid_loaded: LiquidLoadedUpdate | NoChangeType = NO_CHANGE
|
|
461
458
|
|
|
@@ -685,7 +682,7 @@ class StateUpdate:
|
|
|
685
682
|
definition: ModuleDefinition,
|
|
686
683
|
slot_name: DeckSlotName,
|
|
687
684
|
requested_model: ModuleModel,
|
|
688
|
-
serial_number:
|
|
685
|
+
serial_number: Optional[str],
|
|
689
686
|
) -> Self:
|
|
690
687
|
"""Add a new module to state. See `LoadModuleUpdate`."""
|
|
691
688
|
self.loaded_module = LoadModuleUpdate(
|
|
@@ -732,13 +729,9 @@ class StateUpdate:
|
|
|
732
729
|
)
|
|
733
730
|
return self
|
|
734
731
|
|
|
735
|
-
def
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
"""Marks tips in a tip rack to provided tip state. See `TipsStateUpdate`."""
|
|
739
|
-
self.tips_state = TipsStateUpdate(
|
|
740
|
-
tip_state=tip_state, labware_id=labware_id, well_names=well_names
|
|
741
|
-
)
|
|
732
|
+
def mark_tips_as_used(self: Self, labware_id: str, well_names: list[str]) -> Self:
|
|
733
|
+
"""Mark tips in a tip rack as used. See `TipsUsedUpdate`."""
|
|
734
|
+
self.tips_used = TipsUsedUpdate(labware_id=labware_id, well_names=well_names)
|
|
742
735
|
return self
|
|
743
736
|
|
|
744
737
|
def set_liquid_loaded(
|
|
@@ -100,6 +100,7 @@ from .labware import (
|
|
|
100
100
|
LegacyLabwareOffsetCreate,
|
|
101
101
|
LabwareOffsetCreateInternal,
|
|
102
102
|
LoadedLabware,
|
|
103
|
+
LabwareParentDefinition,
|
|
103
104
|
LabwareWellId,
|
|
104
105
|
GripSpecs,
|
|
105
106
|
)
|
|
@@ -131,7 +132,6 @@ from .instrument import (
|
|
|
131
132
|
CurrentWell,
|
|
132
133
|
CurrentPipetteLocation,
|
|
133
134
|
InstrumentOffsetVector,
|
|
134
|
-
GripperMoveType,
|
|
135
135
|
)
|
|
136
136
|
from .execution import EngineStatus, PostRunHardwareState
|
|
137
137
|
from .liquid_level_detection import (
|
|
@@ -145,10 +145,9 @@ from .liquid_level_detection import (
|
|
|
145
145
|
)
|
|
146
146
|
from .liquid_handling import FlowRates
|
|
147
147
|
from .labware_movement import LabwareMovementStrategy, LabwareMovementOffsetData
|
|
148
|
-
from .tip import TipGeometry
|
|
148
|
+
from .tip import TipGeometry
|
|
149
149
|
from .hardware_passthrough import MovementAxis, MotorAxis
|
|
150
150
|
from .util import Vec3f, Dimensions
|
|
151
|
-
from .tasks import Task, TaskSummary, FinishedTask
|
|
152
151
|
|
|
153
152
|
__all__ = [
|
|
154
153
|
# Runtime parameters
|
|
@@ -257,6 +256,7 @@ __all__ = [
|
|
|
257
256
|
"LabwareOffsetCreateInternal",
|
|
258
257
|
"LoadedLabware",
|
|
259
258
|
"LabwareOffsetVector",
|
|
259
|
+
"LabwareParentDefinition",
|
|
260
260
|
"LabwareWellId",
|
|
261
261
|
"GripSpecs",
|
|
262
262
|
# Liquids
|
|
@@ -286,7 +286,6 @@ __all__ = [
|
|
|
286
286
|
"CurrentWell",
|
|
287
287
|
"CurrentPipetteLocation",
|
|
288
288
|
"InstrumentOffsetVector",
|
|
289
|
-
"GripperMoveType",
|
|
290
289
|
# Liquid level detection types
|
|
291
290
|
"LoadedVolumeInfo",
|
|
292
291
|
"ProbedHeightInfo",
|
|
@@ -302,7 +301,6 @@ __all__ = [
|
|
|
302
301
|
"LabwareMovementOffsetData",
|
|
303
302
|
# Tips
|
|
304
303
|
"TipGeometry",
|
|
305
|
-
"TipRackWellState",
|
|
306
304
|
# Hardware passthrough
|
|
307
305
|
"MovementAxis",
|
|
308
306
|
"MotorAxis",
|
|
@@ -311,8 +309,4 @@ __all__ = [
|
|
|
311
309
|
"Dimensions",
|
|
312
310
|
# Convenience re-export
|
|
313
311
|
"LabwareUri",
|
|
314
|
-
# Tasks
|
|
315
|
-
"Task",
|
|
316
|
-
"TaskSummary",
|
|
317
|
-
"FinishedTask",
|
|
318
312
|
]
|