opentrons 8.3.0a0__py2.py3-none-any.whl → 8.3.0a2__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 (229) hide show
  1. opentrons/calibration_storage/deck_configuration.py +3 -3
  2. opentrons/calibration_storage/file_operators.py +3 -3
  3. opentrons/calibration_storage/helpers.py +3 -1
  4. opentrons/calibration_storage/ot2/models/v1.py +16 -29
  5. opentrons/calibration_storage/ot2/tip_length.py +7 -4
  6. opentrons/calibration_storage/ot3/models/v1.py +14 -23
  7. opentrons/cli/analyze.py +18 -6
  8. opentrons/drivers/asyncio/communication/__init__.py +2 -0
  9. opentrons/drivers/asyncio/communication/errors.py +16 -3
  10. opentrons/drivers/asyncio/communication/serial_connection.py +24 -9
  11. opentrons/drivers/command_builder.py +2 -2
  12. opentrons/drivers/flex_stacker/__init__.py +9 -0
  13. opentrons/drivers/flex_stacker/abstract.py +89 -0
  14. opentrons/drivers/flex_stacker/driver.py +260 -0
  15. opentrons/drivers/flex_stacker/simulator.py +109 -0
  16. opentrons/drivers/flex_stacker/types.py +138 -0
  17. opentrons/drivers/heater_shaker/driver.py +18 -3
  18. opentrons/drivers/temp_deck/driver.py +13 -3
  19. opentrons/drivers/thermocycler/driver.py +17 -3
  20. opentrons/execute.py +3 -1
  21. opentrons/hardware_control/__init__.py +1 -2
  22. opentrons/hardware_control/api.py +28 -20
  23. opentrons/hardware_control/backends/flex_protocol.py +4 -6
  24. opentrons/hardware_control/backends/ot3controller.py +177 -59
  25. opentrons/hardware_control/backends/ot3simulator.py +10 -8
  26. opentrons/hardware_control/backends/ot3utils.py +3 -13
  27. opentrons/hardware_control/dev_types.py +2 -0
  28. opentrons/hardware_control/emulation/heater_shaker.py +4 -0
  29. opentrons/hardware_control/emulation/module_server/client.py +1 -1
  30. opentrons/hardware_control/emulation/module_server/server.py +5 -3
  31. opentrons/hardware_control/emulation/settings.py +3 -4
  32. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +2 -1
  33. opentrons/hardware_control/instruments/ot2/pipette.py +9 -21
  34. opentrons/hardware_control/instruments/ot2/pipette_handler.py +8 -1
  35. opentrons/hardware_control/instruments/ot3/gripper.py +2 -2
  36. opentrons/hardware_control/instruments/ot3/pipette.py +13 -22
  37. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -1
  38. opentrons/hardware_control/modules/mod_abc.py +2 -2
  39. opentrons/hardware_control/motion_utilities.py +68 -0
  40. opentrons/hardware_control/nozzle_manager.py +39 -41
  41. opentrons/hardware_control/ot3_calibration.py +1 -1
  42. opentrons/hardware_control/ot3api.py +34 -22
  43. opentrons/hardware_control/protocols/gripper_controller.py +3 -0
  44. opentrons/hardware_control/protocols/hardware_manager.py +5 -1
  45. opentrons/hardware_control/protocols/liquid_handler.py +18 -0
  46. opentrons/hardware_control/protocols/motion_controller.py +6 -0
  47. opentrons/hardware_control/robot_calibration.py +1 -1
  48. opentrons/hardware_control/types.py +61 -0
  49. opentrons/protocol_api/__init__.py +20 -1
  50. opentrons/protocol_api/_liquid.py +24 -49
  51. opentrons/protocol_api/_liquid_properties.py +754 -0
  52. opentrons/protocol_api/_types.py +24 -0
  53. opentrons/protocol_api/core/common.py +2 -0
  54. opentrons/protocol_api/core/engine/instrument.py +67 -10
  55. opentrons/protocol_api/core/engine/labware.py +29 -7
  56. opentrons/protocol_api/core/engine/protocol.py +130 -5
  57. opentrons/protocol_api/core/engine/robot.py +139 -0
  58. opentrons/protocol_api/core/engine/well.py +4 -1
  59. opentrons/protocol_api/core/instrument.py +42 -4
  60. opentrons/protocol_api/core/labware.py +13 -4
  61. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +34 -3
  62. opentrons/protocol_api/core/legacy/legacy_labware_core.py +13 -4
  63. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +32 -1
  64. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  65. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +34 -3
  66. opentrons/protocol_api/core/protocol.py +34 -1
  67. opentrons/protocol_api/core/robot.py +51 -0
  68. opentrons/protocol_api/instrument_context.py +145 -43
  69. opentrons/protocol_api/labware.py +231 -7
  70. opentrons/protocol_api/module_contexts.py +21 -17
  71. opentrons/protocol_api/protocol_context.py +125 -4
  72. opentrons/protocol_api/robot_context.py +204 -32
  73. opentrons/protocol_api/validation.py +261 -3
  74. opentrons/protocol_engine/__init__.py +4 -0
  75. opentrons/protocol_engine/actions/actions.py +2 -3
  76. opentrons/protocol_engine/clients/sync_client.py +18 -0
  77. opentrons/protocol_engine/commands/__init__.py +81 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +0 -2
  79. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +19 -5
  80. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +0 -1
  81. opentrons/protocol_engine/commands/absorbance_reader/read.py +32 -9
  82. opentrons/protocol_engine/commands/air_gap_in_place.py +160 -0
  83. opentrons/protocol_engine/commands/aspirate.py +103 -53
  84. opentrons/protocol_engine/commands/aspirate_in_place.py +55 -51
  85. opentrons/protocol_engine/commands/blow_out.py +44 -39
  86. opentrons/protocol_engine/commands/blow_out_in_place.py +21 -32
  87. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +13 -6
  88. opentrons/protocol_engine/commands/calibration/calibrate_module.py +1 -1
  89. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +3 -3
  90. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +1 -1
  91. opentrons/protocol_engine/commands/command.py +73 -66
  92. opentrons/protocol_engine/commands/command_unions.py +101 -1
  93. opentrons/protocol_engine/commands/comment.py +1 -1
  94. opentrons/protocol_engine/commands/configure_for_volume.py +10 -3
  95. opentrons/protocol_engine/commands/configure_nozzle_layout.py +6 -4
  96. opentrons/protocol_engine/commands/custom.py +6 -12
  97. opentrons/protocol_engine/commands/dispense.py +82 -48
  98. opentrons/protocol_engine/commands/dispense_in_place.py +71 -51
  99. opentrons/protocol_engine/commands/drop_tip.py +52 -31
  100. opentrons/protocol_engine/commands/drop_tip_in_place.py +13 -3
  101. opentrons/protocol_engine/commands/generate_command_schema.py +4 -11
  102. opentrons/protocol_engine/commands/get_next_tip.py +134 -0
  103. opentrons/protocol_engine/commands/get_tip_presence.py +1 -1
  104. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +1 -1
  105. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +1 -1
  106. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +1 -1
  107. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +1 -1
  108. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +1 -1
  109. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +1 -1
  110. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +10 -4
  111. opentrons/protocol_engine/commands/home.py +13 -4
  112. opentrons/protocol_engine/commands/liquid_probe.py +60 -25
  113. opentrons/protocol_engine/commands/load_labware.py +29 -7
  114. opentrons/protocol_engine/commands/load_lid.py +146 -0
  115. opentrons/protocol_engine/commands/load_lid_stack.py +189 -0
  116. opentrons/protocol_engine/commands/load_liquid.py +12 -4
  117. opentrons/protocol_engine/commands/load_liquid_class.py +144 -0
  118. opentrons/protocol_engine/commands/load_module.py +31 -10
  119. opentrons/protocol_engine/commands/load_pipette.py +19 -8
  120. opentrons/protocol_engine/commands/magnetic_module/disengage.py +1 -1
  121. opentrons/protocol_engine/commands/magnetic_module/engage.py +1 -1
  122. opentrons/protocol_engine/commands/move_labware.py +19 -6
  123. opentrons/protocol_engine/commands/move_relative.py +35 -25
  124. opentrons/protocol_engine/commands/move_to_addressable_area.py +40 -27
  125. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +53 -32
  126. opentrons/protocol_engine/commands/move_to_coordinates.py +36 -22
  127. opentrons/protocol_engine/commands/move_to_well.py +40 -24
  128. opentrons/protocol_engine/commands/movement_common.py +338 -0
  129. opentrons/protocol_engine/commands/pick_up_tip.py +49 -27
  130. opentrons/protocol_engine/commands/pipetting_common.py +169 -87
  131. opentrons/protocol_engine/commands/prepare_to_aspirate.py +24 -33
  132. opentrons/protocol_engine/commands/reload_labware.py +1 -1
  133. opentrons/protocol_engine/commands/retract_axis.py +1 -1
  134. opentrons/protocol_engine/commands/robot/__init__.py +69 -0
  135. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +86 -0
  136. opentrons/protocol_engine/commands/robot/common.py +18 -0
  137. opentrons/protocol_engine/commands/robot/move_axes_relative.py +101 -0
  138. opentrons/protocol_engine/commands/robot/move_axes_to.py +100 -0
  139. opentrons/protocol_engine/commands/robot/move_to.py +94 -0
  140. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +77 -0
  141. opentrons/protocol_engine/commands/save_position.py +14 -5
  142. opentrons/protocol_engine/commands/set_rail_lights.py +1 -1
  143. opentrons/protocol_engine/commands/set_status_bar.py +1 -1
  144. opentrons/protocol_engine/commands/temperature_module/deactivate.py +1 -1
  145. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +1 -1
  146. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +10 -4
  147. opentrons/protocol_engine/commands/thermocycler/close_lid.py +1 -1
  148. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +1 -1
  149. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +1 -1
  150. opentrons/protocol_engine/commands/thermocycler/open_lid.py +1 -1
  151. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +8 -2
  152. opentrons/protocol_engine/commands/thermocycler/run_profile.py +9 -3
  153. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +11 -4
  154. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +1 -1
  155. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +1 -1
  156. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +1 -1
  157. opentrons/protocol_engine/commands/touch_tip.py +65 -16
  158. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +4 -1
  159. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -3
  160. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +1 -4
  161. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +1 -4
  162. opentrons/protocol_engine/commands/verify_tip_presence.py +11 -4
  163. opentrons/protocol_engine/commands/wait_for_duration.py +10 -3
  164. opentrons/protocol_engine/commands/wait_for_resume.py +10 -3
  165. opentrons/protocol_engine/errors/__init__.py +8 -0
  166. opentrons/protocol_engine/errors/error_occurrence.py +19 -20
  167. opentrons/protocol_engine/errors/exceptions.py +50 -0
  168. opentrons/protocol_engine/execution/command_executor.py +1 -1
  169. opentrons/protocol_engine/execution/equipment.py +73 -5
  170. opentrons/protocol_engine/execution/gantry_mover.py +364 -8
  171. opentrons/protocol_engine/execution/movement.py +27 -0
  172. opentrons/protocol_engine/execution/pipetting.py +5 -1
  173. opentrons/protocol_engine/execution/tip_handler.py +4 -6
  174. opentrons/protocol_engine/notes/notes.py +1 -1
  175. opentrons/protocol_engine/protocol_engine.py +7 -6
  176. opentrons/protocol_engine/resources/labware_data_provider.py +1 -1
  177. opentrons/protocol_engine/resources/labware_validation.py +5 -0
  178. opentrons/protocol_engine/resources/module_data_provider.py +1 -1
  179. opentrons/protocol_engine/resources/pipette_data_provider.py +12 -0
  180. opentrons/protocol_engine/slot_standardization.py +9 -9
  181. opentrons/protocol_engine/state/_move_types.py +9 -5
  182. opentrons/protocol_engine/state/_well_math.py +193 -0
  183. opentrons/protocol_engine/state/addressable_areas.py +25 -61
  184. opentrons/protocol_engine/state/command_history.py +12 -0
  185. opentrons/protocol_engine/state/commands.py +17 -13
  186. opentrons/protocol_engine/state/files.py +10 -12
  187. opentrons/protocol_engine/state/fluid_stack.py +138 -0
  188. opentrons/protocol_engine/state/frustum_helpers.py +57 -32
  189. opentrons/protocol_engine/state/geometry.py +47 -1
  190. opentrons/protocol_engine/state/labware.py +79 -25
  191. opentrons/protocol_engine/state/liquid_classes.py +82 -0
  192. opentrons/protocol_engine/state/liquids.py +16 -4
  193. opentrons/protocol_engine/state/modules.py +52 -70
  194. opentrons/protocol_engine/state/motion.py +6 -1
  195. opentrons/protocol_engine/state/pipettes.py +135 -58
  196. opentrons/protocol_engine/state/state.py +21 -2
  197. opentrons/protocol_engine/state/state_summary.py +4 -2
  198. opentrons/protocol_engine/state/tips.py +11 -44
  199. opentrons/protocol_engine/state/update_types.py +343 -48
  200. opentrons/protocol_engine/state/wells.py +19 -11
  201. opentrons/protocol_engine/types.py +176 -28
  202. opentrons/protocol_reader/extract_labware_definitions.py +5 -2
  203. opentrons/protocol_reader/file_format_validator.py +5 -5
  204. opentrons/protocol_runner/json_file_reader.py +9 -3
  205. opentrons/protocol_runner/json_translator.py +51 -25
  206. opentrons/protocol_runner/legacy_command_mapper.py +66 -64
  207. opentrons/protocol_runner/protocol_runner.py +35 -4
  208. opentrons/protocol_runner/python_protocol_wrappers.py +1 -1
  209. opentrons/protocol_runner/run_orchestrator.py +13 -3
  210. opentrons/protocols/advanced_control/common.py +38 -0
  211. opentrons/protocols/advanced_control/mix.py +1 -1
  212. opentrons/protocols/advanced_control/transfers/__init__.py +0 -0
  213. opentrons/protocols/advanced_control/transfers/common.py +56 -0
  214. opentrons/protocols/advanced_control/{transfers.py → transfers/transfer.py} +10 -85
  215. opentrons/protocols/api_support/definitions.py +1 -1
  216. opentrons/protocols/api_support/instrument.py +1 -1
  217. opentrons/protocols/api_support/util.py +10 -0
  218. opentrons/protocols/labware.py +70 -8
  219. opentrons/protocols/models/json_protocol.py +5 -9
  220. opentrons/simulate.py +3 -1
  221. opentrons/types.py +162 -2
  222. opentrons/util/entrypoint_util.py +2 -5
  223. opentrons/util/logging_config.py +1 -1
  224. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/METADATA +16 -15
  225. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/RECORD +229 -202
  226. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/WHEEL +1 -1
  227. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/LICENSE +0 -0
  228. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/entry_points.txt +0 -0
  229. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/top_level.txt +0 -0
