opentrons 8.3.1a1__py2.py3-none-any.whl → 8.4.0a0__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 (191) hide show
  1. opentrons/calibration_storage/ot2/mark_bad_calibration.py +2 -0
  2. opentrons/calibration_storage/ot2/tip_length.py +6 -6
  3. opentrons/config/advanced_settings.py +9 -11
  4. opentrons/config/feature_flags.py +0 -4
  5. opentrons/config/reset.py +7 -2
  6. opentrons/drivers/asyncio/communication/__init__.py +2 -0
  7. opentrons/drivers/asyncio/communication/async_serial.py +4 -0
  8. opentrons/drivers/asyncio/communication/errors.py +41 -8
  9. opentrons/drivers/asyncio/communication/serial_connection.py +36 -10
  10. opentrons/drivers/flex_stacker/__init__.py +9 -3
  11. opentrons/drivers/flex_stacker/abstract.py +140 -15
  12. opentrons/drivers/flex_stacker/driver.py +593 -47
  13. opentrons/drivers/flex_stacker/errors.py +64 -0
  14. opentrons/drivers/flex_stacker/simulator.py +222 -24
  15. opentrons/drivers/flex_stacker/types.py +211 -15
  16. opentrons/drivers/flex_stacker/utils.py +19 -0
  17. opentrons/execute.py +4 -2
  18. opentrons/hardware_control/api.py +5 -0
  19. opentrons/hardware_control/backends/flex_protocol.py +4 -0
  20. opentrons/hardware_control/backends/ot3controller.py +12 -1
  21. opentrons/hardware_control/backends/ot3simulator.py +3 -0
  22. opentrons/hardware_control/backends/subsystem_manager.py +7 -2
  23. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +10 -6
  24. opentrons/hardware_control/instruments/ot3/pipette_handler.py +59 -6
  25. opentrons/hardware_control/modules/__init__.py +12 -1
  26. opentrons/hardware_control/modules/absorbance_reader.py +11 -9
  27. opentrons/hardware_control/modules/flex_stacker.py +498 -0
  28. opentrons/hardware_control/modules/heater_shaker.py +12 -10
  29. opentrons/hardware_control/modules/magdeck.py +5 -1
  30. opentrons/hardware_control/modules/tempdeck.py +5 -1
  31. opentrons/hardware_control/modules/thermocycler.py +15 -14
  32. opentrons/hardware_control/modules/types.py +191 -1
  33. opentrons/hardware_control/modules/utils.py +3 -0
  34. opentrons/hardware_control/motion_utilities.py +20 -0
  35. opentrons/hardware_control/ot3api.py +145 -15
  36. opentrons/hardware_control/protocols/liquid_handler.py +47 -1
  37. opentrons/hardware_control/types.py +6 -0
  38. opentrons/legacy_commands/commands.py +19 -3
  39. opentrons/legacy_commands/helpers.py +15 -0
  40. opentrons/legacy_commands/types.py +3 -2
  41. opentrons/protocol_api/__init__.py +2 -0
  42. opentrons/protocol_api/_liquid.py +39 -8
  43. opentrons/protocol_api/_liquid_properties.py +20 -19
  44. opentrons/protocol_api/_transfer_liquid_validation.py +91 -0
  45. opentrons/protocol_api/core/common.py +3 -1
  46. opentrons/protocol_api/core/engine/deck_conflict.py +11 -1
  47. opentrons/protocol_api/core/engine/instrument.py +1233 -65
  48. opentrons/protocol_api/core/engine/labware.py +8 -4
  49. opentrons/protocol_api/core/engine/load_labware_params.py +68 -10
  50. opentrons/protocol_api/core/engine/module_core.py +118 -2
  51. opentrons/protocol_api/core/engine/protocol.py +253 -11
  52. opentrons/protocol_api/core/engine/stringify.py +19 -8
  53. opentrons/protocol_api/core/engine/transfer_components_executor.py +853 -0
  54. opentrons/protocol_api/core/engine/well.py +60 -5
  55. opentrons/protocol_api/core/instrument.py +65 -19
  56. opentrons/protocol_api/core/labware.py +6 -2
  57. opentrons/protocol_api/core/legacy/labware_offset_provider.py +7 -3
  58. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +69 -21
  59. opentrons/protocol_api/core/legacy/legacy_labware_core.py +8 -4
  60. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +36 -0
  61. opentrons/protocol_api/core/legacy/legacy_well_core.py +25 -1
  62. opentrons/protocol_api/core/legacy/load_info.py +4 -12
  63. opentrons/protocol_api/core/legacy/module_geometry.py +6 -1
  64. opentrons/protocol_api/core/legacy/well_geometry.py +3 -3
  65. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +67 -21
  66. opentrons/protocol_api/core/module.py +43 -0
  67. opentrons/protocol_api/core/protocol.py +33 -0
  68. opentrons/protocol_api/core/well.py +21 -1
  69. opentrons/protocol_api/instrument_context.py +245 -123
  70. opentrons/protocol_api/labware.py +75 -11
  71. opentrons/protocol_api/module_contexts.py +140 -0
  72. opentrons/protocol_api/protocol_context.py +156 -16
  73. opentrons/protocol_api/validation.py +51 -41
  74. opentrons/protocol_engine/__init__.py +21 -2
  75. opentrons/protocol_engine/actions/actions.py +5 -5
  76. opentrons/protocol_engine/clients/sync_client.py +6 -0
  77. opentrons/protocol_engine/commands/__init__.py +30 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +0 -1
  79. opentrons/protocol_engine/commands/air_gap_in_place.py +3 -2
  80. opentrons/protocol_engine/commands/aspirate.py +6 -2
  81. opentrons/protocol_engine/commands/aspirate_in_place.py +3 -1
  82. opentrons/protocol_engine/commands/aspirate_while_tracking.py +237 -0
  83. opentrons/protocol_engine/commands/blow_out.py +2 -0
  84. opentrons/protocol_engine/commands/blow_out_in_place.py +4 -1
  85. opentrons/protocol_engine/commands/command_unions.py +69 -0
  86. opentrons/protocol_engine/commands/configure_for_volume.py +3 -0
  87. opentrons/protocol_engine/commands/dispense.py +3 -1
  88. opentrons/protocol_engine/commands/dispense_in_place.py +3 -0
  89. opentrons/protocol_engine/commands/dispense_while_tracking.py +240 -0
  90. opentrons/protocol_engine/commands/drop_tip.py +23 -1
  91. opentrons/protocol_engine/commands/evotip_dispense.py +6 -7
  92. opentrons/protocol_engine/commands/evotip_seal_pipette.py +2 -9
  93. opentrons/protocol_engine/commands/evotip_unseal_pipette.py +1 -7
  94. opentrons/protocol_engine/commands/flex_stacker/__init__.py +106 -0
  95. opentrons/protocol_engine/commands/flex_stacker/close_latch.py +72 -0
  96. opentrons/protocol_engine/commands/flex_stacker/common.py +15 -0
  97. opentrons/protocol_engine/commands/flex_stacker/empty.py +161 -0
  98. opentrons/protocol_engine/commands/flex_stacker/fill.py +164 -0
  99. opentrons/protocol_engine/commands/flex_stacker/open_latch.py +70 -0
  100. opentrons/protocol_engine/commands/flex_stacker/prepare_shuttle.py +112 -0
  101. opentrons/protocol_engine/commands/flex_stacker/retrieve.py +394 -0
  102. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +190 -0
  103. opentrons/protocol_engine/commands/flex_stacker/store.py +288 -0
  104. opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
  105. opentrons/protocol_engine/commands/labware_handling_common.py +24 -0
  106. opentrons/protocol_engine/commands/liquid_probe.py +21 -12
  107. opentrons/protocol_engine/commands/load_labware.py +42 -39
  108. opentrons/protocol_engine/commands/load_lid.py +21 -13
  109. opentrons/protocol_engine/commands/load_lid_stack.py +130 -47
  110. opentrons/protocol_engine/commands/load_module.py +18 -17
  111. opentrons/protocol_engine/commands/load_pipette.py +3 -0
  112. opentrons/protocol_engine/commands/move_labware.py +139 -20
  113. opentrons/protocol_engine/commands/pick_up_tip.py +5 -2
  114. opentrons/protocol_engine/commands/pipetting_common.py +154 -7
  115. opentrons/protocol_engine/commands/prepare_to_aspirate.py +3 -1
  116. opentrons/protocol_engine/commands/reload_labware.py +6 -19
  117. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +3 -1
  118. opentrons/protocol_engine/errors/__init__.py +8 -0
  119. opentrons/protocol_engine/errors/exceptions.py +50 -0
  120. opentrons/protocol_engine/execution/equipment.py +123 -106
  121. opentrons/protocol_engine/execution/labware_movement.py +8 -6
  122. opentrons/protocol_engine/execution/pipetting.py +233 -26
  123. opentrons/protocol_engine/execution/tip_handler.py +14 -5
  124. opentrons/protocol_engine/labware_offset_standardization.py +173 -0
  125. opentrons/protocol_engine/protocol_engine.py +22 -13
  126. opentrons/protocol_engine/resources/deck_configuration_provider.py +94 -2
  127. opentrons/protocol_engine/resources/deck_data_provider.py +1 -1
  128. opentrons/protocol_engine/resources/labware_data_provider.py +32 -12
  129. opentrons/protocol_engine/resources/labware_validation.py +7 -5
  130. opentrons/protocol_engine/slot_standardization.py +11 -23
  131. opentrons/protocol_engine/state/addressable_areas.py +84 -46
  132. opentrons/protocol_engine/state/frustum_helpers.py +26 -10
  133. opentrons/protocol_engine/state/geometry.py +683 -100
  134. opentrons/protocol_engine/state/labware.py +252 -55
  135. opentrons/protocol_engine/state/module_substates/__init__.py +4 -0
  136. opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +68 -0
  137. opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +22 -0
  138. opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +13 -0
  139. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +20 -0
  140. opentrons/protocol_engine/state/modules.py +178 -52
  141. opentrons/protocol_engine/state/pipettes.py +54 -0
  142. opentrons/protocol_engine/state/state.py +1 -1
  143. opentrons/protocol_engine/state/tips.py +14 -0
  144. opentrons/protocol_engine/state/update_types.py +180 -25
  145. opentrons/protocol_engine/state/wells.py +54 -8
  146. opentrons/protocol_engine/types/__init__.py +292 -0
  147. opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
  148. opentrons/protocol_engine/types/command_annotations.py +53 -0
  149. opentrons/protocol_engine/types/deck_configuration.py +72 -0
  150. opentrons/protocol_engine/types/execution.py +96 -0
  151. opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
  152. opentrons/protocol_engine/types/instrument.py +47 -0
  153. opentrons/protocol_engine/types/instrument_sensors.py +47 -0
  154. opentrons/protocol_engine/types/labware.py +110 -0
  155. opentrons/protocol_engine/types/labware_movement.py +22 -0
  156. opentrons/protocol_engine/types/labware_offset_location.py +108 -0
  157. opentrons/protocol_engine/types/labware_offset_vector.py +33 -0
  158. opentrons/protocol_engine/types/liquid.py +40 -0
  159. opentrons/protocol_engine/types/liquid_class.py +59 -0
  160. opentrons/protocol_engine/types/liquid_handling.py +13 -0
  161. opentrons/protocol_engine/types/liquid_level_detection.py +137 -0
  162. opentrons/protocol_engine/types/location.py +193 -0
  163. opentrons/protocol_engine/types/module.py +269 -0
  164. opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
  165. opentrons/protocol_engine/types/run_time_parameters.py +133 -0
  166. opentrons/protocol_engine/types/tip.py +18 -0
  167. opentrons/protocol_engine/types/util.py +21 -0
  168. opentrons/protocol_engine/types/well_position.py +107 -0
  169. opentrons/protocol_reader/extract_labware_definitions.py +7 -3
  170. opentrons/protocol_reader/file_format_validator.py +5 -3
  171. opentrons/protocol_runner/json_translator.py +4 -2
  172. opentrons/protocol_runner/legacy_command_mapper.py +6 -2
  173. opentrons/protocol_runner/run_orchestrator.py +4 -1
  174. opentrons/protocols/advanced_control/transfers/common.py +48 -1
  175. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +204 -0
  176. opentrons/protocols/api_support/definitions.py +1 -1
  177. opentrons/protocols/api_support/instrument.py +16 -3
  178. opentrons/protocols/labware.py +5 -6
  179. opentrons/protocols/models/__init__.py +0 -21
  180. opentrons/simulate.py +4 -2
  181. opentrons/types.py +15 -6
  182. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/METADATA +4 -4
  183. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/RECORD +187 -147
  184. opentrons/calibration_storage/ot2/models/defaults.py +0 -0
  185. opentrons/calibration_storage/ot3/models/defaults.py +0 -0
  186. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  187. opentrons/protocol_engine/types.py +0 -1311
  188. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/LICENSE +0 -0
  189. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/WHEEL +0 -0
  190. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/entry_points.txt +0 -0
  191. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,8 @@
