opentrons 8.4.1a2__py2.py3-none-any.whl → 8.5.0__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.
Files changed (67) hide show
  1. opentrons/config/defaults_ot3.py +1 -1
  2. opentrons/hardware_control/backends/flex_protocol.py +25 -0
  3. opentrons/hardware_control/backends/ot3controller.py +76 -1
  4. opentrons/hardware_control/backends/ot3simulator.py +27 -0
  5. opentrons/hardware_control/instruments/ot3/pipette_handler.py +1 -0
  6. opentrons/hardware_control/ot3api.py +32 -0
  7. opentrons/legacy_commands/commands.py +16 -4
  8. opentrons/legacy_commands/robot_commands.py +51 -0
  9. opentrons/legacy_commands/types.py +91 -2
  10. opentrons/protocol_api/_liquid.py +60 -15
  11. opentrons/protocol_api/_liquid_properties.py +149 -90
  12. opentrons/protocol_api/_transfer_liquid_validation.py +43 -14
  13. opentrons/protocol_api/core/engine/instrument.py +367 -221
  14. opentrons/protocol_api/core/engine/protocol.py +14 -15
  15. opentrons/protocol_api/core/engine/robot.py +2 -2
  16. opentrons/protocol_api/core/engine/transfer_components_executor.py +275 -163
  17. opentrons/protocol_api/core/engine/well.py +16 -0
  18. opentrons/protocol_api/core/instrument.py +11 -5
  19. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +11 -5
  20. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +2 -2
  21. opentrons/protocol_api/core/legacy/legacy_well_core.py +8 -0
  22. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +11 -5
  23. opentrons/protocol_api/core/protocol.py +3 -3
  24. opentrons/protocol_api/core/well.py +8 -0
  25. opentrons/protocol_api/instrument_context.py +478 -111
  26. opentrons/protocol_api/labware.py +10 -0
  27. opentrons/protocol_api/module_contexts.py +5 -2
  28. opentrons/protocol_api/protocol_context.py +76 -11
  29. opentrons/protocol_api/robot_context.py +48 -6
  30. opentrons/protocol_api/validation.py +15 -8
  31. opentrons/protocol_engine/commands/command_unions.py +10 -10
  32. opentrons/protocol_engine/commands/generate_command_schema.py +1 -1
  33. opentrons/protocol_engine/commands/get_next_tip.py +2 -2
  34. opentrons/protocol_engine/commands/load_labware.py +0 -19
  35. opentrons/protocol_engine/commands/pick_up_tip.py +9 -3
  36. opentrons/protocol_engine/commands/robot/__init__.py +20 -20
  37. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +34 -24
  38. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +29 -20
  39. opentrons/protocol_engine/commands/seal_pipette_to_tip.py +1 -1
  40. opentrons/protocol_engine/commands/unsafe/__init__.py +17 -1
  41. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +1 -2
  42. opentrons/protocol_engine/execution/labware_movement.py +9 -2
  43. opentrons/protocol_engine/execution/movement.py +12 -9
  44. opentrons/protocol_engine/execution/queue_worker.py +8 -1
  45. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +52 -19
  46. opentrons/protocol_engine/resources/labware_validation.py +7 -1
  47. opentrons/protocol_engine/state/_well_math.py +2 -2
  48. opentrons/protocol_engine/state/commands.py +14 -28
  49. opentrons/protocol_engine/state/frustum_helpers.py +11 -7
  50. opentrons/protocol_engine/state/labware.py +12 -0
  51. opentrons/protocol_engine/state/modules.py +1 -1
  52. opentrons/protocol_engine/state/pipettes.py +8 -0
  53. opentrons/protocol_engine/state/tips.py +46 -83
  54. opentrons/protocol_engine/state/update_types.py +8 -23
  55. opentrons/protocol_engine/types/liquid_level_detection.py +68 -8
  56. opentrons/protocol_runner/legacy_command_mapper.py +12 -6
  57. opentrons/protocol_runner/run_orchestrator.py +1 -1
  58. opentrons/protocols/advanced_control/transfers/common.py +54 -11
  59. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +55 -28
  60. opentrons/protocols/api_support/definitions.py +1 -1
  61. opentrons/types.py +6 -6
  62. {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/METADATA +4 -4
  63. {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/RECORD +67 -66
  64. {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/LICENSE +0 -0
  65. {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/WHEEL +0 -0
  66. {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/entry_points.txt +0 -0
  67. {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.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(
@@ -27,42 +27,102 @@ class SimulatedProbeResult(BaseModel):
27
27
  return data
28
28
 
29
29
  def __add__(
30
- self, other: float | SimulatedProbeResult
30
+ self, other: float | int | SimulatedProbeResult
31
31
  ) -> float | SimulatedProbeResult:
32
32
  """Bypass addition and just return self."""
33
33
  return self
34
34
 
35
35
  def __sub__(
36
- self, other: float | SimulatedProbeResult
36
+ self, other: float | int | SimulatedProbeResult
37
37
  ) -> float | SimulatedProbeResult:
38
38
  """Bypass subtraction and just return self."""
39
39
  return self
40
40
 
41
41
  def __radd__(
42
- self, other: float | SimulatedProbeResult
42
+ self, other: float | int | SimulatedProbeResult
43
43
  ) -> float | SimulatedProbeResult:
44
44
  """Bypass addition and just return self."""
45
45
  return self
46
46
 
47
47
  def __rsub__(
48
- self, other: float | SimulatedProbeResult
48
+ self, other: float | int | SimulatedProbeResult
49
49
  ) -> float | SimulatedProbeResult:
50
50
  """Bypass subtraction and just return self."""
51
51
  return self
52
52
 
53
- def __gt__(self, other: float | SimulatedProbeResult) -> bool:
53
+ def __mul__(
54
+ self, other: float | int | SimulatedProbeResult
55
+ ) -> float | SimulatedProbeResult:
56
+ """Bypass multiplication and just return self."""
57
+ return self
58
+
59
+ def __rmul__(
60
+ self, other: float | int | SimulatedProbeResult
61
+ ) -> float | SimulatedProbeResult:
62
+ """Bypass multiplication and just return self."""
63
+ return self
64
+
65
+ def __truediv__(
66
+ self, other: float | int | SimulatedProbeResult
67
+ ) -> float | SimulatedProbeResult:
68
+ """Bypass division and just return self."""
69
+ return self
70
+
71
+ def __rtruediv__(
72
+ self, other: float | int | SimulatedProbeResult
73
+ ) -> float | SimulatedProbeResult:
74
+ """Bypass division and just return self."""
75
+ return self
76
+
77
+ def __pow__(
78
+ self, other: float | int | SimulatedProbeResult
79
+ ) -> float | SimulatedProbeResult:
80
+ """Bypass exponent math and just return self."""
81
+ return self
82
+
83
+ def __rpow__(
84
+ self, other: float | int | SimulatedProbeResult
85
+ ) -> float | SimulatedProbeResult:
86
+ """Bypass exponent math and just return self."""
87
+ return self
88
+
89
+ def __mod__(
90
+ self, other: float | int | SimulatedProbeResult
91
+ ) -> float | SimulatedProbeResult:
92
+ """Bypass modulus and just return self."""
93
+ return self
94
+
95
+ def __rmod__(
96
+ self, other: float | int | SimulatedProbeResult
97
+ ) -> float | SimulatedProbeResult:
98
+ """Bypass modulus and just return self."""
99
+ return self
100
+
101
+ def __floordiv__(
102
+ self, other: float | int | SimulatedProbeResult
103
+ ) -> float | SimulatedProbeResult:
104
+ """Bypass floor division and just return self."""
105
+ return self
106
+
107
+ def __rfloordiv__(
108
+ self, other: float | int | SimulatedProbeResult
109
+ ) -> float | SimulatedProbeResult:
110
+ """Bypass floor division and just return self."""
111
+ return self
112
+
113
+ def __gt__(self, other: float | int | SimulatedProbeResult) -> bool:
54
114
  """Bypass 'greater than' and just return self."""
55
115
  return True
56
116
 
57
- def __lt__(self, other: float | SimulatedProbeResult) -> bool:
117
+ def __lt__(self, other: float | int | SimulatedProbeResult) -> bool:
58
118
  """Bypass 'less than' and just return self."""
59
119
  return False
60
120
 
61
- def __ge__(self, other: float | SimulatedProbeResult) -> bool:
121
+ def __ge__(self, other: float | int | SimulatedProbeResult) -> bool:
62
122
  """Bypass 'greater than or eaqual to' and just return self."""
63
123
  return True
64
124
 
65
- def __le__(self, other: float | SimulatedProbeResult) -> bool:
125
+ def __le__(self, other: float | int | SimulatedProbeResult) -> bool:
66
126
  """Bypass 'less than or equal to' and just return self."""
67
127
  return False
68
128
 
@@ -634,17 +634,24 @@ 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
-
647
- succeeded_command = pe_commands.LoadLabware.model_construct(
654
+ succeeded_command = pe_commands.LoadLabware(
648
655
  id=command_id,
649
656
  key=command_id,
650
657
  status=pe_commands.CommandStatus.SUCCEEDED,
@@ -665,8 +672,7 @@ class LegacyCommandMapper:
665
672
  labware_load_info.labware_definition
666
673
  ),
667
674
  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
675
+ locationSequence=location_sequence,
670
676
  ),
671
677
  )
672
678
  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