@@ -228,9 +228,6 @@ class CommandState:
228
228
  This value can be used to generate future hashes.
229
229
  """
230
230
 
231
- failed_command_errors: List[ErrorOccurrence]
232
- """List of command errors that occurred during run execution."""
233
-
234
231
  has_entered_error_recovery: bool
235
232
  """Whether the run has entered error recovery."""
236
233
 
@@ -269,7 +266,6 @@ class CommandStore(HasState[CommandState], HandlesActions):
269
266
  run_started_at=None,
270
267
  latest_protocol_command_hash=None,
271
268
  stopped_by_estop=False,
272
- failed_command_errors=[],
273
269
  error_recovery_policy=error_recovery_policy,
274
270
  has_entered_error_recovery=False,
275
271
  )
@@ -308,7 +304,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
308
304
  # TODO(mc, 2021-06-22): mypy has trouble with this automatic
309
305
  # request > command mapping, figure out how to type precisely
310
306
  # (or wait for a future mypy version that can figure it out).
311
- queued_command = action.request._CommandCls.construct(
307
+ queued_command = action.request._CommandCls.model_construct( # type: ignore[call-arg]
312
308
  id=action.command_id,
313
309
  key=(
314
310
  action.request.key
@@ -330,7 +326,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
330
326
  def _handle_run_command_action(self, action: RunCommandAction) -> None:
331
327
  prev_entry = self._state.command_history.get(action.command_id)
332
328
 
333
- running_command = prev_entry.command.copy(
329
+ running_command = prev_entry.command.model_copy(
334
330
  update={
335
331
  "status": CommandStatus.RUNNING,
336
332
  "startedAt": action.started_at,
@@ -366,7 +362,6 @@ class CommandStore(HasState[CommandState], HandlesActions):
366
362
  notes=action.notes,
367
363
  )
368
364
  self._state.failed_command = self._state.command_history.get(action.command_id)
369
- self._state.failed_command_errors.append(public_error_occurrence)
370
365
 
371
366
  if (
372
367
  prev_entry.command.intent in (CommandIntent.PROTOCOL, None)
@@ -530,7 +525,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
530
525
  notes: Optional[List[CommandNote]],
531
526
  ) -> None:
532
527
  prev_entry = self._state.command_history.get(command_id)
533
- failed_command = prev_entry.command.copy(
528
+ failed_command = prev_entry.command.model_copy(
534
529
  update={
535
530
  "completedAt": failed_at,
536
531
  "status": CommandStatus.FAILED,
@@ -584,7 +579,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
584
579
  )
585
580
 
586
581
 
587
- class CommandView(HasState[CommandState]):
582
+ class CommandView:
588
583
  """Read-only command state view."""
589
584
 
590
585
  _state: CommandState
@@ -684,7 +679,7 @@ class CommandView(HasState[CommandState]):
684
679
  finish_error = self._state.finish_error
685
680
 
686
681
  if run_error and finish_error:
687
- combined_error = ErrorOccurrence.construct(
682
+ combined_error = ErrorOccurrence(
688
683
  id=finish_error.id,
689
684
  createdAt=finish_error.createdAt,
690
685
  errorType="RunAndFinishFailed",
@@ -706,7 +701,12 @@ class CommandView(HasState[CommandState]):
706
701
 
707
702
  def get_all_errors(self) -> List[ErrorOccurrence]:
708
703
  """Get the run's full error list, if there was none, returns an empty list."""
