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.

Files changed (57) hide show
  1. opentrons/config/defaults_ot3.py +1 -1
  2. opentrons/legacy_commands/commands.py +16 -4
  3. opentrons/legacy_commands/robot_commands.py +51 -0
  4. opentrons/legacy_commands/types.py +91 -2
  5. opentrons/protocol_api/_liquid.py +60 -15
  6. opentrons/protocol_api/_liquid_properties.py +137 -90
  7. opentrons/protocol_api/_transfer_liquid_validation.py +10 -6
  8. opentrons/protocol_api/core/engine/instrument.py +172 -75
  9. opentrons/protocol_api/core/engine/protocol.py +13 -14
  10. opentrons/protocol_api/core/engine/robot.py +2 -2
  11. opentrons/protocol_api/core/engine/transfer_components_executor.py +157 -126
  12. opentrons/protocol_api/core/engine/well.py +16 -0
  13. opentrons/protocol_api/core/instrument.py +2 -2
  14. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -2
  15. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -1
  16. opentrons/protocol_api/core/legacy/legacy_well_core.py +8 -0
  17. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -2
  18. opentrons/protocol_api/core/protocol.py +2 -2
  19. opentrons/protocol_api/core/well.py +8 -0
  20. opentrons/protocol_api/instrument_context.py +377 -86
  21. opentrons/protocol_api/labware.py +10 -0
  22. opentrons/protocol_api/protocol_context.py +79 -4
  23. opentrons/protocol_api/robot_context.py +48 -6
  24. opentrons/protocol_api/validation.py +15 -8
  25. opentrons/protocol_engine/commands/command_unions.py +10 -10
  26. opentrons/protocol_engine/commands/generate_command_schema.py +1 -1
  27. opentrons/protocol_engine/commands/get_next_tip.py +2 -2
  28. opentrons/protocol_engine/commands/pick_up_tip.py +9 -3
  29. opentrons/protocol_engine/commands/robot/__init__.py +20 -20
  30. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +34 -24
  31. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +29 -20
  32. opentrons/protocol_engine/commands/seal_pipette_to_tip.py +1 -1
  33. opentrons/protocol_engine/commands/unsafe/__init__.py +17 -1
  34. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +1 -2
  35. opentrons/protocol_engine/execution/labware_movement.py +9 -2
  36. opentrons/protocol_engine/execution/movement.py +12 -9
  37. opentrons/protocol_engine/execution/queue_worker.py +8 -1
  38. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +52 -19
  39. opentrons/protocol_engine/state/_well_math.py +2 -2
  40. opentrons/protocol_engine/state/commands.py +14 -28
  41. opentrons/protocol_engine/state/frustum_helpers.py +11 -7
  42. opentrons/protocol_engine/state/modules.py +1 -1
  43. opentrons/protocol_engine/state/pipettes.py +8 -0
  44. opentrons/protocol_engine/state/tips.py +46 -83
  45. opentrons/protocol_engine/state/update_types.py +8 -23
  46. opentrons/protocol_runner/legacy_command_mapper.py +11 -4
  47. opentrons/protocol_runner/run_orchestrator.py +1 -1
  48. opentrons/protocols/advanced_control/transfers/common.py +54 -11
  49. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +1 -1
  50. opentrons/protocols/api_support/definitions.py +1 -1
  51. opentrons/types.py +6 -6
  52. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/METADATA +4 -4
  53. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/RECORD +57 -56
  54. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/LICENSE +0 -0
  55. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/WHEEL +0 -0
  56. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/entry_points.txt +0 -0
  57. {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 TipRackWellState(Enum):
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
- TipRackStateByWellName = Dict[str, TipRackWellState]
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, TipRackStateByWellName]
44
- column_by_labware_id: Dict[str, List[List[str]]]
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
- column_by_labware_id={},
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
- ] = TipRackWellState.CLEAN
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
- well_name=state_update.tips_used.well_name,
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: TipRackWellState.CLEAN
72
+ well_name: _TipRackWellState.CLEAN
107
73
  for column in definition.ordering
