opentrons 8.4.0a13__py2.py3-none-any.whl → 8.5.0a1__py2.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/config/defaults_ot3.py +1 -1
- opentrons/legacy_commands/commands.py +16 -4
- opentrons/legacy_commands/robot_commands.py +51 -0
- opentrons/legacy_commands/types.py +91 -2
- opentrons/protocol_api/_liquid.py +60 -15
- opentrons/protocol_api/_liquid_properties.py +137 -90
- opentrons/protocol_api/_transfer_liquid_validation.py +10 -6
- opentrons/protocol_api/core/engine/instrument.py +172 -75
- opentrons/protocol_api/core/engine/protocol.py +13 -14
- opentrons/protocol_api/core/engine/robot.py +2 -2
- opentrons/protocol_api/core/engine/transfer_components_executor.py +157 -126
- opentrons/protocol_api/core/engine/well.py +16 -0
- opentrons/protocol_api/core/instrument.py +2 -2
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -2
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -1
- opentrons/protocol_api/core/legacy/legacy_well_core.py +8 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -2
- opentrons/protocol_api/core/protocol.py +2 -2
- opentrons/protocol_api/core/well.py +8 -0
- opentrons/protocol_api/instrument_context.py +377 -86
- opentrons/protocol_api/labware.py +10 -0
- opentrons/protocol_api/protocol_context.py +79 -4
- opentrons/protocol_api/robot_context.py +48 -6
- opentrons/protocol_api/validation.py +15 -8
- opentrons/protocol_engine/commands/command_unions.py +10 -10
- opentrons/protocol_engine/commands/generate_command_schema.py +1 -1
- opentrons/protocol_engine/commands/get_next_tip.py +2 -2
- opentrons/protocol_engine/commands/pick_up_tip.py +9 -3
- opentrons/protocol_engine/commands/robot/__init__.py +20 -20
- opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +34 -24
- opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +29 -20
- opentrons/protocol_engine/commands/seal_pipette_to_tip.py +1 -1
- opentrons/protocol_engine/commands/unsafe/__init__.py +17 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +1 -2
- opentrons/protocol_engine/execution/labware_movement.py +9 -2
- opentrons/protocol_engine/execution/movement.py +12 -9
- opentrons/protocol_engine/execution/queue_worker.py +8 -1
- opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +52 -19
- opentrons/protocol_engine/state/_well_math.py +2 -2
- opentrons/protocol_engine/state/commands.py +14 -28
- opentrons/protocol_engine/state/frustum_helpers.py +11 -7
- opentrons/protocol_engine/state/modules.py +1 -1
- opentrons/protocol_engine/state/pipettes.py +8 -0
- opentrons/protocol_engine/state/tips.py +46 -83
- opentrons/protocol_engine/state/update_types.py +8 -23
- opentrons/protocol_runner/legacy_command_mapper.py +11 -4
- opentrons/protocol_runner/run_orchestrator.py +1 -1
- opentrons/protocols/advanced_control/transfers/common.py +54 -11
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +1 -1
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/types.py +6 -6
- {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/METADATA +4 -4
- {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/RECORD +57 -56
- {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/LICENSE +0 -0
- {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/WHEEL +0 -0
- {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/entry_points.txt +0 -0
- {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/top_level.txt +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import Dict, Optional, List, Union
|
|
5
|
+
from typing import Dict, Iterable, Optional, List, Union
|
|
6
6
|
|
|
7
7
|
from opentrons.types import NozzleMapInterface
|
|
8
8
|
from opentrons.protocol_engine.state import update_types
|
|
@@ -14,36 +14,22 @@ from ..actions import Action, ResetTipsAction, get_state_updates
|
|
|
14
14
|
from opentrons.hardware_control.nozzle_manager import NozzleMap
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class
|
|
17
|
+
class _TipRackWellState(Enum):
|
|
18
18
|
"""The state of a single tip in a tip rack's well."""
|
|
19
19
|
|
|
20
20
|
CLEAN = "clean"
|
|
21
21
|
USED = "used"
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# todo(mm, 2024-10-10): This info is duplicated between here and PipetteState because
|
|
28
|
-
# TipStore is using it to compute which tips a PickUpTip removes from the tip rack,
|
|
29
|
-
# given the pipette's current nozzle map. We could avoid this duplication by moving the
|
|
30
|
-
# computation to TipView, calling it from PickUpTipImplementation, and passing the
|
|
31
|
-
# precomputed list of wells to TipStore.
|
|
32
|
-
@dataclass
|
|
33
|
-
class _PipetteInfo:
|
|
34
|
-
channels: int
|
|
35
|
-
active_channels: int
|
|
36
|
-
nozzle_map: NozzleMap
|
|
24
|
+
_TipRackStateByWellName = Dict[str, _TipRackWellState]
|
|
37
25
|
|
|
38
26
|
|
|
39
27
|
@dataclass
|
|
40
28
|
class TipState:
|
|
41
29
|
"""State of all tips."""
|
|
42
30
|
|
|
43
|
-
tips_by_labware_id: Dict[str,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
pipette_info_by_pipette_id: Dict[str, _PipetteInfo]
|
|
31
|
+
tips_by_labware_id: Dict[str, _TipRackStateByWellName]
|
|
32
|
+
columns_by_labware_id: Dict[str, List[List[str]]]
|
|
47
33
|
|
|
48
34
|
|
|
49
35
|
class TipStore(HasState[TipState], HandlesActions):
|
|
@@ -55,8 +41,7 @@ class TipStore(HasState[TipState], HandlesActions):
|
|
|
55
41
|
"""Initialize a liquid store and its state."""
|
|
56
42
|
self._state = TipState(
|
|
57
43
|
tips_by_labware_id={},
|
|
58
|
-
|
|
59
|
-
pipette_info_by_pipette_id={},
|
|
44
|
+
columns_by_labware_id={},
|
|
60
45
|
)
|
|
61
46
|
|
|
62
47
|
def handle_action(self, action: Action) -> None:
|
|
@@ -70,44 +55,25 @@ class TipStore(HasState[TipState], HandlesActions):
|
|
|
70
55
|
for well_name in self._state.tips_by_labware_id[labware_id].keys():
|
|
71
56
|
self._state.tips_by_labware_id[labware_id][
|
|
72
57
|
well_name
|
|
73
|
-
] =
|
|
58
|
+
] = _TipRackWellState.CLEAN
|
|
74
59
|
|
|
75
60
|
def _handle_state_update(self, state_update: update_types.StateUpdate) -> None:
|
|
76
|
-
if state_update.pipette_config != update_types.NO_CHANGE:
|
|
77
|
-
self._state.pipette_info_by_pipette_id[
|
|
78
|
-
state_update.pipette_config.pipette_id
|
|
79
|
-
] = _PipetteInfo(
|
|
80
|
-
channels=state_update.pipette_config.config.channels,
|
|
81
|
-
active_channels=state_update.pipette_config.config.channels,
|
|
82
|
-
nozzle_map=state_update.pipette_config.config.nozzle_map,
|
|
83
|
-
)
|
|
84
|
-
|
|
85
61
|
if state_update.tips_used != update_types.NO_CHANGE:
|
|
86
62
|
self._set_used_tips(
|
|
87
|
-
pipette_id=state_update.tips_used.pipette_id,
|
|
88
63
|
labware_id=state_update.tips_used.labware_id,
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
if state_update.pipette_nozzle_map != update_types.NO_CHANGE:
|
|
93
|
-
pipette_info = self._state.pipette_info_by_pipette_id[
|
|
94
|
-
state_update.pipette_nozzle_map.pipette_id
|
|
95
|
-
]
|
|
96
|
-
pipette_info.active_channels = (
|
|
97
|
-
state_update.pipette_nozzle_map.nozzle_map.tip_count
|
|
64
|
+
well_names=state_update.tips_used.well_names,
|
|
98
65
|
)
|
|
99
|
-
pipette_info.nozzle_map = state_update.pipette_nozzle_map.nozzle_map
|
|
100
66
|
|
|
101
67
|
if state_update.loaded_labware != update_types.NO_CHANGE:
|
|
102
68
|
labware_id = state_update.loaded_labware.labware_id
|
|
103
69
|
definition = state_update.loaded_labware.definition
|
|
104
70
|
if definition.parameters.isTiprack:
|
|
105
71
|
self._state.tips_by_labware_id[labware_id] = {
|
|
106
|
-
well_name:
|
|
72
|
+
well_name: _TipRackWellState.CLEAN
|
|
107
73
|
for column in definition.ordering
|
|
108
74
|
for well_name in column
|
|
109
75
|
}
|
|
110
|
-
self._state.
|
|
76
|
+
self._state.columns_by_labware_id[labware_id] = [
|
|
111
77
|
column for column in definition.ordering
|
|
112
78
|
]
|
|
113
79
|
if state_update.batch_loaded_labware != update_types.NO_CHANGE:
|
|
@@ -117,20 +83,18 @@ class TipStore(HasState[TipState], HandlesActions):
|
|
|
117
83
|
]
|
|
118
84
|
if definition.parameters.isTiprack:
|
|
119
85
|
self._state.tips_by_labware_id[labware_id] = {
|
|
120
|
-
well_name:
|
|
86
|
+
well_name: _TipRackWellState.CLEAN
|
|
121
87
|
for column in definition.ordering
|
|
122
88
|
for well_name in column
|
|
123
89
|
}
|
|
124
|
-
self._state.
|
|
90
|
+
self._state.columns_by_labware_id[labware_id] = [
|
|
125
91
|
column for column in definition.ordering
|
|
126
92
|
]
|
|
127
93
|
|
|
128
|
-
def _set_used_tips(self,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
for well in wells_covered_dense(nozzle_map, well_name, columns):
|
|
133
|
-
wells[well] = TipRackWellState.USED
|
|
94
|
+
def _set_used_tips(self, labware_id: str, well_names: Iterable[str]) -> None:
|
|
95
|
+
well_states = self._state.tips_by_labware_id.get(labware_id, {})
|
|
96
|
+
for well_name in well_names:
|
|
97
|
+
well_states[well_name] = _TipRackWellState.USED
|
|
134
98
|
|
|
135
99
|
|
|
136
100
|
class TipView:
|
|
@@ -155,7 +119,7 @@ class TipView:
|
|
|
155
119
|
) -> Optional[str]:
|
|
156
120
|
"""Get the next available clean tip. Does not support use of a starting tip if the pipette used is in a partial configuration."""
|
|
157
121
|
wells = self._state.tips_by_labware_id.get(labware_id, {})
|
|
158
|
-
columns = self._state.
|
|
122
|
+
columns = self._state.columns_by_labware_id.get(labware_id, [])
|
|
159
123
|
|
|
160
124
|
# TODO(sf): I'm pretty sure this can be replaced with wells_covered_96 but I'm not quite sure how
|
|
161
125
|
def _identify_tip_cluster(
|
|
@@ -202,9 +166,9 @@ class TipView:
|
|
|
202
166
|
def _validate_tip_cluster(
|
|
203
167
|
active_columns: int, active_rows: int, tip_cluster: List[str]
|
|
204
168
|
) -> Union[str, int, None]:
|
|
205
|
-
if not any(wells[well] ==
|
|
169
|
+
if not any(wells[well] == _TipRackWellState.USED for well in tip_cluster):
|
|
206
170
|
return tip_cluster[0]
|
|
207
|
-
elif all(wells[well] ==
|
|
171
|
+
elif all(wells[well] == _TipRackWellState.USED for well in tip_cluster):
|
|
208
172
|
return None
|
|
209
173
|
else:
|
|
210
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
|
|
@@ -224,12 +188,12 @@ class TipView:
|
|
|
224
188
|
tip_cluster[(active_rows - 1) + (i * active_rows)]
|
|
225
189
|
)
|
|
226
190
|
if all(
|
|
227
|
-
wells[well] ==
|
|
191
|
+
wells[well] == _TipRackWellState.USED
|
|
228
192
|
for well in tip_cluster_final_column
|
|
229
193
|
):
|
|
230
194
|
return None
|
|
231
195
|
elif all(
|
|
232
|
-
wells[well] ==
|
|
196
|
+
wells[well] == _TipRackWellState.USED
|
|
233
197
|
for well in tip_cluster_final_row
|
|
234
198
|
):
|
|
235
199
|
return None
|
|
@@ -386,7 +350,9 @@ class TipView:
|
|
|
386
350
|
starting_column_index = idx
|
|
387
351
|
|
|
388
352
|
for column in columns[starting_column_index:]:
|
|
389
|
-
if not any(
|
|
353
|
+
if not any(
|
|
354
|
+
wells[well] == _TipRackWellState.USED for well in column
|
|
355
|
+
):
|
|
390
356
|
return column[0]
|
|
391
357
|
|
|
392
358
|
elif num_tips == len(wells.keys()): # Get next tips for 96 channel
|
|
@@ -394,7 +360,7 @@ class TipView:
|
|
|
394
360
|
return None
|
|
395
361
|
|
|
396
362
|
if not any(
|
|
397
|
-
tip_state ==
|
|
363
|
+
tip_state == _TipRackWellState.USED for tip_state in wells.values()
|
|
398
364
|
):
|
|
399
365
|
return next(iter(wells))
|
|
400
366
|
|
|
@@ -403,29 +369,10 @@ class TipView:
|
|
|
403
369
|
wells = _drop_wells_before_starting_tip(wells, starting_tip_name)
|
|
404
370
|
|
|
405
371
|
for well_name, tip_state in wells.items():
|
|
406
|
-
if tip_state ==
|
|
372
|
+
if tip_state == _TipRackWellState.CLEAN:
|
|
407
373
|
return well_name
|
|
408
374
|
return None
|
|
409
375
|
|
|
410
|
-
def get_pipette_channels(self, pipette_id: str) -> int:
|
|
411
|
-
"""Return the given pipette's number of channels."""
|
|
412
|
-
return self._state.pipette_info_by_pipette_id[pipette_id].channels
|
|
413
|
-
|
|
414
|
-
def get_pipette_active_channels(self, pipette_id: str) -> int:
|
|
415
|
-
"""Get the number of channels being used in the given pipette's configuration."""
|
|
416
|
-
return self._state.pipette_info_by_pipette_id[pipette_id].active_channels
|
|
417
|
-
|
|
418
|
-
def get_pipette_nozzle_map(self, pipette_id: str) -> NozzleMap:
|
|
419
|
-
"""Get the current nozzle map the given pipette's configuration."""
|
|
420
|
-
return self._state.pipette_info_by_pipette_id[pipette_id].nozzle_map
|
|
421
|
-
|
|
422
|
-
def get_pipette_nozzle_maps(self) -> Dict[str, NozzleMap]:
|
|
423
|
-
"""Get current nozzle maps keyed by pipette id."""
|
|
424
|
-
return {
|
|
425
|
-
pipette_id: pipette_info.nozzle_map
|
|
426
|
-
for pipette_id, pipette_info in self._state.pipette_info_by_pipette_id.items()
|
|
427
|
-
}
|
|
428
|
-
|
|
429
376
|
def has_clean_tip(self, labware_id: str, well_name: str) -> bool:
|
|
430
377
|
"""Get whether a well in a labware has a clean tip.
|
|
431
378
|
|
|
@@ -440,15 +387,31 @@ class TipView:
|
|
|
440
387
|
tip_rack = self._state.tips_by_labware_id.get(labware_id)
|
|
441
388
|
well_state = tip_rack.get(well_name) if tip_rack else None
|
|
442
389
|
|
|
443
|
-
return well_state ==
|
|
390
|
+
return well_state == _TipRackWellState.CLEAN
|
|
391
|
+
|
|
392
|
+
def compute_tips_to_mark_as_used(
|
|
393
|
+
self, labware_id: str, well_name: str, nozzle_map: NozzleMap
|
|
394
|
+
) -> list[str]:
|
|
395
|
+
"""Compute which tips a hypothetical tip pickup should mark as "used".
|
|
396
|
+
|
|
397
|
+
Params:
|
|
398
|
+
labware_id: The labware ID of the tip rack.
|
|
399
|
+
well_name: The single target well of the tip pickup.
|
|
400
|
+
nozzle_map: The nozzle configuration that the pipette will use for the pickup.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
The well names of all the tips that the operation will use.
|
|
404
|
+
"""
|
|
405
|
+
columns = self._state.columns_by_labware_id.get(labware_id, [])
|
|
406
|
+
return list(wells_covered_dense(nozzle_map, well_name, columns))
|
|
444
407
|
|
|
445
408
|
|
|
446
409
|
def _drop_wells_before_starting_tip(
|
|
447
|
-
wells:
|
|
448
|
-
) ->
|
|
410
|
+
wells: _TipRackStateByWellName, starting_tip_name: str
|
|
411
|
+
) -> _TipRackStateByWellName:
|
|
449
412
|
"""Drop any wells that come before the starting tip and return the remaining ones after."""
|
|
450
413
|
seen_starting_well = False
|
|
451
|
-
remaining_wells = {}
|
|
414
|
+
remaining_wells: dict[str, _TipRackWellState] = {}
|
|
452
415
|
for well_name, tip_state in wells.items():
|
|
453
416
|
if well_name == starting_tip_name:
|
|
454
417
|
seen_starting_well = True
|
|
@@ -61,16 +61,6 @@ Unfortunately, mypy doesn't let us write `Literal[CLEAR]`. Use this instead.
|
|
|
61
61
|
"""
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
class _SimulatedEnum(enum.Enum):
|
|
65
|
-
SIMULATED = enum.auto()
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
SIMULATED: typing.Final = _SimulatedEnum.SIMULATED
|
|
69
|
-
"""A sentinel value to indicate that a liquid probe return value is simulated.
|
|
70
|
-
|
|
71
|
-
Useful to avoid throwing unnecessary errors in protocol analysis."""
|
|
72
|
-
|
|
73
|
-
|
|
74
64
|
@dataclasses.dataclass(frozen=True)
|
|
75
65
|
class Well:
|
|
76
66
|
"""Designates a well in a labware."""
|
|
@@ -127,6 +117,7 @@ class BatchLabwareLocationUpdate:
|
|
|
127
117
|
"""The new offsets of each id."""
|
|
128
118
|
|
|
129
119
|
|
|
120
|
+
# todo(mm, 2025-04-28): Combine with BatchLoadedLabwareUpdate.
|
|
130
121
|
@dataclasses.dataclass
|
|
131
122
|
class LoadedLabwareUpdate:
|
|
132
123
|
"""An update that loads a new labware."""
|
|
@@ -250,16 +241,14 @@ class PipetteAspirateReadyUpdate:
|
|
|
250
241
|
class TipsUsedUpdate:
|
|
251
242
|
"""Represents an update that marks tips in a tip rack as used."""
|
|
252
243
|
|
|
253
|
-
pipette_id: str
|
|
254
|
-
"""The pipette that did the tip pickup."""
|
|
255
|
-
|
|
256
244
|
labware_id: str
|
|
245
|
+
"""The labware ID of the tip rack."""
|
|
257
246
|
|
|
258
|
-
|
|
259
|
-
"""The
|
|
247
|
+
well_names: list[str]
|
|
248
|
+
"""The exact wells in the tip rack that should be marked as used.
|
|
260
249
|
|
|
261
|
-
|
|
262
|
-
|
|
250
|
+
This is the *full* list, which is probably more than what appeared in the pickUpTip
|
|
251
|
+
command's params, for multi-channel reasons.
|
|
263
252
|
"""
|
|
264
253
|
|
|
265
254
|
|
|
@@ -701,13 +690,9 @@ class StateUpdate:
|
|
|
701
690
|
)
|
|
702
691
|
return self
|
|
703
692
|
|
|
704
|
-
def mark_tips_as_used(
|
|
705
|
-
self: Self, pipette_id: str, labware_id: str, well_name: str
|
|
706
|
-
) -> Self:
|
|
693
|
+
def mark_tips_as_used(self: Self, labware_id: str, well_names: list[str]) -> Self:
|
|
707
694
|
"""Mark tips in a tip rack as used. See `TipsUsedUpdate`."""
|
|
708
|
-
self.tips_used = TipsUsedUpdate(
|
|
709
|
-
pipette_id=pipette_id, labware_id=labware_id, well_name=well_name
|
|
710
|
-
)
|
|
695
|
+
self.tips_used = TipsUsedUpdate(labware_id=labware_id, well_names=well_names)
|
|
711
696
|
return self
|
|
712
697
|
|
|
713
698
|
def set_liquid_loaded(
|
|
@@ -634,13 +634,21 @@ class LegacyCommandMapper:
|
|
|
634
634
|
count = self._command_count["LOAD_LABWARE"]
|
|
635
635
|
slot = labware_load_info.deck_slot
|
|
636
636
|
location: pe_types.LabwareLocation
|
|
637
|
+
location_sequence: pe_types.LabwareLocationSequence = []
|
|
637
638
|
if labware_load_info.on_module:
|
|
638
|
-
|
|
639
|
-
|
|
639
|
+
module_id = self._module_id_by_slot[slot]
|
|
640
|
+
location = pe_types.ModuleLocation.model_construct(moduleId=module_id)
|
|
641
|
+
location_sequence.append(
|
|
642
|
+
pe_types.OnModuleLocationSequenceComponent(moduleId=module_id)
|
|
640
643
|
)
|
|
641
644
|
else:
|
|
642
645
|
location = pe_types.DeckSlotLocation.model_construct(slotName=slot)
|
|
643
646
|
|
|
647
|
+
location_sequence.append(
|
|
648
|
+
pe_types.OnAddressableAreaLocationSequenceComponent(
|
|
649
|
+
addressableAreaName=slot.value
|
|
650
|
+
)
|
|
651
|
+
)
|
|
644
652
|
command_id = f"commands.LOAD_LABWARE-{count}"
|
|
645
653
|
labware_id = f"labware-{count}"
|
|
646
654
|
|
|
@@ -665,8 +673,7 @@ class LegacyCommandMapper:
|
|
|
665
673
|
labware_load_info.labware_definition
|
|
666
674
|
),
|
|
667
675
|
offsetId=labware_load_info.offset_id,
|
|
668
|
-
|
|
669
|
-
# to do so we'd have to go back and look up where the module gets loaded
|
|
676
|
+
locationSequence=location_sequence,
|
|
670
677
|
),
|
|
671
678
|
)
|
|
672
679
|
queue_action = pe_actions.QueueCommandAction(
|
|
@@ -429,7 +429,7 @@ class RunOrchestrator:
|
|
|
429
429
|
|
|
430
430
|
def get_nozzle_maps(self) -> Mapping[str, NozzleMapInterface]:
|
|
431
431
|
"""Get current nozzle maps keyed by pipette id."""
|
|
432
|
-
return self._protocol_engine.state_view.
|
|
432
|
+
return self._protocol_engine.state_view.pipettes.get_nozzle_configurations()
|
|
433
433
|
|
|
434
434
|
def get_tip_attached(self) -> Dict[str, bool]:
|
|
435
435
|
"""Get current tip state keyed by pipette id."""
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Common functions between v1 transfer and liquid-class-based transfer."""
|
|
2
2
|
import enum
|
|
3
3
|
import math
|
|
4
|
-
from typing import Iterable, Generator, Tuple, TypeVar, Literal, List
|
|
4
|
+
from typing import Iterable, Generator, Tuple, TypeVar, Literal, List, Union
|
|
5
|
+
|
|
6
|
+
from opentrons.protocol_api._liquid_properties import LiquidHandlingPropertyByVolume
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
class NoLiquidClassPropertyError(ValueError):
|
|
@@ -13,9 +15,12 @@ class TransferTipPolicyV2(enum.Enum):
|
|
|
13
15
|
NEVER = "never"
|
|
14
16
|
ALWAYS = "always"
|
|
15
17
|
PER_SOURCE = "per source"
|
|
18
|
+
PER_DESTINATION = "per destination"
|
|
16
19
|
|
|
17
20
|
|
|
18
|
-
TransferTipPolicyV2Type = Literal[
|
|
21
|
+
TransferTipPolicyV2Type = Literal[
|
|
22
|
+
"once", "always", "per source", "never", "per destination"
|
|
23
|
+
]
|
|
19
24
|
|
|
20
25
|
Target = TypeVar("Target")
|
|
21
26
|
|
|
@@ -39,18 +44,24 @@ def check_valid_volume_parameters(
|
|
|
39
44
|
|
|
40
45
|
|
|
41
46
|
def check_valid_liquid_class_volume_parameters(
|
|
42
|
-
aspirate_volume: float,
|
|
47
|
+
aspirate_volume: float,
|
|
48
|
+
air_gap: float,
|
|
49
|
+
max_volume: float,
|
|
50
|
+
current_volume: float,
|
|
43
51
|
) -> None:
|
|
44
|
-
if
|
|
52
|
+
if (
|
|
53
|
+
current_volume != 0.0
|
|
54
|
+
and air_gap + aspirate_volume + current_volume > max_volume
|
|
55
|
+
):
|
|
45
56
|
raise ValueError(
|
|
46
|
-
f"Cannot have an air gap of {air_gap} µL for an aspiration of {aspirate_volume} µL"
|
|
47
|
-
f"
|
|
48
|
-
f" the bounds of the tip."
|
|
57
|
+
f"Cannot have an air gap of {air_gap} µL for an aspiration of {aspirate_volume} µL with"
|
|
58
|
+
f" a max volume of {max_volume} µL when {current_volume} µL has already been aspirated."
|
|
59
|
+
f" Please adjust the retract air gap to fit within the bounds of the tip."
|
|
49
60
|
)
|
|
50
|
-
elif
|
|
61
|
+
elif air_gap + aspirate_volume > max_volume:
|
|
51
62
|
raise ValueError(
|
|
52
|
-
f"Cannot have
|
|
53
|
-
f" with a max volume of {max_volume} µL. Please adjust the
|
|
63
|
+
f"Cannot have an air gap of {air_gap} µL for an aspiration of {aspirate_volume} µL"
|
|
64
|
+
f" with a max volume of {max_volume} µL. Please adjust the retract air gap to fit within"
|
|
54
65
|
f" the bounds of the tip."
|
|
55
66
|
)
|
|
56
67
|
|
|
@@ -95,9 +106,41 @@ def expand_for_volume_constraints_for_liquid_classes(
|
|
|
95
106
|
volumes: Iterable[float],
|
|
96
107
|
targets: Iterable[Target],
|
|
97
108
|
max_volume: float,
|
|
109
|
+
air_gap: Union[LiquidHandlingPropertyByVolume, float],
|
|
110
|
+
disposal_vol: Union[LiquidHandlingPropertyByVolume, float] = 0.0,
|
|
111
|
+
conditioning_vol: Union[LiquidHandlingPropertyByVolume, float] = 0.0,
|
|
98
112
|
) -> Generator[Tuple[float, "Target"], None, None]:
|
|
99
113
|
"""Split a sequence of proposed transfers to keep each under the max volume, splitting larger ones equally."""
|
|
100
114
|
assert max_volume > 0
|
|
101
115
|
for volume, target in zip(volumes, targets):
|
|
102
|
-
|
|
116
|
+
disposal_volume = (
|
|
117
|
+
disposal_vol
|
|
118
|
+
if isinstance(disposal_vol, float)
|
|
119
|
+
else disposal_vol.get_for_volume(volume)
|
|
120
|
+
)
|
|
121
|
+
air_gap_volume = (
|
|
122
|
+
air_gap
|
|
123
|
+
if isinstance(air_gap, float)
|
|
124
|
+
else air_gap.get_for_volume(volume + disposal_volume)
|
|
125
|
+
)
|
|
126
|
+
conditioning_volume = (
|
|
127
|
+
conditioning_vol
|
|
128
|
+
if isinstance(conditioning_vol, float)
|
|
129
|
+
else conditioning_vol.get_for_volume(volume)
|
|
130
|
+
)
|
|
131
|
+
# If there is conditioning volume in a multi-aspirate, it will negate the air gap
|
|
132
|
+
if conditioning_volume > 0:
|
|
133
|
+
air_gap_volume = 0
|
|
134
|
+
adjusted_max_volume = (
|
|
135
|
+
max_volume - air_gap_volume - disposal_volume - conditioning_volume
|
|
136
|
+
)
|
|
137
|
+
if adjusted_max_volume <= 0:
|
|
138
|
+
error_text = f"Pipette cannot aspirate {volume} µL when pipette will need {air_gap_volume} µL for air gap"
|
|
139
|
+
if disposal_volume:
|
|
140
|
+
error_text += f", {disposal_volume} for disposal volume"
|
|
141
|
+
if conditioning_volume:
|
|
142
|
+
error_text += f", {conditioning_volume} for conditioning volume"
|
|
143
|
+
error_text += f" with a max volume of {max_volume} µL."
|
|
144
|
+
raise ValueError(error_text)
|
|
145
|
+
for split_volume in _split_volume_equally(volume, adjusted_max_volume):
|
|
103
146
|
yield split_volume, target
|
|
@@ -48,7 +48,7 @@ def raise_if_location_inside_liquid(
|
|
|
48
48
|
liquid_height_from_bottom = well_core.current_liquid_height()
|
|
49
49
|
except LiquidHeightUnknownError:
|
|
50
50
|
liquid_height_from_bottom = None
|
|
51
|
-
if liquid_height_from_bottom
|
|
51
|
+
if isinstance(liquid_height_from_bottom, (int, float)):
|
|
52
52
|
if liquid_height_from_bottom + well_core.get_bottom(0).z > location.point.z:
|
|
53
53
|
raise RuntimeError(
|
|
54
54
|
f"{location_check_descriptors.location_type.capitalize()} location {location} is"
|
opentrons/types.py
CHANGED
|
@@ -35,12 +35,6 @@ class Point(NamedTuple):
|
|
|
35
35
|
y: float = 0.0
|
|
36
36
|
z: float = 0.0
|
|
37
37
|
|
|
38
|
-
def __eq__(self, other: Any) -> bool:
|
|
39
|
-
if not isinstance(other, Point):
|
|
40
|
-
return False
|
|
41
|
-
pairs = ((self.x, other.x), (self.y, other.y), (self.z, other.z))
|
|
42
|
-
return all(isclose(s, o, rel_tol=1e-05, abs_tol=1e-08) for s, o in pairs)
|
|
43
|
-
|
|
44
38
|
def __add__(self, other: Any) -> Point:
|
|
45
39
|
if not isinstance(other, Point):
|
|
46
40
|
return NotImplemented
|
|
@@ -75,6 +69,12 @@ class Point(NamedTuple):
|
|
|
75
69
|
z_diff = self.z - other.z
|
|
76
70
|
return sqrt(x_diff**2 + y_diff**2 + z_diff**2)
|
|
77
71
|
|
|
72
|
+
def elementwise_isclose(
|
|
73
|
+
self, other: Point, *, rel_tol: float = 1e-05, abs_tol: float = 1e-08
|
|
74
|
+
) -> bool:
|
|
75
|
+
pairs = ((self.x, other.x), (self.y, other.y), (self.z, other.z))
|
|
76
|
+
return all(isclose(s, o, rel_tol=rel_tol, abs_tol=abs_tol) for s, o in pairs)
|
|
77
|
+
|
|
78
78
|
|
|
79
79
|
LocationLabware = Union[
|
|
80
80
|
"Labware",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: opentrons
|
|
3
|
-
Version: 8.
|
|
3
|
+
Version: 8.5.0a1
|
|
4
4
|
Summary: The Opentrons API is a simple framework designed to make writing automated biology lab protocols easy.
|
|
5
5
|
Author: Opentrons
|
|
6
6
|
Author-email: engineering@opentrons.com
|
|
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
21
21
|
Classifier: Topic :: Scientific/Engineering
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
23
|
License-File: ../LICENSE
|
|
24
|
-
Requires-Dist: opentrons-shared-data (==8.
|
|
24
|
+
Requires-Dist: opentrons-shared-data (==8.5.0a1)
|
|
25
25
|
Requires-Dist: aionotify (==0.3.1)
|
|
26
26
|
Requires-Dist: anyio (<4.0.0,>=3.6.1)
|
|
27
27
|
Requires-Dist: jsonschema (<4.18.0,>=3.0.1)
|
|
@@ -35,9 +35,9 @@ Requires-Dist: pyusb (==1.2.1)
|
|
|
35
35
|
Requires-Dist: packaging (>=21.0)
|
|
36
36
|
Requires-Dist: importlib-metadata (>=1.0) ; python_version < "3.8"
|
|
37
37
|
Provides-Extra: flex-hardware
|
|
38
|
-
Requires-Dist: opentrons-hardware[flex] (==8.
|
|
38
|
+
Requires-Dist: opentrons-hardware[flex] (==8.5.0a1) ; extra == 'flex-hardware'
|
|
39
39
|
Provides-Extra: ot2-hardware
|
|
40
|
-
Requires-Dist: opentrons-hardware (==8.
|
|
40
|
+
Requires-Dist: opentrons-hardware (==8.5.0a1) ; extra == 'ot2-hardware'
|
|
41
41
|
|
|
42
42
|
.. _Full API Documentation: http://docs.opentrons.com
|
|
43
43
|
|