709
- return self._state.failed_command_errors
704
+ failed_commands = self._state.command_history.get_all_failed_commands()
705
+ return [
706
+ command_error.error
707
+ for command_error in failed_commands
708
+ if command_error.error is not None
709
+ ]
710
710
 
711
711
  def get_has_entered_recovery_mode(self) -> bool:
712
712
  """Get whether the run has entered recovery mode."""
@@ -916,7 +916,7 @@ class CommandView(HasState[CommandState]):
916
916
  fatal error of the overall run coming from anywhere in the Python script,
917
917
  including in between commands.
918
918
  """
919
- failed_command = self.state.failed_command
919
+ failed_command = self._state.failed_command
920
920
  if (
921
921
  failed_command
922
922
  and failed_command.command.error
@@ -932,12 +932,16 @@ class CommandView(HasState[CommandState]):
932
932
 
933
933
  The command ID is assumed to point to a failed command.
934
934
  """
935
- return self.state.command_error_recovery_types[command_id]
935
+ return self._state.command_error_recovery_types[command_id]
936
936
 
937
937
  def get_is_stopped(self) -> bool:
938
938
  """Get whether an engine stop has completed."""
939
939
  return self._state.run_completed_at is not None
940
940
 
941
+ def get_is_stopped_by_estop(self) -> bool:
942
+ """Return whether the engine was stopped specifically by an E-stop."""
943
+ return self._state.stopped_by_estop
944
+
941
945
  def has_been_played(self) -> bool:
