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
@@ -0,0 +1,82 @@
1
+ """A data store of liquid classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ from typing import Dict
7
+ from typing_extensions import Optional
8
+
9
+ from .. import errors
10
+ from ..actions import Action, get_state_updates
11
+ from ..types import LiquidClassRecord
12
+ from . import update_types
13
+ from ._abstract_store import HasState, HandlesActions
14
+
15
+
16
+ @dataclasses.dataclass
17
+ class LiquidClassState:
18
+ """Our state is a bidirectional mapping between IDs <-> LiquidClassRecords."""
19
+
20
+ # We use the bidirectional map to see if we've already assigned an ID to a liquid class when the
21
+ # engine is asked to store a new liquid class.
22
+ liquid_class_record_by_id: Dict[str, LiquidClassRecord]
23
+ liquid_class_record_to_id: Dict[LiquidClassRecord, str]
24
+
25
+
26
+ class LiquidClassStore(HasState[LiquidClassState], HandlesActions):
27
+ """Container for LiquidClassState."""
28
+
29
+ _state: LiquidClassState
30
+
31
+ def __init__(self) -> None:
32
+ self._state = LiquidClassState(
33
+ liquid_class_record_by_id={},
34
+ liquid_class_record_to_id={},
35
+ )
36
+
37
+ def handle_action(self, action: Action) -> None:
38
+ """Update the state in response to the action."""
39
+ for state_update in get_state_updates(action):
40
+ if state_update.liquid_class_loaded != update_types.NO_CHANGE:
41
+ self._handle_liquid_class_loaded_update(
42
+ state_update.liquid_class_loaded
43
+ )
44
+
45
+ def _handle_liquid_class_loaded_update(
46
+ self, state_update: update_types.LiquidClassLoadedUpdate
47
+ ) -> None:
48
+ # We're just a data store. All the validation and ID generation happens in the command implementation.
49
+ self._state.liquid_class_record_by_id[
50
+ state_update.liquid_class_id
51
+ ] = state_update.liquid_class_record
52
+ self._state.liquid_class_record_to_id[
53
+ state_update.liquid_class_record
54
+ ] = state_update.liquid_class_id
55
+
56
+
57
+ class LiquidClassView:
58
+ """Read-only view of the LiquidClassState."""
59
+
60
+ _state: LiquidClassState
61
+
62
+ def __init__(self, state: LiquidClassState) -> None:
63
+ self._state = state
64
+
65
+ def get(self, liquid_class_id: str) -> LiquidClassRecord:
66
+ """Get the LiquidClassRecord with the given identifier."""
67
+ try:
68
+ return self._state.liquid_class_record_by_id[liquid_class_id]
69
+ except KeyError as e:
70
+ raise errors.LiquidClassDoesNotExistError(
71
+ f"Liquid class ID {liquid_class_id} not found."
72
+ ) from e
73
+
74
+ def get_id_for_liquid_class_record(
75
+ self, liquid_class_record: LiquidClassRecord
76
+ ) -> Optional[str]:
77
+ """See if the given LiquidClassRecord if already in the store, and if so, return its identifier."""
78
+ return self._state.liquid_class_record_to_id.get(liquid_class_record)
79
+
80
+ def get_all(self) -> Dict[str, LiquidClassRecord]:
81
+ """Get all the LiquidClassRecords in the store."""
82
+ return self._state.liquid_class_record_by_id.copy()
@@ -1,11 +1,11 @@
1
1
  """Basic liquid data state and store."""
2
2
  from dataclasses import dataclass
3
3
  from typing import Dict, List
4
- from opentrons.protocol_engine.types import Liquid
4
+ from opentrons.protocol_engine.types import Liquid, LiquidId
5
5
 
6
6
  from ._abstract_store import HasState, HandlesActions
7
7
  from ..actions import Action, AddLiquidAction
8
- from ..errors import LiquidDoesNotExistError
8
+ from ..errors import LiquidDoesNotExistError, InvalidLiquidError
9
9
 
10
10
 
11
11
  @dataclass
@@ -34,7 +34,7 @@ class LiquidStore(HasState[LiquidState], HandlesActions):
34
34
  self._state.liquids_by_id[action.liquid.id] = action.liquid
35
35
 
36
36
 
37
- class LiquidView(HasState[LiquidState]):
37
+ class LiquidView:
38
38
  """Read-only liquid state view."""
39
39
 
40
40
  _state: LiquidState
@@ -51,11 +51,23 @@ class LiquidView(HasState[LiquidState]):
51
51
  """Get all protocol liquids."""
52
52
  return list(self._state.liquids_by_id.values())
53
53
 
54
- def validate_liquid_id(self, liquid_id: str) -> str:
54
+ def validate_liquid_id(self, liquid_id: LiquidId) -> LiquidId:
55
55
  """Check if liquid_id exists in liquids."""
56
+ is_empty = liquid_id == "EMPTY"
57
+ if is_empty:
58
+ return liquid_id
56
59
  has_liquid = liquid_id in self._state.liquids_by_id
57
60
  if not has_liquid:
58
61
  raise LiquidDoesNotExistError(
59
62
  f"Supplied liquidId: {liquid_id} does not exist in the loaded liquids."
60
63
  )
61
64
  return liquid_id
65
+
66
+ def validate_liquid_allowed(self, liquid: Liquid) -> Liquid:
67
+ """Validate that a liquid is legal to load."""
68
+ is_empty = liquid.id == "EMPTY"
69
+ if is_empty:
70
+ raise InvalidLiquidError(
71
+ message='Protocols may not define a liquid with the special id "EMPTY".'
72
+ )
73
+ return liquid
@@ -35,6 +35,7 @@ from opentrons.protocol_engine.state.module_substates.absorbance_reader_substate
35
35
  AbsorbanceReaderMeasureMode,
36
36
  )
37
37
  from opentrons.types import DeckSlotName, MountType, StagingSlotName
38
+ from .update_types import AbsorbanceReaderStateUpdate
38
39
  from ..errors import ModuleNotConnectedError
39
40
 
40
41
  from ..types import (
@@ -63,7 +64,6 @@ from ..commands import (
63
64
  heater_shaker,
64
65
  temperature_module,
65
66
  thermocycler,
66
- absorbance_reader,
67
67
  )
68
68
  from ..actions import (
69
69
  Action,
@@ -296,40 +296,10 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
296
296
  ):
297
297
  self._handle_thermocycler_module_commands(command)
298
298
 
299
- if isinstance(
300
- command.result,
301
- (
302
- absorbance_reader.InitializeResult,
303
- absorbance_reader.ReadAbsorbanceResult,
304
- ),
305
- ):
306
- self._handle_absorbance_reader_commands(command)
307
-
308
299
  def _handle_state_update(self, state_update: update_types.StateUpdate) -> None:
309
- if state_update.absorbance_reader_lid != update_types.NO_CHANGE:
310
- module_id = state_update.absorbance_reader_lid.module_id
311
- is_lid_on = state_update.absorbance_reader_lid.is_lid_on
312
-
313
- # Get current values:
314
- absorbance_reader_substate = self._state.substate_by_module_id[module_id]
315
- assert isinstance(
316
- absorbance_reader_substate, AbsorbanceReaderSubState
317
- ), f"{module_id} is not an absorbance plate reader."
318
- configured = absorbance_reader_substate.configured
319
- measure_mode = absorbance_reader_substate.measure_mode
320
- configured_wavelengths = absorbance_reader_substate.configured_wavelengths
321
- reference_wavelength = absorbance_reader_substate.reference_wavelength
322
- data = absorbance_reader_substate.data
323
-
324
- self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState(
325
- module_id=AbsorbanceReaderId(module_id),
326
- configured=configured,
327
- measured=True,
328
- is_lid_on=is_lid_on,
329
- measure_mode=measure_mode,
330
- configured_wavelengths=configured_wavelengths,
331
- reference_wavelength=reference_wavelength,
332
- data=data,
300
+ if state_update.absorbance_reader_state_update != update_types.NO_CHANGE:
301
+ self._handle_absorbance_reader_commands(
302
+ state_update.absorbance_reader_state_update
333
303
  )
334
304
 
335
305
  def _add_module_substate(
@@ -589,50 +559,61 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
589
559
  )
590
560
 
591
561
  def _handle_absorbance_reader_commands(
592
- self,
593
- command: Union[
594
- absorbance_reader.Initialize,
595
- absorbance_reader.ReadAbsorbance,
596
- ],
562
+ self, absorbance_reader_state_update: AbsorbanceReaderStateUpdate
597
563
  ) -> None:
598
- module_id = command.params.moduleId
564
+ # Get current values:
565
+ module_id = absorbance_reader_state_update.module_id
599
566
  absorbance_reader_substate = self._state.substate_by_module_id[module_id]
600
567
  assert isinstance(
601
568
  absorbance_reader_substate, AbsorbanceReaderSubState
602
569
  ), f"{module_id} is not an absorbance plate reader."
603
-
604
- # Get current values
570
+ is_lid_on = absorbance_reader_substate.is_lid_on
571
+ measured = True
605
572
  configured = absorbance_reader_substate.configured
606
573
  measure_mode = absorbance_reader_substate.measure_mode
607
574
  configured_wavelengths = absorbance_reader_substate.configured_wavelengths
608
575
  reference_wavelength = absorbance_reader_substate.reference_wavelength
609
- is_lid_on = absorbance_reader_substate.is_lid_on
610
-
611
- if isinstance(command.result, absorbance_reader.InitializeResult):
612
- self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState(
613
- module_id=AbsorbanceReaderId(module_id),
614
- configured=True,
615
- measured=False,
616
- is_lid_on=is_lid_on,
617
- measure_mode=AbsorbanceReaderMeasureMode(command.params.measureMode),
618
- configured_wavelengths=command.params.sampleWavelengths,
619
- reference_wavelength=command.params.referenceWavelength,
620
- data=None,
576
+ data = absorbance_reader_substate.data
577
+ if (
578
+ absorbance_reader_state_update.absorbance_reader_lid
579
+ != update_types.NO_CHANGE
580
+ ):
581
+ is_lid_on = absorbance_reader_state_update.absorbance_reader_lid.is_lid_on
582
+ elif (
583
+ absorbance_reader_state_update.initialize_absorbance_reader_update
584
+ != update_types.NO_CHANGE
585
+ ):
586
+ configured = True
587
+ measured = False
588
+ is_lid_on = is_lid_on
589
+ measure_mode = AbsorbanceReaderMeasureMode(
590
+ absorbance_reader_state_update.initialize_absorbance_reader_update.measure_mode
621
591
  )
622
- elif isinstance(command.result, absorbance_reader.ReadAbsorbanceResult):
623
- self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState(
624
- module_id=AbsorbanceReaderId(module_id),
625
- configured=configured,
626
- measured=True,
627
- is_lid_on=is_lid_on,
628
- measure_mode=measure_mode,
629
- configured_wavelengths=configured_wavelengths,
630
- reference_wavelength=reference_wavelength,
631
- data=command.result.data,
592
+ configured_wavelengths = (
593
+ absorbance_reader_state_update.initialize_absorbance_reader_update.sample_wave_lengths
594
+ )
595
+ reference_wavelength = (
596
+ absorbance_reader_state_update.initialize_absorbance_reader_update.reference_wave_length
632
597
  )
598
+ data = None
599
+ elif (
600
+ absorbance_reader_state_update.absorbance_reader_data
601
+ != update_types.NO_CHANGE
602
+ ):
603
+ data = absorbance_reader_state_update.absorbance_reader_data.read_result
604
+ self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState(
605
+ module_id=AbsorbanceReaderId(module_id),
606
+ configured=configured,
607
+ measured=measured,
608
+ is_lid_on=is_lid_on,
609
+ measure_mode=measure_mode,
610
+ configured_wavelengths=configured_wavelengths,
611
+ reference_wavelength=reference_wavelength,
612
+ data=data,
613
+ )
633
614
 
634
615
 
635
- class ModuleView(HasState[ModuleState]):
616
+ class ModuleView:
636
617
  """Read-only view of computed module state."""
637
618
 
638
619
  _state: ModuleState
@@ -654,7 +635,7 @@ class ModuleView(HasState[ModuleState]):
654
635
  DeckSlotLocation(slotName=slot_name) if slot_name is not None else None
655
636
  )
656
637
 
657
- return LoadedModule.construct(
638
+ return LoadedModule.model_construct(
658
639
  id=module_id,
659
640
  location=location,
660
641
  model=attached_module.definition.model,
@@ -860,8 +841,8 @@ class ModuleView(HasState[ModuleState]):
860
841
  Labware Position Check offset.
861
842
  """
