opentrons 8.7.0a9__py3-none-any.whl → 8.8.0a8__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.
- opentrons/_version.py +2 -2
- opentrons/cli/analyze.py +4 -1
- opentrons/config/__init__.py +7 -0
- opentrons/drivers/asyncio/communication/serial_connection.py +126 -49
- 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/drivers/vacuum_module/__init__.py +5 -0
- opentrons/drivers/vacuum_module/abstract.py +93 -0
- opentrons/drivers/vacuum_module/driver.py +208 -0
- opentrons/drivers/vacuum_module/errors.py +39 -0
- opentrons/drivers/vacuum_module/simulator.py +85 -0
- opentrons/drivers/vacuum_module/types.py +79 -0
- opentrons/execute.py +3 -0
- opentrons/hardware_control/api.py +24 -5
- opentrons/hardware_control/backends/controller.py +8 -2
- opentrons/hardware_control/backends/flex_protocol.py +1 -0
- opentrons/hardware_control/backends/ot3controller.py +35 -2
- opentrons/hardware_control/backends/ot3simulator.py +3 -1
- opentrons/hardware_control/backends/ot3utils.py +37 -0
- 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 +105 -10
- 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 +14 -6
- 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/motion_utilities.py +6 -6
- opentrons/hardware_control/nozzle_manager.py +3 -0
- opentrons/hardware_control/ot3api.py +92 -17
- opentrons/hardware_control/poller.py +22 -8
- opentrons/hardware_control/protocols/liquid_handler.py +12 -4
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +43 -2
- opentrons/legacy_commands/commands.py +58 -5
- opentrons/legacy_commands/module_commands.py +52 -0
- opentrons/legacy_commands/protocol_commands.py +53 -1
- opentrons/legacy_commands/types.py +155 -1
- 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/_transfer_liquid_validation.py +17 -2
- opentrons/protocol_api/_types.py +8 -1
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
- opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
- opentrons/protocol_api/core/engine/instrument.py +109 -26
- opentrons/protocol_api/core/engine/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +95 -4
- opentrons/protocol_api/core/engine/pipette_movement_conflict.py +4 -18
- opentrons/protocol_api/core/engine/protocol.py +51 -2
- opentrons/protocol_api/core/engine/stringify.py +2 -0
- opentrons/protocol_api/core/engine/tasks.py +48 -0
- opentrons/protocol_api/core/engine/well.py +8 -0
- opentrons/protocol_api/core/instrument.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -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_instrument_core.py +19 -2
- 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 +58 -2
- opentrons/protocol_api/core/protocol.py +23 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/core/well.py +4 -0
- opentrons/protocol_api/instrument_context.py +388 -2
- opentrons/protocol_api/labware.py +10 -2
- opentrons/protocol_api/module_contexts.py +170 -6
- opentrons/protocol_api/protocol_context.py +87 -21
- opentrons/protocol_api/robot_context.py +41 -25
- opentrons/protocol_api/tasks.py +48 -0
- opentrons/protocol_api/validation.py +49 -3
- opentrons/protocol_engine/__init__.py +4 -0
- opentrons/protocol_engine/actions/__init__.py +6 -2
- opentrons/protocol_engine/actions/actions.py +31 -9
- opentrons/protocol_engine/clients/sync_client.py +42 -7
- opentrons/protocol_engine/commands/__init__.py +56 -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/absorbance_reader/read.py +22 -23
- opentrons/protocol_engine/commands/aspirate.py +1 -0
- opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
- opentrons/protocol_engine/commands/capture_image.py +302 -0
- opentrons/protocol_engine/commands/command.py +2 -0
- opentrons/protocol_engine/commands/command_unions.py +62 -0
- opentrons/protocol_engine/commands/create_timer.py +83 -0
- opentrons/protocol_engine/commands/dispense.py +1 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
- opentrons/protocol_engine/commands/drop_tip.py +32 -8
- opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
- opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
- 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/move_labware.py +3 -4
- opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
- opentrons/protocol_engine/commands/movement_common.py +31 -2
- opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
- opentrons/protocol_engine/commands/pipetting_common.py +48 -3
- 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 +44 -7
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
- 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/create_protocol_engine.py +12 -0
- opentrons/protocol_engine/engine_support.py +3 -0
- opentrons/protocol_engine/errors/__init__.py +12 -0
- opentrons/protocol_engine/errors/exceptions.py +119 -0
- opentrons/protocol_engine/execution/__init__.py +4 -0
- opentrons/protocol_engine/execution/command_executor.py +62 -1
- opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
- opentrons/protocol_engine/execution/labware_movement.py +13 -15
- opentrons/protocol_engine/execution/movement.py +2 -0
- opentrons/protocol_engine/execution/pipetting.py +26 -25
- 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 +137 -36
- opentrons/protocol_engine/resources/__init__.py +4 -0
- opentrons/protocol_engine/resources/camera_provider.py +110 -0
- opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
- opentrons/protocol_engine/resources/file_provider.py +133 -58
- opentrons/protocol_engine/resources/labware_validation.py +10 -6
- opentrons/protocol_engine/slot_standardization.py +2 -0
- opentrons/protocol_engine/state/_well_math.py +60 -18
- opentrons/protocol_engine/state/addressable_areas.py +2 -0
- opentrons/protocol_engine/state/camera.py +54 -0
- opentrons/protocol_engine/state/commands.py +37 -14
- opentrons/protocol_engine/state/geometry.py +276 -379
- opentrons/protocol_engine/state/labware.py +62 -108
- opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
- opentrons/protocol_engine/state/modules.py +30 -8
- opentrons/protocol_engine/state/motion.py +60 -18
- opentrons/protocol_engine/state/preconditions.py +59 -0
- opentrons/protocol_engine/state/state.py +44 -0
- opentrons/protocol_engine/state/state_summary.py +4 -0
- opentrons/protocol_engine/state/tasks.py +139 -0
- opentrons/protocol_engine/state/tips.py +177 -258
- opentrons/protocol_engine/state/update_types.py +26 -9
- opentrons/protocol_engine/types/__init__.py +23 -4
- opentrons/protocol_engine/types/command_preconditions.py +18 -0
- 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/location.py +26 -2
- opentrons/protocol_engine/types/module.py +11 -1
- 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/protocol_runner.py +14 -1
- opentrons/protocol_runner/run_orchestrator.py +49 -2
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +2 -1
- opentrons/simulate.py +51 -15
- opentrons/system/camera.py +334 -4
- opentrons/system/ffmpeg.py +110 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/RECORD +189 -161
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.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,
|
|
@@ -23,6 +24,7 @@ from opentrons.protocol_engine.types import (
|
|
|
23
24
|
ModuleModel,
|
|
24
25
|
ModuleDefinition,
|
|
25
26
|
LabwareWellId,
|
|
27
|
+
PreconditionTypes,
|
|
26
28
|
)
|
|
27
29
|
from opentrons.types import MountType, DeckSlotName
|
|
28
30
|
from opentrons_shared_data.labware.labware_definition import LabwareDefinition
|
|
@@ -235,8 +237,10 @@ class PipetteAspirateReadyUpdate:
|
|
|
235
237
|
|
|
236
238
|
|
|
237
239
|
@dataclasses.dataclass
|
|
238
|
-
class
|
|
239
|
-
"""Represents an update that marks tips in a tip rack as
|
|
240
|
+
class TipsStateUpdate:
|
|
241
|
+
"""Represents an update that marks tips in a tip rack as the requested state."""
|
|
242
|
+
|
|
243
|
+
tip_state: TipRackWellState
|
|
240
244
|
|
|
241
245
|
labware_id: str
|
|
242
246
|
"""The labware ID of the tip rack."""
|
|
@@ -398,6 +402,13 @@ class FilesAddedUpdate:
|
|
|
398
402
|
file_ids: list[str]
|
|
399
403
|
|
|
400
404
|
|
|
405
|
+
@dataclasses.dataclass
|
|
406
|
+
class PreconditionUpdate:
|
|
407
|
+
"""An update that changes command preconditions flags."""
|
|
408
|
+
|
|
409
|
+
preconditions: dict[PreconditionTypes, bool]
|
|
410
|
+
|
|
411
|
+
|
|
401
412
|
@dataclasses.dataclass
|
|
402
413
|
class AddressableAreaUsedUpdate:
|
|
403
414
|
"""An update that says an addressable area has been used."""
|
|
@@ -413,7 +424,7 @@ class LoadModuleUpdate:
|
|
|
413
424
|
definition: ModuleDefinition
|
|
414
425
|
slot_name: DeckSlotName
|
|
415
426
|
requested_model: ModuleModel
|
|
416
|
-
serial_number: Optional[str]
|
|
427
|
+
serial_number: typing.Optional[str]
|
|
417
428
|
|
|
418
429
|
|
|
419
430
|
@dataclasses.dataclass
|
|
@@ -452,7 +463,7 @@ class StateUpdate:
|
|
|
452
463
|
|
|
453
464
|
labware_lid: LabwareLidUpdate | NoChangeType = NO_CHANGE
|
|
454
465
|
|
|
455
|
-
|
|
466
|
+
tips_state: TipsStateUpdate | NoChangeType = NO_CHANGE
|
|
456
467
|
|
|
457
468
|
liquid_loaded: LiquidLoadedUpdate | NoChangeType = NO_CHANGE
|
|
458
469
|
|
|
@@ -474,6 +485,8 @@ class StateUpdate:
|
|
|
474
485
|
|
|
475
486
|
ready_to_aspirate: PipetteAspirateReadyUpdate | NoChangeType = NO_CHANGE
|
|
476
487
|
|
|
488
|
+
precondition_update: PreconditionUpdate | NoChangeType = NO_CHANGE
|
|
489
|
+
|
|
477
490
|
def append(self, other: Self) -> Self:
|
|
478
491
|
"""Apply another `StateUpdate` "on top of" this one.
|
|
479
492
|
|
|
@@ -682,7 +695,7 @@ class StateUpdate:
|
|
|
682
695
|
definition: ModuleDefinition,
|
|
683
696
|
slot_name: DeckSlotName,
|
|
684
697
|
requested_model: ModuleModel,
|
|
685
|
-
serial_number: Optional[str],
|
|
698
|
+
serial_number: typing.Optional[str],
|
|
686
699
|
) -> Self:
|
|
687
700
|
"""Add a new module to state. See `LoadModuleUpdate`."""
|
|
688
701
|
self.loaded_module = LoadModuleUpdate(
|
|
@@ -729,9 +742,13 @@ class StateUpdate:
|
|
|
729
742
|
)
|
|
730
743
|
return self
|
|
731
744
|
|
|
732
|
-
def
|
|
733
|
-
|
|
734
|
-
|
|
745
|
+
def update_tip_rack_well_state(
|
|
746
|
+
self: Self, tip_state: TipRackWellState, labware_id: str, well_names: list[str]
|
|
747
|
+
) -> Self:
|
|
748
|
+
"""Marks tips in a tip rack to provided tip state. See `TipsStateUpdate`."""
|
|
749
|
+
self.tips_state = TipsStateUpdate(
|
|
750
|
+
tip_state=tip_state, labware_id=labware_id, well_names=well_names
|
|
751
|
+
)
|
|
735
752
|
return self
|
|
736
753
|
|
|
737
754
|
def set_liquid_loaded(
|