942
946
  """Get whether engine has started."""
943
947
  return self._state.run_started_at is not None
@@ -2,12 +2,11 @@
2
2
  from dataclasses import dataclass
3
3
  from typing import List
4
4
 
5
+ from opentrons.protocol_engine.actions.get_state_update import get_state_updates
6
+ from opentrons.protocol_engine.state import update_types
7
+
5
8
  from ._abstract_store import HasState, HandlesActions
6
- from ..actions import Action, SucceedCommandAction
7
- from ..commands import (
8
- Command,
9
- absorbance_reader,
10
- )
9
+ from ..actions import Action
11
10
 
12
11
 
13
12
  @dataclass
@@ -28,16 +27,15 @@ class FileStore(HasState[FileState], HandlesActions):
28
27
 
29
28
  def handle_action(self, action: Action) -> None:
30
29
  """Modify state in reaction to an action."""
31
- if isinstance(action, SucceedCommandAction):
32
- self._handle_command(action.command)
30
+ for state_update in get_state_updates(action):
31
+ self._handle_state_update(state_update)
33
32
 
34
- def _handle_command(self, command: Command) -> None:
35
- if isinstance(command.result, absorbance_reader.ReadAbsorbanceResult):
36
- if command.result.fileIds is not None:
37
- self._state.file_ids.extend(command.result.fileIds)
33
+ def _handle_state_update(self, state_update: update_types.StateUpdate) -> None:
34
+ if state_update.files_added != update_types.NO_CHANGE:
35
+ self._state.file_ids.extend(state_update.files_added.file_ids)
38
36
 
39
37
 
40
- class FileView(HasState[FileState]):
38
+ class FileView:
41
39
  """Read-only engine created file state view."""
42
40
 
43
41
  _state: FileState
