opentrons 8.7.0a6__py3-none-any.whl → 8.7.0a7__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 +129 -52
- opentrons/drivers/heater_shaker/abstract.py +5 -0
- opentrons/drivers/heater_shaker/driver.py +10 -0
- opentrons/drivers/heater_shaker/simulator.py +4 -0
- opentrons/drivers/thermocycler/abstract.py +6 -0
- opentrons/drivers/thermocycler/driver.py +61 -10
- opentrons/drivers/thermocycler/simulator.py +6 -0
- opentrons/hardware_control/api.py +24 -5
- opentrons/hardware_control/backends/controller.py +8 -2
- opentrons/hardware_control/backends/ot3controller.py +3 -0
- opentrons/hardware_control/backends/ot3simulator.py +2 -1
- opentrons/hardware_control/backends/simulator.py +2 -1
- opentrons/hardware_control/backends/subsystem_manager.py +5 -2
- opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
- opentrons/hardware_control/emulation/connection_handler.py +8 -5
- opentrons/hardware_control/emulation/heater_shaker.py +12 -3
- opentrons/hardware_control/emulation/settings.py +1 -1
- opentrons/hardware_control/emulation/thermocycler.py +67 -15
- opentrons/hardware_control/module_control.py +82 -8
- opentrons/hardware_control/modules/__init__.py +3 -0
- opentrons/hardware_control/modules/absorbance_reader.py +11 -4
- opentrons/hardware_control/modules/flex_stacker.py +38 -9
- opentrons/hardware_control/modules/heater_shaker.py +42 -5
- opentrons/hardware_control/modules/magdeck.py +8 -4
- opentrons/hardware_control/modules/mod_abc.py +13 -5
- opentrons/hardware_control/modules/tempdeck.py +25 -5
- opentrons/hardware_control/modules/thermocycler.py +68 -11
- opentrons/hardware_control/modules/types.py +20 -1
- opentrons/hardware_control/modules/utils.py +11 -4
- opentrons/hardware_control/nozzle_manager.py +3 -0
- opentrons/hardware_control/ot3api.py +26 -5
- opentrons/hardware_control/poller.py +22 -8
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +31 -2
- opentrons/legacy_commands/module_commands.py +23 -0
- opentrons/legacy_commands/protocol_commands.py +20 -0
- opentrons/legacy_commands/types.py +80 -0
- opentrons/motion_planning/deck_conflict.py +17 -12
- opentrons/motion_planning/waypoints.py +15 -29
- opentrons/protocol_api/__init__.py +5 -1
- opentrons/protocol_api/_types.py +6 -1
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/_default_labware_versions.py +32 -11
- opentrons/protocol_api/core/engine/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +75 -8
- opentrons/protocol_api/core/engine/protocol.py +18 -1
- opentrons/protocol_api/core/engine/tasks.py +48 -0
- opentrons/protocol_api/core/engine/well.py +8 -0
- opentrons/protocol_api/core/legacy/legacy_module_core.py +24 -4
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +11 -1
- opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
- opentrons/protocol_api/core/legacy/tasks.py +19 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
- opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
- opentrons/protocol_api/core/module.py +37 -4
- opentrons/protocol_api/core/protocol.py +11 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/core/well.py +4 -0
- opentrons/protocol_api/labware.py +5 -0
- opentrons/protocol_api/module_contexts.py +117 -11
- opentrons/protocol_api/protocol_context.py +26 -4
- opentrons/protocol_api/robot_context.py +38 -21
- opentrons/protocol_api/tasks.py +48 -0
- opentrons/protocol_api/validation.py +6 -1
- opentrons/protocol_engine/actions/__init__.py +4 -2
- opentrons/protocol_engine/actions/actions.py +22 -9
- opentrons/protocol_engine/clients/sync_client.py +42 -7
- opentrons/protocol_engine/commands/__init__.py +42 -0
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
- opentrons/protocol_engine/commands/aspirate.py +1 -0
- opentrons/protocol_engine/commands/command.py +1 -0
- opentrons/protocol_engine/commands/command_unions.py +49 -0
- opentrons/protocol_engine/commands/create_timer.py +83 -0
- opentrons/protocol_engine/commands/dispense.py +1 -0
- opentrons/protocol_engine/commands/drop_tip.py +32 -8
- opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
- opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
- opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
- opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
- opentrons/protocol_engine/commands/movement_common.py +2 -0
- opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
- opentrons/protocol_engine/commands/set_tip_state.py +97 -0
- opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
- opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
- opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
- opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +40 -6
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +29 -5
- opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
- opentrons/protocol_engine/commands/touch_tip.py +1 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
- opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
- opentrons/protocol_engine/errors/__init__.py +4 -0
- opentrons/protocol_engine/errors/exceptions.py +55 -0
- opentrons/protocol_engine/execution/__init__.py +2 -0
- opentrons/protocol_engine/execution/command_executor.py +8 -0
- opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
- opentrons/protocol_engine/execution/labware_movement.py +9 -12
- opentrons/protocol_engine/execution/movement.py +2 -0
- opentrons/protocol_engine/execution/queue_worker.py +4 -0
- opentrons/protocol_engine/execution/run_control.py +8 -0
- opentrons/protocol_engine/execution/task_handler.py +157 -0
- opentrons/protocol_engine/protocol_engine.py +75 -34
- opentrons/protocol_engine/resources/__init__.py +2 -0
- opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
- opentrons/protocol_engine/resources/labware_validation.py +10 -6
- opentrons/protocol_engine/state/_well_math.py +60 -18
- opentrons/protocol_engine/state/addressable_areas.py +2 -0
- opentrons/protocol_engine/state/commands.py +14 -11
- opentrons/protocol_engine/state/geometry.py +213 -374
- opentrons/protocol_engine/state/labware.py +52 -102
- opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1331 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
- opentrons/protocol_engine/state/modules.py +21 -8
- opentrons/protocol_engine/state/motion.py +44 -0
- opentrons/protocol_engine/state/state.py +14 -0
- opentrons/protocol_engine/state/state_summary.py +2 -0
- opentrons/protocol_engine/state/tasks.py +139 -0
- opentrons/protocol_engine/state/tips.py +177 -258
- opentrons/protocol_engine/state/update_types.py +16 -9
- opentrons/protocol_engine/types/__init__.py +9 -3
- opentrons/protocol_engine/types/deck_configuration.py +5 -1
- opentrons/protocol_engine/types/instrument.py +8 -1
- opentrons/protocol_engine/types/labware.py +1 -13
- opentrons/protocol_engine/types/module.py +10 -0
- opentrons/protocol_engine/types/tasks.py +38 -0
- opentrons/protocol_engine/types/tip.py +9 -0
- opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
- opentrons/protocol_runner/run_orchestrator.py +18 -2
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +2 -1
- opentrons/simulate.py +48 -15
- opentrons/system/camera.py +1 -1
- {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/RECORD +143 -127
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
"""Tip state tracking."""
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from
|
|
5
|
-
from typing import Dict, Iterable, Optional, List, Union
|
|
4
|
+
from typing import Dict, Iterable, Optional, List, Set
|
|
6
5
|
|
|
7
|
-
from opentrons.types import NozzleMapInterface
|
|
6
|
+
from opentrons.types import NozzleMapInterface, NozzleConfigurationType
|
|
8
7
|
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
|
-
|
|
11
|
+
from ._well_math import (
|
|
12
|
+
wells_covered_dense,
|
|
13
|
+
wells_covered_by_pipette_configuration,
|
|
14
|
+
wells_covered_by_physical_pipette,
|
|
15
|
+
)
|
|
16
|
+
from ..actions import Action, get_state_updates
|
|
13
17
|
|
|
14
18
|
from opentrons.hardware_control.nozzle_manager import NozzleMap
|
|
15
19
|
|
|
16
20
|
|
|
17
|
-
|
|
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]
|
|
21
|
+
_TipRackStateByWellName = Dict[str, TipRackWellState]
|
|
25
22
|
|
|
26
23
|
|
|
27
24
|
@dataclass
|
|
@@ -49,19 +46,12 @@ class TipStore(HasState[TipState], HandlesActions):
|
|
|
49
46
|
for state_update in get_state_updates(action):
|
|
50
47
|
self._handle_state_update(state_update)
|
|
51
48
|
|
|
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
|
-
|
|
60
49
|
def _handle_state_update(self, state_update: update_types.StateUpdate) -> None:
|
|
61
|
-
if state_update.
|
|
62
|
-
self.
|
|
63
|
-
labware_id=state_update.
|
|
64
|
-
well_names=state_update.
|
|
50
|
+
if state_update.tips_state != update_types.NO_CHANGE:
|
|
51
|
+
self._set_tip_state(
|
|
52
|
+
labware_id=state_update.tips_state.labware_id,
|
|
53
|
+
well_names=state_update.tips_state.well_names,
|
|
54
|
+
tip_state=state_update.tips_state.tip_state,
|
|
65
55
|
)
|
|
66
56
|
|
|
67
57
|
if state_update.loaded_labware != update_types.NO_CHANGE:
|
|
@@ -69,7 +59,7 @@ class TipStore(HasState[TipState], HandlesActions):
|
|
|
69
59
|
definition = state_update.loaded_labware.definition
|
|
70
60
|
if definition.parameters.isTiprack:
|
|
71
61
|
self._state.tips_by_labware_id[labware_id] = {
|
|
72
|
-
well_name:
|
|
62
|
+
well_name: TipRackWellState.CLEAN
|
|
73
63
|
for column in definition.ordering
|
|
74
64
|
for well_name in column
|
|
75
65
|
}
|
|
@@ -83,7 +73,7 @@ class TipStore(HasState[TipState], HandlesActions):
|
|
|
83
73
|
]
|
|
84
74
|
if definition.parameters.isTiprack:
|
|
85
75
|
self._state.tips_by_labware_id[labware_id] = {
|
|
86
|
-
well_name:
|
|
76
|
+
well_name: TipRackWellState.CLEAN
|
|
87
77
|
for column in definition.ordering
|
|
88
78
|
for well_name in column
|
|
89
79
|
}
|
|
@@ -91,10 +81,12 @@ class TipStore(HasState[TipState], HandlesActions):
|
|
|
91
81
|
column for column in definition.ordering
|
|
92
82
|
]
|
|
93
83
|
|
|
94
|
-
def
|
|
84
|
+
def _set_tip_state(
|
|
85
|
+
self, labware_id: str, well_names: Iterable[str], tip_state: TipRackWellState
|
|
86
|
+
) -> None:
|
|
95
87
|
well_states = self._state.tips_by_labware_id.get(labware_id, {})
|
|
96
88
|
for well_name in well_names:
|
|
97
|
-
well_states[well_name] =
|
|
89
|
+
well_states[well_name] = tip_state
|
|
98
90
|
|
|
99
91
|
|
|
100
92
|
class TipView:
|
|
@@ -117,226 +109,15 @@ class TipView:
|
|
|
117
109
|
starting_tip_name: Optional[str],
|
|
118
110
|
nozzle_map: Optional[NozzleMapInterface],
|
|
119
111
|
) -> Optional[str]:
|
|
120
|
-
"""
|
|
121
|
-
wells = self._state.tips_by_labware_id.get(labware_id, {})
|
|
122
|
-
columns = self._state.columns_by_labware_id.get(labware_id, [])
|
|
112
|
+
"""Gets the next available clean tip.
|
|
123
113
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
)
|
|
114
|
+
Does not support use of a starting tip if the pipette used is in a partial configuration.
|
|
115
|
+
"""
|
|
116
|
+
if starting_tip_name is None and nozzle_map is not None:
|
|
117
|
+
return self._get_next_tip_with_nozzle_map(labware_id, nozzle_map)
|
|
339
118
|
else:
|
|
119
|
+
wells = self._state.tips_by_labware_id.get(labware_id, {})
|
|
120
|
+
columns = self._state.columns_by_labware_id.get(labware_id, [])
|
|
340
121
|
if columns and num_tips == len(columns[0]): # Get next tips for 8-channel
|
|
341
122
|
column_head = [column[0] for column in columns]
|
|
342
123
|
starting_column_index = 0
|
|
@@ -350,17 +131,15 @@ class TipView:
|
|
|
350
131
|
starting_column_index = idx
|
|
351
132
|
|
|
352
133
|
for column in columns[starting_column_index:]:
|
|
353
|
-
if
|
|
354
|
-
wells[well] == _TipRackWellState.USED for well in column
|
|
355
|
-
):
|
|
134
|
+
if all(wells[well] == TipRackWellState.CLEAN for well in column):
|
|
356
135
|
return column[0]
|
|
357
136
|
|
|
358
137
|
elif num_tips == len(wells.keys()): # Get next tips for 96 channel
|
|
359
138
|
if starting_tip_name and starting_tip_name != columns[0][0]:
|
|
360
139
|
return None
|
|
361
140
|
|
|
362
|
-
if
|
|
363
|
-
tip_state ==
|
|
141
|
+
if all(
|
|
142
|
+
tip_state == TipRackWellState.CLEAN for tip_state in wells.values()
|
|
364
143
|
):
|
|
365
144
|
return next(iter(wells))
|
|
366
145
|
|
|
@@ -369,10 +148,74 @@ class TipView:
|
|
|
369
148
|
wells = _drop_wells_before_starting_tip(wells, starting_tip_name)
|
|
370
149
|
|
|
371
150
|
for well_name, tip_state in wells.items():
|
|
372
|
-
if tip_state ==
|
|
151
|
+
if tip_state == TipRackWellState.CLEAN:
|
|
373
152
|
return well_name
|
|
374
153
|
return None
|
|
375
154
|
|
|
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
|
+
|
|
376
219
|
def has_clean_tip(self, labware_id: str, well_name: str) -> bool:
|
|
377
220
|
"""Get whether a well in a labware has a clean tip.
|
|
378
221
|
|
|
@@ -387,12 +230,12 @@ class TipView:
|
|
|
387
230
|
tip_rack = self._state.tips_by_labware_id.get(labware_id)
|
|
388
231
|
well_state = tip_rack.get(well_name) if tip_rack else None
|
|
389
232
|
|
|
390
|
-
return well_state ==
|
|
233
|
+
return well_state == TipRackWellState.CLEAN
|
|
391
234
|
|
|
392
|
-
def
|
|
235
|
+
def compute_tips_to_mark_as_used_or_empty(
|
|
393
236
|
self, labware_id: str, well_name: str, nozzle_map: NozzleMap
|
|
394
237
|
) -> list[str]:
|
|
395
|
-
"""Compute which tips a hypothetical tip pickup should mark as "used".
|
|
238
|
+
"""Compute which tips a hypothetical tip pickup/drop should mark as "used" or "empty".
|
|
396
239
|
|
|
397
240
|
Params:
|
|
398
241
|
labware_id: The labware ID of the tip rack.
|
|
@@ -403,7 +246,15 @@ class TipView:
|
|
|
403
246
|
The well names of all the tips that the operation will use.
|
|
404
247
|
"""
|
|
405
248
|
columns = self._state.columns_by_labware_id.get(labware_id, [])
|
|
406
|
-
return list(
|
|
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
|
+
)
|
|
407
258
|
|
|
408
259
|
|
|
409
260
|
def _drop_wells_before_starting_tip(
|
|
@@ -411,10 +262,78 @@ def _drop_wells_before_starting_tip(
|
|
|
411
262
|
) -> _TipRackStateByWellName:
|
|
412
263
|
"""Drop any wells that come before the starting tip and return the remaining ones after."""
|
|
413
264
|
seen_starting_well = False
|
|
414
|
-
remaining_wells: dict[str,
|
|
265
|
+
remaining_wells: dict[str, TipRackWellState] = {}
|
|
415
266
|
for well_name, tip_state in wells.items():
|
|
416
267
|
if well_name == starting_tip_name:
|
|
417
268
|
seen_starting_well = True
|
|
418
269
|
if seen_starting_well:
|
|
419
270
|
remaining_wells[well_name] = tip_state
|
|
420
271
|
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
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
|
|
11
11
|
from opentrons.hardware_control.nozzle_manager import NozzleMap
|
|
@@ -15,6 +15,7 @@ from opentrons.protocol_engine.types import (
|
|
|
15
15
|
LabwareLocation,
|
|
16
16
|
OnLabwareLocation,
|
|
17
17
|
TipGeometry,
|
|
18
|
+
TipRackWellState,
|
|
18
19
|
AspiratedFluid,
|
|
19
20
|
LiquidClassRecord,
|
|
20
21
|
ABSMeasureMode,
|
|
@@ -235,8 +236,10 @@ class PipetteAspirateReadyUpdate:
|
|
|
235
236
|
|
|
236
237
|
|
|
237
238
|
@dataclasses.dataclass
|
|
238
|
-
class
|
|
239
|
-
"""Represents an update that marks tips in a tip rack as
|
|
239
|
+
class TipsStateUpdate:
|
|
240
|
+
"""Represents an update that marks tips in a tip rack as the requested state."""
|
|
241
|
+
|
|
242
|
+
tip_state: TipRackWellState
|
|
240
243
|
|
|
241
244
|
labware_id: str
|
|
242
245
|
"""The labware ID of the tip rack."""
|
|
@@ -413,7 +416,7 @@ class LoadModuleUpdate:
|
|
|
413
416
|
definition: ModuleDefinition
|
|
414
417
|
slot_name: DeckSlotName
|
|
415
418
|
requested_model: ModuleModel
|
|
416
|
-
serial_number: Optional[str]
|
|
419
|
+
serial_number: typing.Optional[str]
|
|
417
420
|
|
|
418
421
|
|
|
419
422
|
@dataclasses.dataclass
|
|
@@ -452,7 +455,7 @@ class StateUpdate:
|
|
|
452
455
|
|
|
453
456
|
labware_lid: LabwareLidUpdate | NoChangeType = NO_CHANGE
|
|
454
457
|
|
|
455
|
-
|
|
458
|
+
tips_state: TipsStateUpdate | NoChangeType = NO_CHANGE
|
|
456
459
|
|
|
457
460
|
liquid_loaded: LiquidLoadedUpdate | NoChangeType = NO_CHANGE
|
|
458
461
|
|
|
@@ -682,7 +685,7 @@ class StateUpdate:
|
|
|
682
685
|
definition: ModuleDefinition,
|
|
683
686
|
slot_name: DeckSlotName,
|
|
684
687
|
requested_model: ModuleModel,
|
|
685
|
-
serial_number: Optional[str],
|
|
688
|
+
serial_number: typing.Optional[str],
|
|
686
689
|
) -> Self:
|
|
687
690
|
"""Add a new module to state. See `LoadModuleUpdate`."""
|
|
688
691
|
self.loaded_module = LoadModuleUpdate(
|
|
@@ -729,9 +732,13 @@ class StateUpdate:
|
|
|
729
732
|
)
|
|
730
733
|
return self
|
|
731
734
|
|
|
732
|
-
def
|
|
733
|
-
|
|
734
|
-
|
|
735
|
+
def update_tip_rack_well_state(
|
|
736
|
+
self: Self, tip_state: TipRackWellState, labware_id: str, well_names: list[str]
|
|
737
|
+
) -> Self:
|
|
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
|
+
)
|
|
735
742
|
return self
|
|
736
743
|
|
|
737
744
|
def set_liquid_loaded(
|
|
@@ -100,7 +100,6 @@ from .labware import (
|
|
|
100
100
|
LegacyLabwareOffsetCreate,
|
|
101
101
|
LabwareOffsetCreateInternal,
|
|
102
102
|
LoadedLabware,
|
|
103
|
-
LabwareParentDefinition,
|
|
104
103
|
LabwareWellId,
|
|
105
104
|
GripSpecs,
|
|
106
105
|
)
|
|
@@ -132,6 +131,7 @@ from .instrument import (
|
|
|
132
131
|
CurrentWell,
|
|
133
132
|
CurrentPipetteLocation,
|
|
134
133
|
InstrumentOffsetVector,
|
|
134
|
+
GripperMoveType,
|
|
135
135
|
)
|
|
136
136
|
from .execution import EngineStatus, PostRunHardwareState
|
|
137
137
|
from .liquid_level_detection import (
|
|
@@ -145,9 +145,10 @@ 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, TipRackWellState
|
|
149
149
|
from .hardware_passthrough import MovementAxis, MotorAxis
|
|
150
150
|
from .util import Vec3f, Dimensions
|
|
151
|
+
from .tasks import Task, TaskSummary, FinishedTask
|
|
151
152
|
|
|
152
153
|
__all__ = [
|
|
153
154
|
# Runtime parameters
|
|
@@ -256,7 +257,6 @@ __all__ = [
|
|
|
256
257
|
"LabwareOffsetCreateInternal",
|
|
257
258
|
"LoadedLabware",
|
|
258
259
|
"LabwareOffsetVector",
|
|
259
|
-
"LabwareParentDefinition",
|
|
260
260
|
"LabwareWellId",
|
|
261
261
|
"GripSpecs",
|
|
262
262
|
# Liquids
|
|
@@ -286,6 +286,7 @@ __all__ = [
|
|
|
286
286
|
"CurrentWell",
|
|
287
287
|
"CurrentPipetteLocation",
|
|
288
288
|
"InstrumentOffsetVector",
|
|
289
|
+
"GripperMoveType",
|
|
289
290
|
# Liquid level detection types
|
|
290
291
|
"LoadedVolumeInfo",
|
|
291
292
|
"ProbedHeightInfo",
|
|
@@ -301,6 +302,7 @@ __all__ = [
|
|
|
301
302
|
"LabwareMovementOffsetData",
|
|
302
303
|
# Tips
|
|
303
304
|
"TipGeometry",
|
|
305
|
+
"TipRackWellState",
|
|
304
306
|
# Hardware passthrough
|
|
305
307
|
"MovementAxis",
|
|
306
308
|
"MotorAxis",
|
|
@@ -309,4 +311,8 @@ __all__ = [
|
|
|
309
311
|
"Dimensions",
|
|
310
312
|
# Convenience re-export
|
|
311
313
|
"LabwareUri",
|
|
314
|
+
# Tasks
|
|
315
|
+
"Task",
|
|
316
|
+
"TaskSummary",
|
|
317
|
+
"FinishedTask",
|
|
312
318
|
]
|