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.

Files changed (147) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/drivers/asyncio/communication/serial_connection.py +55 -129
  3. opentrons/drivers/flex_stacker/driver.py +6 -1
  4. opentrons/drivers/heater_shaker/abstract.py +0 -5
  5. opentrons/drivers/heater_shaker/driver.py +0 -10
  6. opentrons/drivers/heater_shaker/simulator.py +0 -4
  7. opentrons/drivers/thermocycler/abstract.py +0 -6
  8. opentrons/drivers/thermocycler/driver.py +10 -61
  9. opentrons/drivers/thermocycler/simulator.py +0 -6
  10. opentrons/hardware_control/api.py +5 -24
  11. opentrons/hardware_control/backends/controller.py +2 -8
  12. opentrons/hardware_control/backends/flex_protocol.py +1 -0
  13. opentrons/hardware_control/backends/ot3controller.py +3 -3
  14. opentrons/hardware_control/backends/ot3simulator.py +2 -2
  15. opentrons/hardware_control/backends/simulator.py +1 -2
  16. opentrons/hardware_control/backends/subsystem_manager.py +2 -5
  17. opentrons/hardware_control/emulation/abstract_emulator.py +4 -6
  18. opentrons/hardware_control/emulation/connection_handler.py +5 -8
  19. opentrons/hardware_control/emulation/heater_shaker.py +3 -12
  20. opentrons/hardware_control/emulation/settings.py +1 -1
  21. opentrons/hardware_control/emulation/thermocycler.py +15 -67
  22. opentrons/hardware_control/module_control.py +8 -82
  23. opentrons/hardware_control/modules/__init__.py +0 -3
  24. opentrons/hardware_control/modules/absorbance_reader.py +4 -11
  25. opentrons/hardware_control/modules/flex_stacker.py +9 -38
  26. opentrons/hardware_control/modules/heater_shaker.py +5 -42
  27. opentrons/hardware_control/modules/magdeck.py +4 -8
  28. opentrons/hardware_control/modules/mod_abc.py +5 -13
  29. opentrons/hardware_control/modules/tempdeck.py +5 -25
  30. opentrons/hardware_control/modules/thermocycler.py +11 -68
  31. opentrons/hardware_control/modules/types.py +1 -20
  32. opentrons/hardware_control/modules/utils.py +4 -11
  33. opentrons/hardware_control/nozzle_manager.py +0 -3
  34. opentrons/hardware_control/ot3api.py +7 -26
  35. opentrons/hardware_control/poller.py +8 -22
  36. opentrons/hardware_control/protocols/gripper_controller.py +1 -0
  37. opentrons/hardware_control/scripts/update_module_fw.py +0 -5
  38. opentrons/hardware_control/types.py +2 -31
  39. opentrons/legacy_commands/module_commands.py +0 -23
  40. opentrons/legacy_commands/protocol_commands.py +0 -20
  41. opentrons/legacy_commands/types.py +0 -80
  42. opentrons/motion_planning/deck_conflict.py +12 -17
  43. opentrons/motion_planning/waypoints.py +29 -15
  44. opentrons/protocol_api/__init__.py +1 -5
  45. opentrons/protocol_api/_types.py +1 -6
  46. opentrons/protocol_api/core/common.py +1 -3
  47. opentrons/protocol_api/core/engine/_default_labware_versions.py +11 -32
  48. opentrons/protocol_api/core/engine/labware.py +1 -8
  49. opentrons/protocol_api/core/engine/module_core.py +8 -75
  50. opentrons/protocol_api/core/engine/protocol.py +1 -18
  51. opentrons/protocol_api/core/engine/well.py +0 -8
  52. opentrons/protocol_api/core/legacy/legacy_module_core.py +4 -24
  53. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -11
  54. opentrons/protocol_api/core/legacy/legacy_well_core.py +0 -4
  55. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +2 -14
  56. opentrons/protocol_api/core/module.py +4 -37
  57. opentrons/protocol_api/core/protocol.py +2 -11
  58. opentrons/protocol_api/core/well.py +0 -4
  59. opentrons/protocol_api/labware.py +0 -5
  60. opentrons/protocol_api/module_contexts.py +61 -122
  61. opentrons/protocol_api/protocol_context.py +4 -26
  62. opentrons/protocol_api/robot_context.py +21 -38
  63. opentrons/protocol_api/validation.py +1 -6
  64. opentrons/protocol_engine/actions/__init__.py +2 -4
  65. opentrons/protocol_engine/actions/actions.py +9 -22
  66. opentrons/protocol_engine/clients/sync_client.py +7 -42
  67. opentrons/protocol_engine/commands/__init__.py +0 -42
  68. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +15 -2
  69. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +15 -2
  70. opentrons/protocol_engine/commands/aspirate.py +0 -1
  71. opentrons/protocol_engine/commands/command.py +0 -1
  72. opentrons/protocol_engine/commands/command_unions.py +0 -49
  73. opentrons/protocol_engine/commands/dispense.py +0 -1
  74. opentrons/protocol_engine/commands/drop_tip.py +8 -32
  75. opentrons/protocol_engine/commands/heater_shaker/__init__.py +0 -14
  76. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +4 -5
  77. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +5 -31
  78. opentrons/protocol_engine/commands/movement_common.py +0 -2
  79. opentrons/protocol_engine/commands/pick_up_tip.py +11 -21
  80. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +7 -38
  81. opentrons/protocol_engine/commands/thermocycler/__init__.py +0 -16
  82. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +0 -6
  83. opentrons/protocol_engine/commands/thermocycler/run_profile.py +0 -8
  84. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +6 -40
  85. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +5 -29
  86. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  87. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +22 -6
  88. opentrons/protocol_engine/errors/__init__.py +0 -4
  89. opentrons/protocol_engine/errors/exceptions.py +0 -55
  90. opentrons/protocol_engine/execution/__init__.py +0 -2
  91. opentrons/protocol_engine/execution/command_executor.py +0 -8
  92. opentrons/protocol_engine/execution/create_queue_worker.py +1 -5
  93. opentrons/protocol_engine/execution/labware_movement.py +21 -10
  94. opentrons/protocol_engine/execution/movement.py +0 -2
  95. opentrons/protocol_engine/execution/queue_worker.py +0 -4
  96. opentrons/protocol_engine/execution/run_control.py +0 -8
  97. opentrons/protocol_engine/protocol_engine.py +34 -75
  98. opentrons/protocol_engine/resources/__init__.py +0 -2
  99. opentrons/protocol_engine/resources/deck_configuration_provider.py +0 -7
  100. opentrons/protocol_engine/resources/labware_validation.py +6 -10
  101. opentrons/protocol_engine/state/_labware_origin_math.py +636 -0
  102. opentrons/protocol_engine/state/_well_math.py +18 -60
  103. opentrons/protocol_engine/state/addressable_areas.py +0 -2
  104. opentrons/protocol_engine/state/commands.py +11 -14
  105. opentrons/protocol_engine/state/geometry.py +374 -213
  106. opentrons/protocol_engine/state/labware.py +102 -52
  107. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +0 -37
  108. opentrons/protocol_engine/state/modules.py +8 -21
  109. opentrons/protocol_engine/state/motion.py +0 -44
  110. opentrons/protocol_engine/state/state.py +0 -14
  111. opentrons/protocol_engine/state/state_summary.py +0 -2
  112. opentrons/protocol_engine/state/tips.py +258 -177
  113. opentrons/protocol_engine/state/update_types.py +9 -16
  114. opentrons/protocol_engine/types/__init__.py +3 -9
  115. opentrons/protocol_engine/types/deck_configuration.py +1 -5
  116. opentrons/protocol_engine/types/instrument.py +1 -8
  117. opentrons/protocol_engine/types/labware.py +13 -1
  118. opentrons/protocol_engine/types/module.py +0 -10
  119. opentrons/protocol_engine/types/tip.py +0 -9
  120. opentrons/protocol_runner/create_simulating_orchestrator.py +2 -29
  121. opentrons/protocol_runner/run_orchestrator.py +2 -18
  122. opentrons/protocols/api_support/definitions.py +1 -1
  123. opentrons/protocols/api_support/types.py +1 -2
  124. opentrons/simulate.py +15 -48
  125. opentrons/system/camera.py +1 -1
  126. {opentrons-8.7.0a7.dist-info → opentrons-8.7.0a9.dist-info}/METADATA +4 -4
  127. {opentrons-8.7.0a7.dist-info → opentrons-8.7.0a9.dist-info}/RECORD +130 -146
  128. opentrons/protocol_api/core/engine/tasks.py +0 -48
  129. opentrons/protocol_api/core/legacy/tasks.py +0 -19
  130. opentrons/protocol_api/core/legacy_simulator/tasks.py +0 -19
  131. opentrons/protocol_api/core/tasks.py +0 -31
  132. opentrons/protocol_api/tasks.py +0 -48
  133. opentrons/protocol_engine/commands/create_timer.py +0 -83
  134. opentrons/protocol_engine/commands/heater_shaker/common.py +0 -20
  135. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +0 -136
  136. opentrons/protocol_engine/commands/set_tip_state.py +0 -97
  137. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +0 -191
  138. opentrons/protocol_engine/commands/wait_for_tasks.py +0 -98
  139. opentrons/protocol_engine/execution/task_handler.py +0 -157
  140. opentrons/protocol_engine/resources/concurrency_provider.py +0 -27
  141. opentrons/protocol_engine/state/labware_origin_math/errors.py +0 -94
  142. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +0 -1331
  143. opentrons/protocol_engine/state/tasks.py +0 -139
  144. opentrons/protocol_engine/types/tasks.py +0 -38
  145. {opentrons-8.7.0a7.dist-info → opentrons-8.7.0a9.dist-info}/WHEEL +0 -0
  146. {opentrons-8.7.0a7.dist-info → opentrons-8.7.0a9.dist-info}/entry_points.txt +0 -0
  147. {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 typing import Dict, Iterable, Optional, List, Set
4
+ from enum import Enum
5
+ from typing import Dict, Iterable, Optional, List, Union
5
6
 
6
- from opentrons.types import NozzleMapInterface, NozzleConfigurationType
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
- wells_covered_dense,
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
- _TipRackStateByWellName = Dict[str, TipRackWellState]
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.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,
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: TipRackWellState.CLEAN
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: TipRackWellState.CLEAN
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 _set_tip_state(
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] = tip_state
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
- """Gets the next available clean tip.
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
- 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)
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 all(wells[well] == TipRackWellState.CLEAN for well in column):
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 all(
142
- tip_state == TipRackWellState.CLEAN for tip_state in wells.values()
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 == TipRackWellState.CLEAN:
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 == TipRackWellState.CLEAN
390
+ return well_state == _TipRackWellState.CLEAN
234
391
 
235
- def compute_tips_to_mark_as_used_or_empty(
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/drop should mark as "used" or "empty".
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, TipRackWellState] = {}
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 TipsStateUpdate:
240
- """Represents an update that marks tips in a tip rack as the requested state."""
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: typing.Optional[str]
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
- tips_state: TipsStateUpdate | NoChangeType = NO_CHANGE
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: typing.Optional[str],
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 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
- )
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, TipRackWellState
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
  ]