@@ -0,0 +1,138 @@
1
+ """Implements fluid stack tracking for pipettes.
2
+
3
+ Inside a pipette's tip, there can be a mix of kinds of fluids - here, "fluid" means "liquid" (i.e. a protocol-relevant
4
+ working liquid that is aspirated or dispensed from wells) or "air" (i.e. because there was an air gap). Since sometimes
5
+ you want air gaps in different places - physically-below liquid to prevent dripping, physically-above liquid to provide
6
+ extra room to push the plunger - we need to support some notion of at least phsyical ordinal position of air and liquid,
7
+ and we do so as a logical stack because that's physically relevant.
8
+ """
9
+ from logging import getLogger
10
+ from numpy import isclose
11
+ from ..types import AspiratedFluid, FluidKind
12
+
13
+ _LOG = getLogger(__name__)
14
+
15
+
16
+ class FluidStack:
17
+ """A FluidStack data structure is a list of AspiratedFluids, with stack-style (last-in-first-out) ordering.
18
+
19
+ The front of the list is the physical-top of the liquid stack (logical-bottom of the stack data structure)
20
+ and the back of the list is the physical-bottom of the liquid stack (logical-top of the stack data structure).
21
+ The state is internal and the interaction surface is the methods. This is a mutating API.
22
+ """
23
+
24
+ _FluidStack = list[AspiratedFluid]
25
+
26
+ _fluid_stack: _FluidStack
27
+
28
+ def __init__(self, _fluid_stack: _FluidStack | None = None) -> None:
29
+ """Build a FluidStack.
30
+
31
+ The argument is provided for testing and shouldn't be generally used.
32
+ """
33
+ self._fluid_stack = _fluid_stack or []
34
+
35
+ def add_fluid(self, new: AspiratedFluid) -> None:
36
+ """Add fluid to a stack.
37
+
38
+ If the new fluid is of a different kind than what's on the physical-bottom of the stack, add a new record.
39
+ If the new fluid is of the same kind as what's on the physical-bottom of the stack, add the new volume to
40
+ the same record.
41
+ """
42
+ if len(self._fluid_stack) == 0 or self._fluid_stack[-1].kind != new.kind:
43
+ # this is a new kind of fluid, append the record
44
+ self._fluid_stack.append(new)
45
+ else:
46
+ # this is more of the same kind of fluid, add the volumes
47
+ old_fluid = self._fluid_stack.pop(-1)
48
+ self._fluid_stack.append(
49
+ AspiratedFluid(kind=new.kind, volume=old_fluid.volume + new.volume)
50
+ )
51
+
52
+ def _alter_fluid_records(
53
+ self, remove: int, new_last: AspiratedFluid | None
54
+ ) -> None:
55
+ if remove >= len(self._fluid_stack) or len(self._fluid_stack) == 0:
56
+ self._fluid_stack = []
57
+ return
58
+ if remove != 0:
59
+ removed = self._fluid_stack[:-remove]
60
+ else:
61
+ removed = self._fluid_stack
62
+ if new_last:
63
+ removed[-1] = new_last
64
+ self._fluid_stack = removed
65
+
66
+ def remove_fluid(self, volume: float) -> None:
67
+ """Remove a specific amount of fluid from the physical-bottom of the stack.
68
+
69
+ This will consume records that are wholly included in the provided volume and alter the remaining
70
+ final records (if any) to decrement the amount of volume removed from it.
71
+
72
+ This function is designed to be used inside pipette store action handlers, which are generally not
73
+ exception-safe, and therefore swallows and logs errors.
74
+ """
75
+ self._fluid_stack_iterator = reversed(self._fluid_stack)
76
+ removed_elements: list[AspiratedFluid] = []
77
+ while volume > 0:
78
+ try:
79
+ last_stack_element = next(self._fluid_stack_iterator)
80
+ except StopIteration:
81
+ _LOG.error(
82
+ f"Attempting to remove more fluid than present, {volume}uL left over"
83
+ )
84
+ self._alter_fluid_records(len(removed_elements), None)
85
+ return
86
+ if last_stack_element.volume < volume:
87
+ removed_elements.append(last_stack_element)
88
+ volume -= last_stack_element.volume
89
+ elif isclose(last_stack_element.volume, volume):
90
+ self._alter_fluid_records(len(removed_elements) + 1, None)
91
+ return
92
+ else:
93
+ self._alter_fluid_records(
94
+ len(removed_elements),
95
+ AspiratedFluid(
96
+ kind=last_stack_element.kind,
97
+ volume=last_stack_element.volume - volume,
98
+ ),
99
+ )
100
+ return
101
+
102
+ _LOG.error(f"Failed to handle removing {volume}uL from {self._fluid_stack}")
103
+
104
+ def aspirated_volume(self, kind: FluidKind | None = None) -> float:
105
+ """Measure the total amount of fluid (optionally filtered by kind) in the stack."""
106
+ volume = 0.0
107
+ for el in self._fluid_stack:
108
+ if kind is not None and el.kind != kind:
109
+ continue
110
+ volume += el.volume
111
+ return volume
112
+
113
+ def liquid_part_of_dispense_volume(self, volume: float) -> float:
114
+ """Get the amount of liquid in the specified volume starting at the physical-bottom of the stack."""
115
+ liquid_volume = 0.0
116
+ for el in reversed(self._fluid_stack):
117
+ if el.kind == FluidKind.LIQUID:
118
+ liquid_volume += min(volume, el.volume)
119
+ volume -= min(el.volume, volume)
120
+ if isclose(volume, 0.0):
121
+ return liquid_volume
122
+ return liquid_volume
123
+
124
+ def __eq__(self, other: object) -> bool:
125
+ """Equality."""
126
+ if isinstance(other, type(self)):
127
+ return other._fluid_stack == self._fluid_stack
128
+ return False
129
+
130
+ def __repr__(self) -> str:
131
+ """String representation of a fluid stack."""
132
+ if self._fluid_stack:
133
+ stringified_stack = (
134
+ f'(top) {", ".join([str(item) for item in self._fluid_stack])} (bottom)'
135
+ )
136
+ else:
137
+ stringified_stack = "empty"
138
+ return f"<{self.__class__.__name__}: {stringified_stack}>"
@@ -220,28 +220,40 @@ def _get_segment_capacity(segment: WellSegment) -> float:
220
220
  section_height = segment.topHeight - segment.bottomHeight
221
221
  match segment:
222
222
  case SphericalSegment():