1
1
  """Models and implementation for the ``moveLabware`` command."""
2
2
 
3
3
  from __future__ import annotations
4
- from typing import TYPE_CHECKING, Optional, Type, Any
4
+ from typing import TYPE_CHECKING, Optional, Type, Any, List
5
+ from typing_extensions import TypedDict # note: need this instead of typing for py<3.12
5
6
 
6
7
  from pydantic.json_schema import SkipJsonSchema
7
8
  from pydantic import BaseModel, Field
@@ -18,7 +19,7 @@ from opentrons.types import Point
18
19
  from ..types import (
19
20
  ModuleModel,
20
21
  CurrentWell,
21
- LabwareLocation,
22
+ LoadableLabwareLocation,
22
23
  DeckSlotLocation,
23
24
  ModuleLocation,
24
25
  OnLabwareLocation,
@@ -26,6 +27,9 @@ from ..types import (
26
27
  LabwareMovementStrategy,
27
28
  LabwareOffsetVector,
28
29
  LabwareMovementOffsetData,
30
+ LabwareLocationSequence,
31
+ NotOnDeckLocationSequenceComponent,
32
+ OFF_DECK_LOCATION,
29
33
  )
30
34
  from ..errors import (
31
35
  LabwareMovementNotAllowedError,
@@ -64,7 +68,9 @@ class MoveLabwareParams(BaseModel):
64
68
  """Input parameters for a ``moveLabware`` command."""
65
69
 
66
70
  labwareId: str = Field(..., description="The ID of the labware to move.")
67
- newLocation: LabwareLocation = Field(..., description="Where to move the labware.")
71
+ newLocation: LoadableLabwareLocation = Field(
72
+ ..., description="Where to move the labware."
73
+ )
68
74
  strategy: LabwareMovementStrategy = Field(
69
75
  ...,
70
76
  description="Whether to use the gripper to perform the labware movement"
@@ -100,6 +106,31 @@ class MoveLabwareResult(BaseModel):
100
106
  " so the default of (0, 0, 0) will be used."
101
107
  ),
102
108
  )
109
+ eventualDestinationLocationSequence: LabwareLocationSequence | None = Field(
110
+ None,
111
+ description=(
112
+ "The full location in which this labware will eventually reside. This will typically be the same as its "
113
+ "immediate destination, but if this labware is going to the trash then this field will be off deck."
114
+ ),
115
+ )
116
+ immediateDestinationLocationSequence: LabwareLocationSequence | None = Field(
117
+ None,
118
+ description=(
119
+ "The full location to which this labware is being moved, right now."
120
+ ),
121
+ )
122
+ originLocationSequence: LabwareLocationSequence | None = Field(
123
+ None,
124
+ description="The full location down to the deck of the labware before this command.",
125
+ )
126
+
127
+
128
+ class ErrorDetails(TypedDict):
129
+ """Location details for a failed gripper move."""
130
+
131
+ originLocationSequence: LabwareLocationSequence
132
+ immediateDestinationLocationSequence: LabwareLocationSequence
133
+ eventualDestinationLocationSequence: LabwareLocationSequence
103
134
 
104
135
 
105
136
  class GripperMovementError(ErrorOccurrence):
@@ -112,6 +143,8 @@ class GripperMovementError(ErrorOccurrence):
112
143
 
113
144
  errorType: Literal["gripperMovement"] = "gripperMovement"
114
145
 
146
+ errorInfo: ErrorDetails
147
+
115
148
 
116
149
  _ExecuteReturn = SuccessData[MoveLabwareResult] | DefinedErrorData[GripperMovementError]
117
150
 
@@ -152,6 +185,11 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
152
185
  f"Cannot move fixed trash labware '{current_labware_definition.parameters.loadName}'."
153
186
  )