108
74
  for well_name in column
109
75
  }
110
- self._state.column_by_labware_id[labware_id] = [
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: TipRackWellState.CLEAN
86
+ well_name: _TipRackWellState.CLEAN
121
87
  for column in definition.ordering
122
88
  for well_name in column
123
89
  }
124
- self._state.column_by_labware_id[labware_id] = [
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, pipette_id: str, well_name: str, labware_id: str) -> None:
129
- columns = self._state.column_by_labware_id.get(labware_id, [])
130
- wells = self._state.tips_by_labware_id.get(labware_id, {})
131
- nozzle_map = self._state.pipette_info_by_pipette_id[pipette_id].nozzle_map
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.column_by_labware_id.get(labware_id, [])
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] == TipRackWellState.USED for well in tip_cluster):
169
+ if not any(wells[well] == _TipRackWellState.USED for well in tip_cluster):
206
170
  return tip_cluster[0]
207
- elif all(wells[well] == TipRackWellState.USED for well in tip_cluster):
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] == TipRackWellState.USED
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] == TipRackWellState.USED
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(wells[well] == TipRackWellState.USED for well in column):
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 == TipRackWellState.USED for tip_state in wells.values()
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 == TipRackWellState.CLEAN:
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 == TipRackWellState.CLEAN
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: TipRackStateByWellName, starting_tip_name: str
448
- ) -> TipRackStateByWellName:
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
- well_name: str
259
- """The well that the pipette's primary nozzle targeted.
247
+ well_names: list[str]
248
+ """The exact wells in the tip rack that should be marked as used.
260
249
 
261
- Wells in addition to this one will also be marked as used, depending on the
262
- pipette's nozzle layout.
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
- location = pe_types.ModuleLocation.model_construct(
639
- moduleId=self._module_id_by_slot[slot]
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
- # These legacy json protocols don't get location sequences because
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.tips.get_pipette_nozzle_maps()
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["once", "always", "per source", "never"]
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, air_gap: float, disposal_volume: float, max_volume: float
47
+ aspirate_volume: float,
48
+ air_gap: float,
49
+ max_volume: float,
50
+ current_volume: float,
43
51
  ) -> None:
44
- if air_gap + aspirate_volume > max_volume:
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" with a max volume of {max_volume} µL. Please adjust the retract air gap to fit within"
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 disposal_volume + aspirate_volume > max_volume:
61
+ elif air_gap + aspirate_volume > max_volume:
51
62
  raise ValueError(
52
- f"Cannot have a dispense volume of {disposal_volume} µL for an aspiration of {aspirate_volume} µL"
53
- f" with a max volume of {max_volume} µL. Please adjust the dispense volume to fit within"
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
- for split_volume in _split_volume_equally(volume, max_volume):
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 is not None:
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"
@@ -1,6 +1,6 @@
1
1
  from .types import APIVersion
2
2
 
3
- MAX_SUPPORTED_VERSION = APIVersion(2, 23)
3
+ MAX_SUPPORTED_VERSION = APIVersion(2, 24)
4
4
  """The maximum supported protocol API version in this release."""
5
5
 
6
6
  MIN_SUPPORTED_VERSION = APIVersion(2, 0)
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.4.0a13
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.4.0a13)
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.4.0a13) ; extra == 'flex-hardware'
38
+ Requires-Dist: opentrons-hardware[flex] (==8.5.0a1) ; extra == 'flex-hardware'
39
39
  Provides-Extra: ot2-hardware
40
- Requires-Dist: opentrons-hardware (==8.4.0a13) ; extra == 'ot2-hardware'
40
+ Requires-Dist: opentrons-hardware (==8.5.0a1) ; extra == 'ot2-hardware'
41
41
 
42
42
  .. _Full API Documentation: http://docs.opentrons.com
43
43