223
- return _volume_from_height_spherical(
224
- target_height=segment.topHeight,
225
- radius_of_curvature=segment.radiusOfCurvature,
223
+ return (
224
+ _volume_from_height_spherical(
225
+ target_height=segment.topHeight,
226
+ radius_of_curvature=segment.radiusOfCurvature,
227
+ )
228
+ * segment.count
226
229
  )
227
230
  case CuboidalFrustum():
228
- return _volume_from_height_rectangular(
229
- target_height=section_height,
230
- bottom_length=segment.bottomYDimension,
231
- bottom_width=segment.bottomXDimension,
232
- top_length=segment.topYDimension,
233
- top_width=segment.topXDimension,
234
- total_frustum_height=section_height,
231
+ return (
232
+ _volume_from_height_rectangular(
233
+ target_height=section_height,
234
+ bottom_length=segment.bottomYDimension,
235
+ bottom_width=segment.bottomXDimension,
236
+ top_length=segment.topYDimension,
237
+ top_width=segment.topXDimension,
238
+ total_frustum_height=section_height,
239
+ )
240
+ * segment.count
235
241
  )
236
242
  case ConicalFrustum():
237
- return _volume_from_height_circular(
238
- target_height=section_height,
239
- total_frustum_height=section_height,
240
- bottom_radius=(segment.bottomDiameter / 2),
241
- top_radius=(segment.topDiameter / 2),
243
+ return (
244
+ _volume_from_height_circular(
245
+ target_height=section_height,
246
+ total_frustum_height=section_height,
247
+ bottom_radius=(segment.bottomDiameter / 2),
248
+ top_radius=(segment.topDiameter / 2),
249
+ )
250
+ * segment.count
242
251
  )
243
252
  case SquaredConeSegment():
244
- return _volume_from_height_squared_cone(section_height, segment)
253
+ return (
254
+ _volume_from_height_squared_cone(section_height, segment)
255
+ * segment.count
256
+ )
245
257
  case _:
246
258
  # TODO: implement volume calculations for truncated circular and rounded rectangular segments
247
259
  raise NotImplementedError(
@@ -272,6 +284,7 @@ def height_at_volume_within_section(
272
284
  section_height: float,
273
285
  ) -> float:
274
286
  """Calculate a height within a bounded section according to geometry."""
287
+ target_volume_relative = target_volume_relative / section.count
275
288
  match section:
276
289
  case SphericalSegment():
277
290
  return _height_from_volume_spherical(
@@ -311,28 +324,40 @@ def volume_at_height_within_section(
311
324
  """Calculate a volume within a bounded section according to geometry."""
312
325
  match section:
313
326
  case SphericalSegment():
314
- return _volume_from_height_spherical(
315
- target_height=target_height_relative,
316
- radius_of_curvature=section.radiusOfCurvature,
327
+ return (
328
+ _volume_from_height_spherical(
329
+ target_height=target_height_relative,
330
+ radius_of_curvature=section.radiusOfCurvature,
331
+ )
332
+ * section.count
317
333
  )
318
334
  case ConicalFrustum():
319
- return _volume_from_height_circular(
320
- target_height=target_height_relative,
321
- total_frustum_height=section_height,
322
- bottom_radius=(section.bottomDiameter / 2),
323
- top_radius=(section.topDiameter / 2),
335
+ return (
336
+ _volume_from_height_circular(
337
+ target_height=target_height_relative,
338
+ total_frustum_height=section_height,
339
+ bottom_radius=(section.bottomDiameter / 2),
340
+ top_radius=(section.topDiameter / 2),
341
+ )
342
+ * section.count
324
343
  )
325
344
  case CuboidalFrustum():
326
- return _volume_from_height_rectangular(
327
- target_height=target_height_relative,
328
- total_frustum_height=section_height,
329
- bottom_width=section.bottomXDimension,
330
- bottom_length=section.bottomYDimension,
331
- top_width=section.topXDimension,
332
- top_length=section.topYDimension,
345
+ return (
346
+ _volume_from_height_rectangular(
347
+ target_height=target_height_relative,
348
+ total_frustum_height=section_height,
349
+ bottom_width=section.bottomXDimension,
350
+ bottom_length=section.bottomYDimension,
351
+ top_width=section.topXDimension,
352
+ top_length=section.topYDimension,
353
+ )
354
+ * section.count
333
355
  )
334
356
  case SquaredConeSegment():
335
- return _volume_from_height_squared_cone(target_height_relative, section)
357
+ return (
358
+ _volume_from_height_squared_cone(target_height_relative, section)
359
+ * section.count
360
+ )
336
361
  case _:
337
362
  # TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712
338
363
  # we need to input the math attached to that issue
@@ -1,4 +1,5 @@
1
1
  """Geometry state getters."""
2
+
2
3
  import enum
3
4
  from numpy import array, dot, double as npdouble
4
5
  from numpy.typing import NDArray
@@ -8,6 +9,7 @@ from functools import cached_property
8
9
 
9
10
  from opentrons.types import Point, DeckSlotName, StagingSlotName, MountType
10
11
 
12
+ from opentrons_shared_data.errors.exceptions import InvalidStoredData
11
13
  from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN
12
14
  from opentrons_shared_data.deck.types import CutoutFixture
13
15
  from opentrons_shared_data.pipette import PIPETTE_X_SPAN
@@ -61,6 +63,7 @@ from .frustum_helpers import (
61
63
  find_volume_at_well_height,
62
64
  find_height_at_well_volume,
63
65
  )
66
+ from ._well_math import wells_covered_by_pipette_configuration, nozzles_per_well
64
67
 
65
68
 
66
69
  SLOT_WIDTH = 128
@@ -486,7 +489,7 @@ class GeometryView:
486
489
  well_depth=well_depth,
487
490
  operation_volume=operation_volume,
488
491
  )
489
- offset = offset.copy(update={"z": offset.z + offset_adjustment})
492
+ offset = offset.model_copy(update={"z": offset.z + offset_adjustment})
490
493
  self.validate_well_position(
491
494
  well_location=well_location, z_offset=offset.z, pipette_id=pipette_id
492
495
  )
@@ -1559,3 +1562,46 @@ class GeometryView:
1559
1562
  raise errors.InvalidDispenseVolumeError(
1560
1563
  f"Attempting to dispense {volume}µL of liquid into a well that can only hold {well_volumetric_capacity}µL (well {well_name} in labware_id: {labware_id})"
1561
1564
  )
1565
+
1566
+ def get_wells_covered_by_pipette_with_active_well(
1567
+ self, labware_id: str, target_well_name: str, pipette_id: str
1568
+ ) -> list[str]:
1569
+ """Get a flat list of wells that are covered by a pipette when moved to a specified well.
1570
+
1571
+ When you move a pipette in a multichannel configuration to a specific well - the target well -
1572
+ the pipette will operate on other wells as well.
1573
+
1574
+ For instance, a pipette with a COLUMN configuration with well A1 of an SBS standard labware target
1575
+ will also "cover", under this definition, wells B1-H1. That same pipette, when C5 is the target well, will "cover"
1576
+ wells C5-H5.
1577
+
1578
+ This math only works, and may only be applied, if one of the following is true:
1579
+ - The pipette is in a SINGLE configuration
1580
+ - The pipette is in a non-SINGLE configuration, and the labware is an SBS-format 96 or 384 well plate (and is so
1581
+ marked in its definition's parameters.format key, as 96Standard or 384Standard)
1582
+
1583
+ If all of the following do not apply, regardless of the nozzle configuration of the pipette this function will
1584
+ return only the labware covered by the primary well.
1585
+ """
1586
+ pipette_nozzle_map = self._pipettes.get_nozzle_configuration(pipette_id)
1587
+ labware_columns = [
1588
+ column for column in self._labware.get_definition(labware_id).ordering
1589
+ ]
1590
+ try:
1591
+ return list(
1592
+ wells_covered_by_pipette_configuration(
1593
+ pipette_nozzle_map, target_well_name, labware_columns
1594
+ )
1595
+ )
1596
+ except InvalidStoredData:
1597
+ return [target_well_name]
1598
+
1599
+ def get_nozzles_per_well(
1600
+ self, labware_id: str, target_well_name: str, pipette_id: str
1601
+ ) -> int:
1602
+ """Get the number of nozzles that will interact with each well."""
1603
+ return nozzles_per_well(
1604
+ self._pipettes.get_nozzle_configuration(pipette_id),
1605
+ target_well_name,
1606
+ self._labware.get_definition(labware_id).ordering,
1607
+ )
@@ -131,7 +131,7 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
131
131
  for fixed_labware in deck_fixed_labware
132
132
  }
133
133
  labware_by_id = {
134
- fixed_labware.labware_id: LoadedLabware.construct(
134
+ fixed_labware.labware_id: LoadedLabware.model_construct(
135
135
  id=fixed_labware.labware_id,
136
136
  location=fixed_labware.location,
137
137
  loadName=fixed_labware.definition.parameters.loadName,
@@ -156,10 +156,12 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
156
156
  """Modify state in reaction to an action."""
157
157
  for state_update in get_state_updates(action):
158
158
  self._add_loaded_labware(state_update)
159
+ self._add_loaded_lid_stack(state_update)
159
160
  self._set_labware_location(state_update)
161
+ self._set_labware_lid(state_update)
160
162
 
161
163
  if isinstance(action, AddLabwareOffsetAction):
162
- labware_offset = LabwareOffset.construct(
164
+ labware_offset = LabwareOffset.model_construct(
163
165
  id=action.labware_offset_id,
164
166
  createdAt=action.created_at,
165
167
  definitionUri=action.request.definitionUri,
@@ -212,7 +214,7 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
212
214
 
213
215
  self._state.labware_by_id[
214
216
  loaded_labware_update.labware_id
215
- ] = LoadedLabware.construct(
217
+ ] = LoadedLabware.model_construct(
216
218
  id=loaded_labware_update.labware_id,
217
219
  location=location,
218
220
  loadName=loaded_labware_update.definition.parameters.loadName,
@@ -221,6 +223,63 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
221
223
  displayName=display_name,
222
224
  )
223
225
 
226
+ def _add_loaded_lid_stack(self, state_update: update_types.StateUpdate) -> None:
227
+ loaded_lid_stack_update = state_update.loaded_lid_stack
228
+ if loaded_lid_stack_update != update_types.NO_CHANGE:
229
+ # Add the stack object
230
+ stack_definition_uri = uri_from_details(
231
+ namespace=loaded_lid_stack_update.stack_object_definition.namespace,
232
+ load_name=loaded_lid_stack_update.stack_object_definition.parameters.loadName,
233
+ version=loaded_lid_stack_update.stack_object_definition.version,
234
+ )
235
+ self.state.definitions_by_uri[
236
+ stack_definition_uri
237
+ ] = loaded_lid_stack_update.stack_object_definition
238
+ self._state.labware_by_id[
239
+ loaded_lid_stack_update.stack_id
240
+ ] = LoadedLabware.construct(
241
+ id=loaded_lid_stack_update.stack_id,
242
+ location=loaded_lid_stack_update.stack_location,
243
+ loadName=loaded_lid_stack_update.stack_object_definition.parameters.loadName,
244
+ definitionUri=stack_definition_uri,
245
+ offsetId=None,
246
+ displayName=None,
247
+ )
248
+
249
+ # Add the Lids on top of the stack object
250
+ for i in range(len(loaded_lid_stack_update.labware_ids)):
251
+ definition_uri = uri_from_details(
252
+ namespace=loaded_lid_stack_update.definition.namespace,
253
+ load_name=loaded_lid_stack_update.definition.parameters.loadName,
254
+ version=loaded_lid_stack_update.definition.version,
255
+ )
256
+
257
+ self._state.definitions_by_uri[
258
+ definition_uri
259
+ ] = loaded_lid_stack_update.definition
260
+
261
+ location = loaded_lid_stack_update.new_locations_by_id[
262
+ loaded_lid_stack_update.labware_ids[i]
263
+ ]
264
+
265
+ self._state.labware_by_id[
266
+ loaded_lid_stack_update.labware_ids[i]
267
+ ] = LoadedLabware.construct(
268
+ id=loaded_lid_stack_update.labware_ids[i],
269
+ location=location,
270
+ loadName=loaded_lid_stack_update.definition.parameters.loadName,
271
+ definitionUri=definition_uri,
272
+ offsetId=None,
273
+ displayName=None,
274
+ )
275
+
276
+ def _set_labware_lid(self, state_update: update_types.StateUpdate) -> None:
277
+ labware_lid_update = state_update.labware_lid
278
+ if labware_lid_update != update_types.NO_CHANGE:
279
+ parent_labware_id = labware_lid_update.parent_labware_id
280
+ lid_id = labware_lid_update.lid_id
281
+ self._state.labware_by_id[parent_labware_id].lid_id = lid_id
282
+
224
283
  def _set_labware_location(self, state_update: update_types.StateUpdate) -> None:
225
284
  labware_location_update = state_update.labware_location
226
285
  if labware_location_update != update_types.NO_CHANGE:
@@ -244,7 +303,7 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
244
303
  self._state.labware_by_id[labware_id].location = new_location
245
304
 
246
305
 
247
- class LabwareView(HasState[LabwareState]):
306
+ class LabwareView:
248
307
  """Read-only labware state view."""
249
308
 
250
309
  _state: LabwareState
@@ -268,7 +327,7 @@ class LabwareView(HasState[LabwareState]):
268
327
 
269
328
  def get_id_by_module(self, module_id: str) -> str:
270
329
  """Return the ID of the labware loaded on the given module."""
271
- for labware_id, labware in self.state.labware_by_id.items():
330
+ for labware_id, labware in self._state.labware_by_id.items():
272
331
  if (
273
332
  isinstance(labware.location, ModuleLocation)
274
333
  and labware.location.moduleId == module_id
@@ -281,7 +340,7 @@ class LabwareView(HasState[LabwareState]):
281
340
 
282
341
  def get_id_by_labware(self, labware_id: str) -> str:
283
342
  """Return the ID of the labware loaded on the given labware."""
284
- for labware in self.state.labware_by_id.values():
343
+ for labware in self._state.labware_by_id.values():
285
344
  if (
286
345
  isinstance(labware.location, OnLabwareLocation)
287
346
  and labware.location.labwareId == labware_id
@@ -441,21 +500,7 @@ class LabwareView(HasState[LabwareState]):
441
500
 
442
501
  If not defined within a labware, defaults to one.
443
502
  """
444
- stacking_quirks = {
445
- "stackingMaxFive": 5,
446
- "stackingMaxFour": 4,
447
- "stackingMaxThree": 3,
448
- "stackingMaxTwo": 2,
449
- "stackingMaxOne": 1,
450
- "stackingMaxZero": 0,
451
- }
452
- for quirk in stacking_quirks.keys():
453
- if (
454
- labware.parameters.quirks is not None
455
- and quirk in labware.parameters.quirks
456
- ):
457
- return stacking_quirks[quirk]
458
- return 1
503
+ return labware.stackLimit if labware.stackLimit is not None else 1
459
504
 
460
505
  def get_should_center_pipette_on_target_well(self, labware_id: str) -> bool:
461
506
  """True if a pipette moving to a well of this labware should center its body on the target.
@@ -815,6 +860,11 @@ class LabwareView(HasState[LabwareState]):
815
860
  return self.raise_if_labware_inaccessible_by_pipette(
816
861
  labware_location.labwareId
817
862
  )
863
+ elif labware.lid_id is not None:
864
+ raise errors.LocationNotAccessibleByPipetteError(
865
+ f"Cannot move pipette to {labware.loadName} "
866
+ "because labware is currently covered by a lid."
867
+ )
818
868
  elif isinstance(labware_location, AddressableAreaLocation):
819
869
  if fixture_validation.is_staging_slot(labware_location.addressableAreaName):
820
870
  raise errors.LocationNotAccessibleByPipetteError(
@@ -998,11 +1048,15 @@ class LabwareView(HasState[LabwareState]):
998
1048
  return None
999
1049
  else:
1000
1050
  return LabwareMovementOffsetData(
1001
- pickUpOffset=cast(
1002
- LabwareOffsetVector, parsed_offsets[offset_key].pickUpOffset
1051
+ pickUpOffset=LabwareOffsetVector.model_construct(
1052
+ x=parsed_offsets[offset_key].pickUpOffset.x,
1053
+ y=parsed_offsets[offset_key].pickUpOffset.y,
1054
+ z=parsed_offsets[offset_key].pickUpOffset.z,
1003
1055
  ),
1004
- dropOffset=cast(
1005
- LabwareOffsetVector, parsed_offsets[offset_key].dropOffset
1056
+ dropOffset=LabwareOffsetVector.model_construct(
1057
+ x=parsed_offsets[offset_key].dropOffset.x,
1058
+ y=parsed_offsets[offset_key].dropOffset.y,
1059
+ z=parsed_offsets[offset_key].dropOffset.z,
1006
1060
  ),
1007
1061
  )
1008
1062