154
187
 
188
+ origin_location_sequence = self._state_view.geometry.get_location_sequence(
189
+ params.labwareId
190
+ )
191
+ eventual_destination_location_sequence: LabwareLocationSequence | None = None
192
+
155
193
  if isinstance(params.newLocation, AddressableAreaLocation):
156
194
  area_name = params.newLocation.addressableAreaName
157
195
  if (
@@ -181,9 +219,19 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
181
219
  y=0,
182
220
  z=0,
183
221
  )
222
+ eventual_destination_location_sequence = [
223
+ NotOnDeckLocationSequenceComponent(
224
+ logicalLocationName=OFF_DECK_LOCATION
225
+ )
226
+ ]
184
227
  elif fixture_validation.is_trash(area_name):
185
228
  # When dropping labware in the trash bins we want to ensure they are lids
186
229
  # and enforce a y-axis drop offset to ensure they fall within the trash bin
230
+ eventual_destination_location_sequence = [
231
+ NotOnDeckLocationSequenceComponent(
232
+ logicalLocationName=OFF_DECK_LOCATION
233
+ )
234
+ ]
187
235
  if labware_validation.validate_definition_is_lid(
188
236
  self._state_view.labware.get_definition(params.labwareId)
189
237
  ):
@@ -220,7 +268,7 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
220
268
  )