862
843
  if (
863
- self.state.deck_type == DeckType.OT2_STANDARD
864
- or self.state.deck_type == DeckType.OT2_SHORT_TRASH
844
+ self._state.deck_type == DeckType.OT2_STANDARD
845
+ or self._state.deck_type == DeckType.OT2_SHORT_TRASH
865
846
  ):
866
847
  definition = self.get_definition(module_id)
867
848
  slot = self.get_location(module_id).slotName.id
@@ -908,7 +889,7 @@ class ModuleView(HasState[ModuleState]):
908
889
  "Module location invalid for nominal module offset calculation."
909
890
  )
910
891
  module_addressable_area = self.ensure_and_convert_module_fixture_location(
911
- location, self.state.deck_type, module.model
892
+ location, module.model
912
893
  )
913
894
  module_addressable_area_position = (
914
895
  addressable_areas.get_addressable_area_offsets_from_cutout(
@@ -1281,13 +1262,14 @@ class ModuleView(HasState[ModuleState]):
1281
1262
  def ensure_and_convert_module_fixture_location(
1282
1263
  self,
1283
1264
  deck_slot: DeckSlotName,
1284
- deck_type: DeckType,
1285
1265
  model: ModuleModel,
1286
1266
  ) -> str:
1287
1267
  """Ensure module fixture load location is valid.
1288
1268
 
1289
1269
  Also, convert the deck slot to a valid module fixture addressable area.
1290
1270
  """
1271
+ deck_type = self._state.deck_type
1272
+
1291
1273
  if deck_type == DeckType.OT2_STANDARD or deck_type == DeckType.OT2_SHORT_TRASH:
1292
1274
  raise ValueError(
1293
1275
  f"Invalid Deck Type: {deck_type.name} - Does not support modules as fixtures."
@@ -327,6 +327,7 @@ class MotionView:
327
327
  labware_id: str,
328
328
  well_name: str,
329
329
  center_point: Point,
330
+ mm_from_edge: float = 0,
330
331
  radius: float = 1.0,
331
332
  ) -> List[motion_planning.Waypoint]:
332
333
  """Get a list of touch points for a touch tip operation."""
@@ -346,7 +347,11 @@ class MotionView:
346
347
  )
347
348
 
348
349
  positions = _move_types.get_edge_point_list(
349
- center_point, x_offset, y_offset, edge_path_type
350
+ center=center_point,
351
+ x_radius=x_offset,
352
+ y_radius=y_offset,
353
+ mm_from_edge=mm_from_edge,
354
+ edge_path_type=edge_path_type,
350
355
  )
351
356
  critical_point: Optional[CriticalPoint] = None
352
357
 
@@ -1,28 +1,33 @@
1
1
  """Basic pipette data state and store."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import dataclasses
6
+ from logging import getLogger
5
7
  from typing import (
6
8
  Dict,
7
9
  List,
8
10
  Mapping,
9
11
  Optional,
10
12
  Tuple,
11
- Union,
13
+ cast,
12
14
  )
13
15
 
16
+ from typing_extensions import assert_never
17
+
14
18
  from opentrons_shared_data.pipette import pipette_definition
19
+ from opentrons_shared_data.pipette.ul_per_mm import calculate_ul_per_mm
20
+ from opentrons_shared_data.pipette.types import UlPerMmAction
21
+
15
22
  from opentrons.config.defaults_ot2 import Z_RETRACT_DISTANCE
16
23
  from opentrons.hardware_control.dev_types import PipetteDict
17
24
  from opentrons.hardware_control import CriticalPoint
18
25
  from opentrons.hardware_control.nozzle_manager import (
19
- NozzleConfigurationType,
20
26
  NozzleMap,
21
27
  )
22
- from opentrons.types import MountType, Mount as HwMount, Point
28
+ from opentrons.types import MountType, Mount as HwMount, Point, NozzleConfigurationType
23
29
 
24
- from . import update_types
25
- from .. import commands
30
+ from . import update_types, fluid_stack
26
31
  from .. import errors
27
32
  from ..types import (
28
33
  LoadedPipette,
@@ -36,13 +41,13 @@ from ..types import (
36
41
  )
37
42
  from ..actions import (
38
43
  Action,
39
- FailCommandAction,
40
44
  SetPipetteMovementSpeedAction,
41
- SucceedCommandAction,
42
45
  get_state_updates,
43
46
  )
44
47
  from ._abstract_store import HasState, HandlesActions
45
48
 
49
+ LOG = getLogger(__name__)
50
+
46
51
 
47
52
  @dataclasses.dataclass(frozen=True)
48
53
  class HardwarePipette:
@@ -98,6 +103,8 @@ class StaticPipetteConfig:
98
103
  bounding_nozzle_offsets: BoundingNozzlesOffsets
99
104
  default_nozzle_map: NozzleMap # todo(mm, 2024-10-14): unused, remove?
100
105
  lld_settings: Optional[Dict[str, Dict[str, float]]]
106
+ plunger_positions: Dict[str, float]
107
+ shaft_ul_per_mm: float
101
108
  available_sensors: pipette_definition.AvailableSensorDefinition
102
109
 
103
110
 
@@ -109,7 +116,7 @@ class PipetteState:
109
116
  # attributes are populated at the appropriate times. Refactor to a
110
117
  # single dict-of-many-things instead of many dicts-of-single-things.
111
118
  pipettes_by_id: Dict[str, LoadedPipette]
112
- aspirated_volume_by_id: Dict[str, Optional[float]]
119
+ pipette_contents_by_id: Dict[str, Optional[fluid_stack.FluidStack]]
113
120
  current_location: Optional[CurrentPipetteLocation]
114
121
  current_deck_point: CurrentDeckPoint
115
122
  attached_tip_by_id: Dict[str, Optional[TipGeometry]]
@@ -129,7 +136,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
129
136
  """Initialize a PipetteStore and its state."""
130
137
  self._state = PipetteState(
131
138
  pipettes_by_id={},
132
- aspirated_volume_by_id={},
139
+ pipette_contents_by_id={},
133
140
  attached_tip_by_id={},
134
141
  current_location=None,
135
142
  current_deck_point=CurrentDeckPoint(mount=None, deck_point=None),
@@ -148,11 +155,9 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
148
155
  self._update_pipette_config(state_update)
149
156
  self._update_pipette_nozzle_map(state_update)
150
157
  self._update_tip_state(state_update)
158
+ self._update_volumes(state_update)
151
159
 
152
- if isinstance(action, (SucceedCommandAction, FailCommandAction)):
153
- self._update_volumes(action)
154
-
155
- elif isinstance(action, SetPipetteMovementSpeedAction):
160
+ if isinstance(action, SetPipetteMovementSpeedAction):
156
161
  self._state.movement_speed_by_id[action.pipette_id] = action.speed
157
162
 
158
163
  def _set_load_pipette(self, state_update: update_types.StateUpdate) -> None:
@@ -167,7 +172,6 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
167
172
  self._state.liquid_presence_detection_by_id[pipette_id] = (
168
173
  state_update.loaded_pipette.liquid_presence_detection or False
169
174
  )
170
- self._state.aspirated_volume_by_id[pipette_id] = None
171
175
  self._state.movement_speed_by_id[pipette_id] = None
172
176
  self._state.attached_tip_by_id[pipette_id] = None
173
177
 
@@ -178,7 +182,6 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
178
182
  attached_tip = state_update.pipette_tip_state.tip_geometry
179
183
 
180
184
  self._state.attached_tip_by_id[pipette_id] = attached_tip
181
- self._state.aspirated_volume_by_id[pipette_id] = 0
182
185
 
183
186
  static_config = self._state.static_config_by_id.get(pipette_id)
184
187
  if static_config:
@@ -205,7 +208,6 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
205
208
 
206
209
  else:
207
210
  pipette_id = state_update.pipette_tip_state.pipette_id
208
- self._state.aspirated_volume_by_id[pipette_id] = None
209
211
  self._state.attached_tip_by_id[pipette_id] = None
210
212
 
211
213
  static_config = self._state.static_config_by_id.get(pipette_id)
@@ -293,6 +295,8 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
293
295
  ),
294
296
  default_nozzle_map=config.nozzle_map,
295
297
  lld_settings=config.pipette_lld_settings,
298
+ plunger_positions=config.plunger_positions,
299
+ shaft_ul_per_mm=config.shaft_ul_per_mm,
296
300
  available_sensors=config.available_sensors,
297
301
  )
298
302
  self._state.flow_rates_by_id[
@@ -310,54 +314,43 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
310
314
  state_update.pipette_nozzle_map.pipette_id
311
315
  ] = state_update.pipette_nozzle_map.nozzle_map
312
316
 
313
- def _update_volumes(
314
- self, action: Union[SucceedCommandAction, FailCommandAction]
317
+ def _update_volumes(self, state_update: update_types.StateUpdate) -> None:
318
+ if state_update.pipette_aspirated_fluid == update_types.NO_CHANGE:
319
+ return
320
+ if state_update.pipette_aspirated_fluid.type == "aspirated":
321
+ self._update_aspirated(state_update.pipette_aspirated_fluid)
322
+ elif state_update.pipette_aspirated_fluid.type == "ejected":
323
+ self._update_ejected(state_update.pipette_aspirated_fluid)
324
+ elif state_update.pipette_aspirated_fluid.type == "empty":
325
+ self._update_empty(state_update.pipette_aspirated_fluid)
326
+ elif state_update.pipette_aspirated_fluid.type == "unknown":
327
+ self._update_unknown(state_update.pipette_aspirated_fluid)
328
+ else:
329
+ assert_never(state_update.pipette_aspirated_fluid.type)
330
+
331
+ def _update_aspirated(
332
+ self, update: update_types.PipetteAspiratedFluidUpdate
315
333
  ) -> None:
316
- # todo(mm, 2024-10-10): Port these isinstance checks to StateUpdate.
317
- # https://opentrons.atlassian.net/browse/EXEC-754
334
+ self._fluid_stack_log_if_empty(update.pipette_id).add_fluid(update.fluid)
318
335
 
319
- if isinstance(action, SucceedCommandAction) and isinstance(
320
- action.command.result,
321
- (commands.AspirateResult, commands.AspirateInPlaceResult),
322
- ):
323
- pipette_id = action.command.params.pipetteId
324
- previous_volume = self._state.aspirated_volume_by_id[pipette_id] or 0
325
- # PipetteHandler will have clamped action.command.result.volume for us, so
326
- # next_volume should always be in bounds.
327
- next_volume = previous_volume + action.command.result.volume
336
+ def _update_ejected(self, update: update_types.PipetteEjectedFluidUpdate) -> None:
337
+ self._fluid_stack_log_if_empty(update.pipette_id).remove_fluid(update.volume)
328
338
 
329
- self._state.aspirated_volume_by_id[pipette_id] = next_volume
339
+ def _update_empty(self, update: update_types.PipetteEmptyFluidUpdate) -> None:
340
+ self._state.pipette_contents_by_id[update.pipette_id] = fluid_stack.FluidStack()
330
341
 
331
- elif isinstance(action, SucceedCommandAction) and isinstance(
332
- action.command.result,
333
- (commands.DispenseResult, commands.DispenseInPlaceResult),
334
- ):
335
- pipette_id = action.command.params.pipetteId
336
- previous_volume = self._state.aspirated_volume_by_id[pipette_id] or 0
337
- # PipetteHandler will have clamped action.command.result.volume for us, so
338
- # next_volume should always be in bounds.
339
- next_volume = previous_volume - action.command.result.volume
340
- self._state.aspirated_volume_by_id[pipette_id] = next_volume
341
-
342
- elif isinstance(action, SucceedCommandAction) and isinstance(
343
- action.command.result,
344
- (
345
- commands.BlowOutResult,
346
- commands.BlowOutInPlaceResult,
347
- commands.unsafe.UnsafeBlowOutInPlaceResult,
348
- ),
349
- ):
350
- pipette_id = action.command.params.pipetteId
351
- self._state.aspirated_volume_by_id[pipette_id] = None
342
+ def _update_unknown(self, update: update_types.PipetteUnknownFluidUpdate) -> None:
343
+ self._state.pipette_contents_by_id[update.pipette_id] = None
352
344
 
353
- elif isinstance(action, SucceedCommandAction) and isinstance(
354
- action.command.result, commands.PrepareToAspirateResult
355
- ):
356
- pipette_id = action.command.params.pipetteId
357
- self._state.aspirated_volume_by_id[pipette_id] = 0
345
+ def _fluid_stack_log_if_empty(self, pipette_id: str) -> fluid_stack.FluidStack:
346
+ stack = self._state.pipette_contents_by_id[pipette_id]
347
+ if stack is None:
348
+ LOG.error("Pipette state tried to alter an unknown-contents pipette")
349
+ return fluid_stack.FluidStack()
350
+ return stack
358
351
 
359
352
 
360
- class PipetteView(HasState[PipetteState]):
353
+ class PipetteView:
361
354
  """Read-only view of computed pipettes state."""
362
355
 
363
356
  _state: PipetteState
@@ -459,6 +452,10 @@ class PipetteView(HasState[PipetteState]):
459
452
  def get_aspirated_volume(self, pipette_id: str) -> Optional[float]:
460
453
  """Get the currently aspirated volume of a pipette by ID.
461
454
 
455
+ This is the volume currently displaced by the plunger relative to its bottom position,
456
+ regardless of whether that volume likely contains liquid or air. This makes it the right
457
+ function to call to know how much more volume the plunger may displace.
458
+
462
459
  Returns:
463
460
  The volume the pipette has aspirated.
464
461
  None, after blow-out and the plunger is in an unsafe position.
@@ -470,13 +467,50 @@ class PipetteView(HasState[PipetteState]):
470
467
  self.validate_tip_state(pipette_id, True)
471
468
 
472
469
  try:
473
- return self._state.aspirated_volume_by_id[pipette_id]
470
+ stack = self._state.pipette_contents_by_id[pipette_id]
471
+ if stack is None:
472
+ return None
473
+ return stack.aspirated_volume()
474
474
 
475
475
  except KeyError as e:
476
476
  raise errors.PipetteNotLoadedError(
477
477
  f"Pipette {pipette_id} not found; unable to get current volume."
478
478
  ) from e
479
479
 
480
+ def get_liquid_dispensed_by_ejecting_volume(
481
+ self, pipette_id: str, volume: float
482
+ ) -> Optional[float]:
483
+ """Get the amount of liquid (not air) that will be dispensed if the pipette ejects a specified volume.
484
+
485
+ For instance, if the pipette contains, in vertical order,
486
+ 10 ul air
487
+ 80 ul liquid
488
+ 5 ul air
489
+
490
+ then dispensing 10ul would result in 5ul of liquid; dispensing 85 ul would result in 80ul liquid; dispensing
491
+ 95ul would result in 80ul liquid.
492
+
493
+ Returns:
494
+ The volume of liquid that would be dispensed by the requested volume.
495
+ None, after blow-out or when the plunger is in an unsafe position.
496
+
497
+ Raises:
498
+ PipetteNotLoadedError: pipette ID does not exist.
499
+ TipnotAttachedError: No tip is attached to the pipette.
500
+ """
501
+ self.validate_tip_state(pipette_id, True)
502
+
503
+ try:
504
+ stack = self._state.pipette_contents_by_id[pipette_id]
505
+ if stack is None:
506
+ return None
507
+ return stack.liquid_part_of_dispense_volume(volume)
508
+
509
+ except KeyError as e:
510
+ raise errors.PipetteNotLoadedError(
511
+ f"Pipette {pipette_id} not found; unable to get current liquid volume."
512
+ ) from e
513
+
480
514
  def get_working_volume(self, pipette_id: str) -> float:
481
515
  """Get the working maximum volume of a pipette by ID.
482
516
 
@@ -643,6 +677,10 @@ class PipetteView(HasState[PipetteState]):
643
677
  nozzle_map = self._state.nozzle_configuration_by_id[pipette_id]
644
678
  return nozzle_map.starting_nozzle
645
679
 
680
+ def get_nozzle_configuration(self, pipette_id: str) -> NozzleMap:
681
+ """Get the nozzle map of the pipette."""
682
+ return self._state.nozzle_configuration_by_id[pipette_id]
683
+
646
684
  def _get_critical_point_offset_without_tip(
647
685
  self, pipette_id: str, critical_point: Optional[CriticalPoint]
648
686
  ) -> Point:
@@ -740,3 +778,42 @@ class PipetteView(HasState[PipetteState]):
740
778
  raise errors.PipetteNotLoadedError(
741
779
  f"Pipette {pipette_id} not found; unable to determine if pipette liquid presence detection enabled."
742
780
  ) from e
781
+
782
+ def get_nozzle_configuration_supports_lld(self, pipette_id: str) -> bool:
783
+ """Determine if the current partial tip configuration supports LLD."""
784
+ nozzle_map = self.get_nozzle_configuration(pipette_id)
785
+ if (
786
+ nozzle_map.physical_nozzle_count == 96
787
+ and nozzle_map.back_left != nozzle_map.full_instrument_back_left
788
+ and nozzle_map.front_right != nozzle_map.full_instrument_front_right
789
+ ):
790
+ return False
791
+ return True
792
+
793
+ def lookup_volume_to_mm_conversion(
794
+ self, pipette_id: str, volume: float, action: str
795
+ ) -> float:
796
+ """Get the volumn to mm conversion for a pipette."""
797
+ try:
798
+ lookup_volume = self.get_working_volume(pipette_id)
799
+ except errors.TipNotAttachedError:
800
+ lookup_volume = self.get_maximum_volume(pipette_id)
801
+
802
+ pipette_config = self.get_config(pipette_id)
803
+ lookup_table_from_config = pipette_config.tip_configuration_lookup_table
804
+ try:
805
+ tip_settings = lookup_table_from_config[lookup_volume]
806
+ except KeyError:
807
+ tip_settings = list(lookup_table_from_config.values())[0]
808
+ return calculate_ul_per_mm(
809
+ volume,
810
+ cast(UlPerMmAction, action),
811
+ tip_settings,
812
+ shaft_ul_per_mm=pipette_config.shaft_ul_per_mm,
813
+ )
814
+
815
+ def lookup_plunger_position_name(
816
+ self, pipette_id: str, position_name: str
817
+ ) -> float:
818
+ """Get the plunger position provided for the given pipette id."""
819
+ return self.get_config(pipette_id).plunger_positions[position_name]