221
269
 
222
270
  # Check that labware and destination do not have labware on top
223
- self._state_view.labware.raise_if_labware_has_labware_on_top(
271
+ self._state_view.labware.raise_if_labware_has_non_lid_labware_on_top(
224
272
  labware_id=params.labwareId
225
273
  )
226
274
 
@@ -282,7 +330,6 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
282
330
  raise LabwareMovementNotAllowedError(
283
331
  f"Cannot move adapter '{current_labware_definition.parameters.loadName}' with gripper."
284
332
  )
285
-
286
333
  validated_current_loc = (
287
334
  self._state_view.geometry.ensure_valid_gripper_location(
288
335
  current_labware.location
@@ -299,6 +346,16 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
299
346
  if trash_lid_drop_offset:
300
347
  user_offset_data.dropOffset += trash_lid_drop_offset
301
348
 
349
+ immediate_destination_location_sequence = (
350
+ self._state_view.geometry.get_predicted_location_sequence(
351
+ validated_new_loc
352
+ )
353
+ )
354
+ if eventual_destination_location_sequence is None:
355
+ eventual_destination_location_sequence = (
356
+ immediate_destination_location_sequence
357
+ )
358
+
302
359
  try:
303
360
  # Skips gripper moves when using virtual gripper
304
361
  await self._labware_movement.move_labware_with_gripper(
@@ -315,20 +372,23 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
315
372
  # todo(mm, 2024-09-26): Catch LabwareNotPickedUpError when that exists and
316
373
  # move_labware_with_gripper() raises it.
317
374
  ) as exception:
318
- gripper_movement_error: GripperMovementError | None = (
319
- GripperMovementError(
320
- id=self._model_utils.generate_id(),
321
- createdAt=self._model_utils.get_timestamp(),
322
- errorCode=exception.code.value.code,
323
- detail=exception.code.value.detail,
324
- wrappedErrors=[
325
- ErrorOccurrence.from_failed(
326
- id=self._model_utils.generate_id(),
327
- createdAt=self._model_utils.get_timestamp(),
328
- error=exception,
329
- )
330
- ],
331
- )
375
+ gripper_movement_error: GripperMovementError | None = GripperMovementError(
376
+ id=self._model_utils.generate_id(),
377
+ createdAt=self._model_utils.get_timestamp(),
378
+ errorCode=exception.code.value.code,
379
+ detail=exception.code.value.detail,
380
+ errorInfo={
381
+ "originLocationSequence": origin_location_sequence,
382
+ "immediateDestinationLocationSequence": immediate_destination_location_sequence,
383
+ "eventualDestinationLocationSequence": eventual_destination_location_sequence,
384
+ },
385
+ wrappedErrors=[
386
+ ErrorOccurrence.from_failed(
387
+ id=self._model_utils.generate_id(),
388
+ createdAt=self._model_utils.get_timestamp(),
389
+ error=exception,
390
+ )
391
+ ],
332
392
  )
333
393
  else:
334
394
  gripper_movement_error = None
@@ -344,7 +404,27 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
344
404
 
345
405
  elif params.strategy == LabwareMovementStrategy.MANUAL_MOVE_WITH_PAUSE:
346
406
  # Pause to allow for manual labware movement
407
+ immediate_destination_location_sequence = (
408
+ self._state_view.geometry.get_predicted_location_sequence(
409
+ params.newLocation
410
+ )
411
+ )
412
+ if eventual_destination_location_sequence is None:
413
+ eventual_destination_location_sequence = (
414
+ immediate_destination_location_sequence
415
+ )
416
+
347
417
  await self._run_control.wait_for_resume()
418
+ else:
419
+ immediate_destination_location_sequence = (
420
+ self._state_view.geometry.get_predicted_location_sequence(
421
+ params.newLocation
422
+ )
423
+ )
424
+ if eventual_destination_location_sequence is None:
425
+ eventual_destination_location_sequence = (
426
+ immediate_destination_location_sequence
427
+ )
348
428
 
349
429
  # We may have just moved the labware that contains the current well out from
350
430
  # under the pipette. Clear the current location to reflect the fact that the
@@ -364,8 +444,47 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
364
444
  new_offset_id=new_offset_id,
365
445
  )
366
446
 
447
+ if labware_validation.validate_definition_is_lid(
448
+ definition=self._state_view.labware.get_definition(params.labwareId)
449
+ ):
450
+ parent_updates: List[str] = []
451
+ lid_updates: List[str | None] = []
452
+ # when moving a lid between locations we need to:
453
+ if (
454
+ isinstance(current_labware.location, OnLabwareLocation)
455
+ and self._state_view.labware.get_lid_by_labware_id(
456
+ current_labware.location.labwareId
457
+ )
458
+ is not None
459
+ ):
460
+ # if the source location was a parent labware and not a lid stack or lid, update the parent labware lid ID to None (no more lid)
461
+ parent_updates.append(current_labware.location.labwareId)
462
+ lid_updates.append(None)
463
+
464
+ # If we're moving to a non lid object, add to the setlids list of things to do
465
+ if isinstance(
466
+ available_new_location, OnLabwareLocation
467
+ ) and not labware_validation.validate_definition_is_lid(
468
+ self._state_view.labware.get_definition(
469
+ available_new_location.labwareId
470
+ )
471
+ ):
472
+ parent_updates.append(available_new_location.labwareId)
473
+ lid_updates.append(params.labwareId)
474
+ # Add to setlids
475
+ if len(parent_updates) > 0:
476
+ state_update.set_lids(
477
+ parent_labware_ids=parent_updates,
478
+ lid_ids=lid_updates,
479
+ )
480
+
367
481
  return SuccessData(
368
- public=MoveLabwareResult(offsetId=new_offset_id),
482
+ public=MoveLabwareResult(
483
+ offsetId=new_offset_id,
484
+ originLocationSequence=origin_location_sequence,
485
+ immediateDestinationLocationSequence=immediate_destination_location_sequence,
486
+ eventualDestinationLocationSequence=eventual_destination_location_sequence,
487
+ ),
369
488
  state_update=state_update,
370
489
  )
371
490
 
@@ -150,7 +150,7 @@ class PickUpTipImplementation(AbstractCommandImpl[PickUpTipParams, _ExecuteRetur
150
150
  pipette_id=pipette_id,
151
151
  tip_geometry=e.tip_geometry,
152
152
  )
153
- .set_fluid_empty(pipette_id=pipette_id)
153
+ .set_fluid_empty(pipette_id=pipette_id, clean_tip=True)
154
154
  .mark_tips_as_used(
155
155
  pipette_id=pipette_id, labware_id=labware_id, well_name=well_name
156
156
  )
@@ -188,7 +188,10 @@ class PickUpTipImplementation(AbstractCommandImpl[PickUpTipParams, _ExecuteRetur
188
188
  .mark_tips_as_used(
189
189
  pipette_id=pipette_id, labware_id=labware_id, well_name=well_name
190
190
  )
191
- .set_fluid_empty(pipette_id=pipette_id)
191
+ .set_fluid_empty(pipette_id=pipette_id, clean_tip=True)
192
+ .set_pipette_ready_to_aspirate(
193
+ pipette_id=pipette_id, ready_to_aspirate=True
194
+ )
192
195
  )
193
196
  return SuccessData(
194
197
  public=PickUpTipResult(
@@ -1,8 +1,8 @@
1
1
  """Common pipetting command base models."""
2
2
 
3
3
  from __future__ import annotations
4
- from typing import Literal, Tuple, TYPE_CHECKING
5
-
4
+ from typing import Literal, Tuple, TYPE_CHECKING, Optional
5
+ import numpy
6
6
  from typing_extensions import TypedDict
7
7
  from pydantic import BaseModel, Field
8
8
 
@@ -20,6 +20,10 @@ if TYPE_CHECKING:
20
20
  from ..notes import CommandNoteAdder
21
21
 
22
22
 
23
+ DEFAULT_CORRECTION_VOLUME = 0.0
24
+ """Default correction volume (uL) for any aspirate/ dispense volume."""
25
+
26
+
23
27
  class PipetteIdMixin(BaseModel):
24
28
  """Mixin for command requests that take a pipette ID."""
25
29
 
@@ -41,6 +45,10 @@ class AspirateVolumeMixin(BaseModel):
41
45
  " There is some tolerance for floating point rounding errors.",
42
46
  ge=0,
43
47
  )
48
+ correctionVolume: Optional[float] = Field(
49
+ None,
50
+ description="The correction volume in uL.",
51
+ )
44
52
 
45
53
 
46
54
  class DispenseVolumeMixin(BaseModel):
@@ -53,6 +61,10 @@ class DispenseVolumeMixin(BaseModel):
53
61
  " There is some tolerance for floating point rounding errors.",
54
62
  ge=0,
55
63
  )
64
+ correctionVolume: Optional[float] = Field(
65
+ None,
66
+ description="The correction volume in uL.",
67
+ )
56
68
 
57
69
 
58
70
  class FlowRateMixin(BaseModel):
@@ -176,18 +188,69 @@ async def aspirate_in_place(
176
188
  pipette_id: str,
177
189
  volume: float,
178
190
  flow_rate: float,
191
+ correction_volume: float,
179
192
  location_if_error: ErrorLocationInfo,
180
193
  command_note_adder: CommandNoteAdder,
181
194
  pipetting: PipettingHandler,
182
195
  model_utils: ModelUtils,
183
196
  ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]:
184
- """Execute an aspirate in place microoperation."""
197
+ """Execute an aspirate in place micro-operation."""
185
198
  try:
186
199
  volume_aspirated = await pipetting.aspirate_in_place(
187
200
  pipette_id=pipette_id,
188
201
  volume=volume,
189
202
  flow_rate=flow_rate,
190
203
  command_note_adder=command_note_adder,
204
+ correction_volume=correction_volume,
205
+ )
206
+ except PipetteOverpressureError as e:
207
+ return DefinedErrorData(
208
+ public=OverpressureError(
209
+ id=model_utils.generate_id(),
210
+ createdAt=model_utils.get_timestamp(),
211
+ wrappedErrors=[
212
+ ErrorOccurrence.from_failed(
213
+ id=model_utils.generate_id(),
214
+ createdAt=model_utils.get_timestamp(),
215
+ error=e,
216
+ )
217
+ ],
218
+ errorInfo=location_if_error,
219
+ ),
220
+ state_update=StateUpdate().set_fluid_unknown(pipette_id=pipette_id),
221
+ )
222
+ else:
223
+ return SuccessData(
224
+ public=BaseLiquidHandlingResult(
225
+ volume=volume_aspirated,
226
+ ),
227
+ state_update=StateUpdate().set_fluid_aspirated(
228
+ pipette_id=pipette_id,
229
+ fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=volume_aspirated),
230
+ ),
231
+ )
232
+
233
+
234
+ async def aspirate_while_tracking(
235
+ pipette_id: str,
236
+ labware_id: str,
237
+ well_name: str,
238
+ volume: float,
239
+ flow_rate: float,
240
+ location_if_error: ErrorLocationInfo,
241
+ command_note_adder: CommandNoteAdder,
242
+ pipetting: PipettingHandler,
243
+ model_utils: ModelUtils,
244
+ ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]:
245
+ """Execute an aspirate while tracking microoperation."""
246
+ try:
247
+ volume_aspirated = await pipetting.aspirate_while_tracking(
248
+ pipette_id=pipette_id,
249
+ labware_id=labware_id,
250
+ well_name=well_name,
251
+ volume=volume,
252
+ flow_rate=flow_rate,
253
+ command_note_adder=command_note_adder,
191
254
  )
192
255
  except PipetteOverpressureError as e:
193
256
  return DefinedErrorData(
@@ -217,22 +280,95 @@ async def aspirate_in_place(
217
280
  )
218
281
 
219
282
 
283
+ async def dispense_while_tracking(
284
+ pipette_id: str,
285
+ labware_id: str,
286
+ well_name: str,
287
+ volume: float,
288
+ flow_rate: float,
289
+ push_out: float | None,
290
+ location_if_error: ErrorLocationInfo,
291
+ pipetting: PipettingHandler,
292
+ model_utils: ModelUtils,
293
+ ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]:
294
+ """Execute an dispense while tracking microoperation."""
295
+ # The current volume won't be none since it passed validation
296
+ current_volume = (
297
+ pipetting.get_state_view().pipettes.get_aspirated_volume(pipette_id) or 0.0
298
+ )
299
+ is_full_dispense = bool(numpy.isclose(current_volume - volume, 0))
300
+ ready = push_out == 0 if push_out is not None else not is_full_dispense
301
+ try:
302
+ volume_dispensed = await pipetting.dispense_while_tracking(
303
+ pipette_id=pipette_id,
304
+ labware_id=labware_id,
305
+ well_name=well_name,
306
+ volume=volume,
307
+ flow_rate=flow_rate,
308
+ push_out=push_out,
309
+ is_full_dispense=is_full_dispense,
310
+ )
311
+ except PipetteOverpressureError as e:
312
+ return DefinedErrorData(
313
+ public=OverpressureError(
314
+ id=model_utils.generate_id(),
315
+ createdAt=model_utils.get_timestamp(),
316
+ wrappedErrors=[
317
+ ErrorOccurrence.from_failed(
318
+ id=model_utils.generate_id(),
319
+ createdAt=model_utils.get_timestamp(),
320
+ error=e,
321
+ )
322
+ ],
323
+ errorInfo=location_if_error,
324
+ ),
325
+ state_update=StateUpdate()
326
+ .set_fluid_unknown(pipette_id=pipette_id)
327
+ .set_pipette_ready_to_aspirate(
328
+ pipette_id=pipette_id, ready_to_aspirate=False
329
+ ),
330
+ )
331
+ else:
332
+ return SuccessData(
333
+ public=BaseLiquidHandlingResult(
334
+ volume=volume_dispensed,
335
+ ),
336
+ state_update=StateUpdate()
337
+ .set_fluid_ejected(
338
+ pipette_id=pipette_id,
339
+ volume=volume_dispensed,
340
+ )
341
+ .set_pipette_ready_to_aspirate(
342
+ pipette_id=pipette_id, ready_to_aspirate=ready
343
+ ),
344
+ )
345
+
346
+
220
347
  async def dispense_in_place(
221
348
  pipette_id: str,
222
349
  volume: float,
223
350
  flow_rate: float,
224
351
  push_out: float | None,
352
+ correction_volume: float,
225
353
  location_if_error: ErrorLocationInfo,
226
354
  pipetting: PipettingHandler,
227
355
  model_utils: ModelUtils,
228
356
  ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]:
229
- """Dispense-in-place as a microoperation."""
357
+ """Dispense-in-place as a micro-operation."""
358
+ # The current volume won't be none since it passed validation
359
+ current_volume = (
360
+ pipetting.get_state_view().pipettes.get_aspirated_volume(pipette_id) or 0.0
361
+ )
362
+ is_full_dispense = bool(numpy.isclose(current_volume - volume, 0))
363
+ ready: bool = push_out == 0 if push_out is not None else not is_full_dispense
230
364
  try:
231
365
  volume = await pipetting.dispense_in_place(
232
366
  pipette_id=pipette_id,
233
367
  volume=volume,
234
368
  flow_rate=flow_rate,
235
369
  push_out=push_out,
370
+ is_full_dispense=is_full_dispense,
371
+ correction_volume=correction_volume,
236
372
  )
237
373
  except PipetteOverpressureError as e:
238
374
  return DefinedErrorData(
@@ -248,13 +384,19 @@ async def dispense_in_place(
248
384
  ],
249
385
  errorInfo=location_if_error,
250
386
  ),
251
- state_update=StateUpdate().set_fluid_unknown(pipette_id=pipette_id),
387
+ state_update=StateUpdate()
388
+ .set_fluid_unknown(pipette_id=pipette_id)
389
+ .set_pipette_ready_to_aspirate(
390
+ pipette_id=pipette_id, ready_to_aspirate=False
391
+ ),
252
392
  )
253
393
  else:
254
394
  return SuccessData(
255
395
  public=BaseLiquidHandlingResult(volume=volume),
256
- state_update=StateUpdate().set_fluid_ejected(
257
- pipette_id=pipette_id, volume=volume
396
+ state_update=StateUpdate()
397
+ .set_fluid_ejected(pipette_id=pipette_id, volume=volume)
398
+ .set_pipette_ready_to_aspirate(
399
+ pipette_id=pipette_id, ready_to_aspirate=ready
258
400
  ),
259
401
  )
260
402
 
@@ -290,3 +432,8 @@ async def blow_out_in_place(
290
432
  public=EmptyResult(),
291
433
  state_update=StateUpdate().set_fluid_empty(pipette_id=pipette_id),
292
434
  )
435
+
436
+
437
+ async def increase_evo_disp_count(pipette_id: str, pipetting: PipettingHandler) -> None:
438
+ """Tell a pipette to increase it's evo-tip-dispense-count in eeprom."""
439
+ await pipetting.increase_evo_disp_count(pipette_id)
@@ -84,7 +84,9 @@ class PrepareToAspirateImplementation(
84
84
  else:
85
85
  return SuccessData(
86
86
  public=PrepareToAspirateResult(),
87
- state_update=prepare_result.state_update,
87
+ state_update=prepare_result.state_update.set_pipette_ready_to_aspirate(
88
+ pipette_id=params.pipetteId, ready_to_aspirate=True
89
+ ),
88
90
  )
89
91
 
90
92
 
@@ -1,9 +1,11 @@
1
1
  """Reload labware command request, result, and implementation models."""
2
+
2
3
  from __future__ import annotations
3
4
  from pydantic import BaseModel, Field
4
5
  from typing import TYPE_CHECKING, Optional, Type
5
6
  from typing_extensions import Literal
6
7
 
8
+ from .labware_handling_common import LabwarePositionResultMixin
7
9
  from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
8
10
  from ..errors.error_occurrence import ErrorOccurrence
9
11
  from ..state.update_types import StateUpdate
@@ -24,27 +26,9 @@ class ReloadLabwareParams(BaseModel):
24
26
  )
25
27
 
26
28
 
27
- class ReloadLabwareResult(BaseModel):
29
+ class ReloadLabwareResult(LabwarePositionResultMixin):
28
30
  """Result data from the execution of a LoadLabware command."""
29
31
 
30
- labwareId: str = Field(
31
- ...,
32
- description="An ID to reference this labware in subsequent commands. Same as the one in the parameters.",
33
- )
34
- offsetId: Optional[str] = Field(
35
- # Default `None` instead of `...` so this field shows up as non-required in
36
- # OpenAPI. The server is allowed to omit it or make it null.
37
- None,
38
- description=(
39
- "An ID referencing the labware offset that will apply"
40
- " to the reloaded labware."
41
- " This offset will be in effect until the labware is moved"
42
- " with a `moveLabware` command."
43
- " Null or undefined means no offset applies,"
44
- " so the default of (0, 0, 0) will be used."
45
- ),
46
- )
47
-
48
32
 
49
33
  class ReloadLabwareImplementation(
50
34
  AbstractCommandImpl[ReloadLabwareParams, SuccessData[ReloadLabwareResult]]
@@ -77,6 +61,9 @@ class ReloadLabwareImplementation(
77
61
  public=ReloadLabwareResult(
78
62
  labwareId=params.labwareId,
79
63
  offsetId=reloaded_labware.offsetId,
64
+ locationSequence=self._state_view.geometry.get_predicted_location_sequence(
65
+ reloaded_labware.location
66
+ ),
80
67
  ),
81
68
  state_update=state_update,
82
69
  )
@@ -69,7 +69,9 @@ class UnsafeBlowOutInPlaceImplementation(
69
69
  )
70
70
  state_update = update_types.StateUpdate()
71
71
  state_update.set_fluid_empty(pipette_id=params.pipetteId)
72
-
72
+ state_update.set_pipette_ready_to_aspirate(
73
+ pipette_id=params.pipetteId, ready_to_aspirate=False
74
+ )
73
75
  return SuccessData(
74
76
  public=UnsafeBlowOutInPlaceResult(), state_update=state_update
75
77
  )
@@ -76,6 +76,7 @@ from .exceptions import (
76
76
  CommandNotAllowedError,
77
77
  InvalidLiquidHeightFound,
78
78
  LiquidHeightUnknownError,
79
+ LiquidVolumeUnknownError,
79
80
  IncompleteLabwareDefinitionError,
80
81
  IncompleteWellDefinitionError,
81
82
  OperationLocationNotInWellError,
@@ -84,6 +85,9 @@ from .exceptions import (
84
85
  InvalidLiquidError,
85
86
  LiquidClassDoesNotExistError,
86
87
  LiquidClassRedefinitionError,
88
+ OffsetLocationInvalidError,
89
+ FlexStackerLabwarePoolNotYetDefinedError,
90
+ FlexStackerNotLogicallyEmptyError,
87
91
  )
88
92
 
89
93
  from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError
@@ -164,11 +168,15 @@ __all__ = [
164
168
  "LocationIsLidDockSlotError",
165
169
  "InvalidAxisForRobotType",
166
170
  "NotSupportedOnRobotType",
171
+ "OffsetLocationInvalidError",
172
+ "FlexStackerLabwarePoolNotYetDefinedError",
173
+ "FlexStackerNotLogicallyEmptyError",
167
174
  # error occurrence models
168
175
  "ErrorOccurrence",
169
176
  "CommandNotAllowedError",
170
177
  "InvalidLiquidHeightFound",
171
178
  "LiquidHeightUnknownError",
179
+ "LiquidVolumeUnknownError",
172
180
  "IncompleteLabwareDefinitionError",
173
181
  "IncompleteWellDefinitionError",
174
182
  "OperationLocationNotInWellError",