opentrons 8.2.0a4__py2.py3-none-any.whl → 8.3.0a1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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/config/defaults_ot3.py +1 -0
  9. opentrons/drivers/asyncio/communication/__init__.py +2 -0
  10. opentrons/drivers/asyncio/communication/errors.py +16 -3
  11. opentrons/drivers/asyncio/communication/serial_connection.py +24 -9
  12. opentrons/drivers/command_builder.py +2 -2
  13. opentrons/drivers/flex_stacker/__init__.py +9 -0
  14. opentrons/drivers/flex_stacker/abstract.py +89 -0
  15. opentrons/drivers/flex_stacker/driver.py +260 -0
  16. opentrons/drivers/flex_stacker/simulator.py +109 -0
  17. opentrons/drivers/flex_stacker/types.py +138 -0
  18. opentrons/drivers/heater_shaker/driver.py +18 -3
  19. opentrons/drivers/temp_deck/driver.py +13 -3
  20. opentrons/drivers/thermocycler/driver.py +17 -3
  21. opentrons/execute.py +3 -1
  22. opentrons/hardware_control/__init__.py +1 -2
  23. opentrons/hardware_control/api.py +28 -20
  24. opentrons/hardware_control/backends/flex_protocol.py +17 -7
  25. opentrons/hardware_control/backends/ot3controller.py +213 -63
  26. opentrons/hardware_control/backends/ot3simulator.py +18 -9
  27. opentrons/hardware_control/backends/ot3utils.py +43 -15
  28. opentrons/hardware_control/dev_types.py +4 -0
  29. opentrons/hardware_control/emulation/heater_shaker.py +4 -0
  30. opentrons/hardware_control/emulation/module_server/client.py +1 -1
  31. opentrons/hardware_control/emulation/module_server/server.py +5 -3
  32. opentrons/hardware_control/emulation/settings.py +3 -4
  33. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +2 -1
  34. opentrons/hardware_control/instruments/ot2/pipette.py +15 -22
  35. opentrons/hardware_control/instruments/ot2/pipette_handler.py +8 -1
  36. opentrons/hardware_control/instruments/ot3/gripper.py +2 -2
  37. opentrons/hardware_control/instruments/ot3/pipette.py +23 -22
  38. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -1
  39. opentrons/hardware_control/modules/mod_abc.py +2 -2
  40. opentrons/hardware_control/motion_utilities.py +68 -0
  41. opentrons/hardware_control/nozzle_manager.py +39 -41
  42. opentrons/hardware_control/ot3_calibration.py +1 -1
  43. opentrons/hardware_control/ot3api.py +60 -23
  44. opentrons/hardware_control/protocols/gripper_controller.py +3 -0
  45. opentrons/hardware_control/protocols/hardware_manager.py +5 -1
  46. opentrons/hardware_control/protocols/liquid_handler.py +18 -0
  47. opentrons/hardware_control/protocols/motion_controller.py +6 -0
  48. opentrons/hardware_control/robot_calibration.py +1 -1
  49. opentrons/hardware_control/types.py +61 -0
  50. opentrons/protocol_api/__init__.py +20 -1
  51. opentrons/protocol_api/_liquid.py +24 -49
  52. opentrons/protocol_api/_liquid_properties.py +754 -0
  53. opentrons/protocol_api/_types.py +24 -0
  54. opentrons/protocol_api/core/common.py +2 -0
  55. opentrons/protocol_api/core/engine/instrument.py +82 -10
  56. opentrons/protocol_api/core/engine/labware.py +29 -7
  57. opentrons/protocol_api/core/engine/protocol.py +130 -5
  58. opentrons/protocol_api/core/engine/robot.py +139 -0
  59. opentrons/protocol_api/core/engine/well.py +4 -1
  60. opentrons/protocol_api/core/instrument.py +46 -4
  61. opentrons/protocol_api/core/labware.py +13 -4
  62. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +37 -3
  63. opentrons/protocol_api/core/legacy/legacy_labware_core.py +13 -4
  64. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +32 -1
  65. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  66. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +37 -3
  67. opentrons/protocol_api/core/protocol.py +34 -1
  68. opentrons/protocol_api/core/robot.py +51 -0
  69. opentrons/protocol_api/instrument_context.py +158 -44
  70. opentrons/protocol_api/labware.py +231 -7
  71. opentrons/protocol_api/module_contexts.py +21 -17
  72. opentrons/protocol_api/protocol_context.py +125 -4
  73. opentrons/protocol_api/robot_context.py +204 -32
  74. opentrons/protocol_api/validation.py +262 -3
  75. opentrons/protocol_engine/__init__.py +4 -0
  76. opentrons/protocol_engine/actions/actions.py +2 -3
  77. opentrons/protocol_engine/clients/sync_client.py +18 -0
  78. opentrons/protocol_engine/commands/__init__.py +81 -0
  79. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +0 -2
  80. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +19 -5
  81. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +0 -1
  82. opentrons/protocol_engine/commands/absorbance_reader/read.py +32 -9
  83. opentrons/protocol_engine/commands/air_gap_in_place.py +160 -0
  84. opentrons/protocol_engine/commands/aspirate.py +103 -53
  85. opentrons/protocol_engine/commands/aspirate_in_place.py +55 -51
  86. opentrons/protocol_engine/commands/blow_out.py +44 -39
  87. opentrons/protocol_engine/commands/blow_out_in_place.py +21 -32
  88. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +13 -6
  89. opentrons/protocol_engine/commands/calibration/calibrate_module.py +1 -1
  90. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +3 -3
  91. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +1 -1
  92. opentrons/protocol_engine/commands/command.py +73 -66
  93. opentrons/protocol_engine/commands/command_unions.py +101 -1
  94. opentrons/protocol_engine/commands/comment.py +1 -1
  95. opentrons/protocol_engine/commands/configure_for_volume.py +10 -3
  96. opentrons/protocol_engine/commands/configure_nozzle_layout.py +6 -4
  97. opentrons/protocol_engine/commands/custom.py +6 -12
  98. opentrons/protocol_engine/commands/dispense.py +82 -48
  99. opentrons/protocol_engine/commands/dispense_in_place.py +71 -51
  100. opentrons/protocol_engine/commands/drop_tip.py +52 -31
  101. opentrons/protocol_engine/commands/drop_tip_in_place.py +13 -3
  102. opentrons/protocol_engine/commands/generate_command_schema.py +4 -11
  103. opentrons/protocol_engine/commands/get_next_tip.py +134 -0
  104. opentrons/protocol_engine/commands/get_tip_presence.py +1 -1
  105. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +1 -1
  106. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +1 -1
  107. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +1 -1
  108. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +1 -1
  109. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +1 -1
  110. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +1 -1
  111. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +10 -4
  112. opentrons/protocol_engine/commands/home.py +13 -4
  113. opentrons/protocol_engine/commands/liquid_probe.py +67 -24
  114. opentrons/protocol_engine/commands/load_labware.py +29 -7
  115. opentrons/protocol_engine/commands/load_lid.py +146 -0
  116. opentrons/protocol_engine/commands/load_lid_stack.py +189 -0
  117. opentrons/protocol_engine/commands/load_liquid.py +12 -4
  118. opentrons/protocol_engine/commands/load_liquid_class.py +144 -0
  119. opentrons/protocol_engine/commands/load_module.py +31 -10
  120. opentrons/protocol_engine/commands/load_pipette.py +19 -8
  121. opentrons/protocol_engine/commands/magnetic_module/disengage.py +1 -1
  122. opentrons/protocol_engine/commands/magnetic_module/engage.py +1 -1
  123. opentrons/protocol_engine/commands/move_labware.py +19 -6
  124. opentrons/protocol_engine/commands/move_relative.py +35 -25
  125. opentrons/protocol_engine/commands/move_to_addressable_area.py +40 -27
  126. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +53 -32
  127. opentrons/protocol_engine/commands/move_to_coordinates.py +36 -22
  128. opentrons/protocol_engine/commands/move_to_well.py +40 -24
  129. opentrons/protocol_engine/commands/movement_common.py +338 -0
  130. opentrons/protocol_engine/commands/pick_up_tip.py +49 -27
  131. opentrons/protocol_engine/commands/pipetting_common.py +169 -87
  132. opentrons/protocol_engine/commands/prepare_to_aspirate.py +24 -33
  133. opentrons/protocol_engine/commands/reload_labware.py +1 -1
  134. opentrons/protocol_engine/commands/retract_axis.py +1 -1
  135. opentrons/protocol_engine/commands/robot/__init__.py +69 -0
  136. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +86 -0
  137. opentrons/protocol_engine/commands/robot/common.py +18 -0
  138. opentrons/protocol_engine/commands/robot/move_axes_relative.py +101 -0
  139. opentrons/protocol_engine/commands/robot/move_axes_to.py +100 -0
  140. opentrons/protocol_engine/commands/robot/move_to.py +94 -0
  141. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +77 -0
  142. opentrons/protocol_engine/commands/save_position.py +14 -5
  143. opentrons/protocol_engine/commands/set_rail_lights.py +1 -1
  144. opentrons/protocol_engine/commands/set_status_bar.py +1 -1
  145. opentrons/protocol_engine/commands/temperature_module/deactivate.py +1 -1
  146. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +1 -1
  147. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +10 -4
  148. opentrons/protocol_engine/commands/thermocycler/close_lid.py +1 -1
  149. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +1 -1
  150. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +1 -1
  151. opentrons/protocol_engine/commands/thermocycler/open_lid.py +1 -1
  152. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +8 -2
  153. opentrons/protocol_engine/commands/thermocycler/run_profile.py +9 -3
  154. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +11 -4
  155. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +1 -1
  156. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +1 -1
  157. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +1 -1
  158. opentrons/protocol_engine/commands/touch_tip.py +65 -16
  159. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +4 -1
  160. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -3
  161. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +1 -4
  162. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +1 -4
  163. opentrons/protocol_engine/commands/verify_tip_presence.py +11 -4
  164. opentrons/protocol_engine/commands/wait_for_duration.py +10 -3
  165. opentrons/protocol_engine/commands/wait_for_resume.py +10 -3
  166. opentrons/protocol_engine/errors/__init__.py +8 -0
  167. opentrons/protocol_engine/errors/error_occurrence.py +19 -20
  168. opentrons/protocol_engine/errors/exceptions.py +50 -0
  169. opentrons/protocol_engine/execution/command_executor.py +1 -1
  170. opentrons/protocol_engine/execution/equipment.py +73 -5
  171. opentrons/protocol_engine/execution/gantry_mover.py +364 -8
  172. opentrons/protocol_engine/execution/movement.py +27 -0
  173. opentrons/protocol_engine/execution/pipetting.py +5 -1
  174. opentrons/protocol_engine/execution/tip_handler.py +4 -6
  175. opentrons/protocol_engine/notes/notes.py +1 -1
  176. opentrons/protocol_engine/protocol_engine.py +7 -6
  177. opentrons/protocol_engine/resources/labware_data_provider.py +1 -1
  178. opentrons/protocol_engine/resources/labware_validation.py +5 -0
  179. opentrons/protocol_engine/resources/module_data_provider.py +1 -1
  180. opentrons/protocol_engine/resources/pipette_data_provider.py +26 -0
  181. opentrons/protocol_engine/slot_standardization.py +9 -9
  182. opentrons/protocol_engine/state/_move_types.py +9 -5
  183. opentrons/protocol_engine/state/_well_math.py +193 -0
  184. opentrons/protocol_engine/state/addressable_areas.py +25 -61
  185. opentrons/protocol_engine/state/command_history.py +12 -0
  186. opentrons/protocol_engine/state/commands.py +17 -13
  187. opentrons/protocol_engine/state/files.py +10 -12
  188. opentrons/protocol_engine/state/fluid_stack.py +138 -0
  189. opentrons/protocol_engine/state/frustum_helpers.py +57 -32
  190. opentrons/protocol_engine/state/geometry.py +47 -1
  191. opentrons/protocol_engine/state/labware.py +79 -25
  192. opentrons/protocol_engine/state/liquid_classes.py +82 -0
  193. opentrons/protocol_engine/state/liquids.py +16 -4
  194. opentrons/protocol_engine/state/modules.py +52 -70
  195. opentrons/protocol_engine/state/motion.py +6 -1
  196. opentrons/protocol_engine/state/pipettes.py +144 -58
  197. opentrons/protocol_engine/state/state.py +21 -2
  198. opentrons/protocol_engine/state/state_summary.py +4 -2
  199. opentrons/protocol_engine/state/tips.py +11 -44
  200. opentrons/protocol_engine/state/update_types.py +343 -48
  201. opentrons/protocol_engine/state/wells.py +19 -11
  202. opentrons/protocol_engine/types.py +176 -28
  203. opentrons/protocol_reader/extract_labware_definitions.py +5 -2
  204. opentrons/protocol_reader/file_format_validator.py +5 -5
  205. opentrons/protocol_runner/json_file_reader.py +9 -3
  206. opentrons/protocol_runner/json_translator.py +51 -25
  207. opentrons/protocol_runner/legacy_command_mapper.py +66 -64
  208. opentrons/protocol_runner/protocol_runner.py +35 -4
  209. opentrons/protocol_runner/python_protocol_wrappers.py +1 -1
  210. opentrons/protocol_runner/run_orchestrator.py +13 -3
  211. opentrons/protocols/advanced_control/common.py +38 -0
  212. opentrons/protocols/advanced_control/mix.py +1 -1
  213. opentrons/protocols/advanced_control/transfers/__init__.py +0 -0
  214. opentrons/protocols/advanced_control/transfers/common.py +56 -0
  215. opentrons/protocols/advanced_control/{transfers.py → transfers/transfer.py} +10 -85
  216. opentrons/protocols/api_support/definitions.py +1 -1
  217. opentrons/protocols/api_support/instrument.py +1 -1
  218. opentrons/protocols/api_support/util.py +10 -0
  219. opentrons/protocols/labware.py +39 -6
  220. opentrons/protocols/models/json_protocol.py +5 -9
  221. opentrons/simulate.py +3 -1
  222. opentrons/types.py +162 -2
  223. opentrons/util/logging_config.py +1 -1
  224. {opentrons-8.2.0a4.dist-info → opentrons-8.3.0a1.dist-info}/METADATA +16 -15
  225. {opentrons-8.2.0a4.dist-info → opentrons-8.3.0a1.dist-info}/RECORD +229 -202
  226. {opentrons-8.2.0a4.dist-info → opentrons-8.3.0a1.dist-info}/WHEEL +1 -1
  227. {opentrons-8.2.0a4.dist-info → opentrons-8.3.0a1.dist-info}/LICENSE +0 -0
  228. {opentrons-8.2.0a4.dist-info → opentrons-8.3.0a1.dist-info}/entry_points.txt +0 -0
  229. {opentrons-8.2.0a4.dist-info → opentrons-8.3.0a1.dist-info}/top_level.txt +0 -0
@@ -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,9 @@ 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
108
+ available_sensors: pipette_definition.AvailableSensorDefinition
101
109
 
102
110
 
103
111
  @dataclasses.dataclass
@@ -108,7 +116,7 @@ class PipetteState:
108
116
  # attributes are populated at the appropriate times. Refactor to a
109
117
  # single dict-of-many-things instead of many dicts-of-single-things.
110
118
  pipettes_by_id: Dict[str, LoadedPipette]
111
- aspirated_volume_by_id: Dict[str, Optional[float]]
119
+ pipette_contents_by_id: Dict[str, Optional[fluid_stack.FluidStack]]
112
120
  current_location: Optional[CurrentPipetteLocation]
113
121
  current_deck_point: CurrentDeckPoint
114
122
  attached_tip_by_id: Dict[str, Optional[TipGeometry]]
@@ -128,7 +136,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
128
136
  """Initialize a PipetteStore and its state."""
129
137
  self._state = PipetteState(
130
138
  pipettes_by_id={},
131
- aspirated_volume_by_id={},
139
+ pipette_contents_by_id={},
132
140
  attached_tip_by_id={},
133
141
  current_location=None,
134
142
  current_deck_point=CurrentDeckPoint(mount=None, deck_point=None),
@@ -147,11 +155,9 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
147
155
  self._update_pipette_config(state_update)
148
156
  self._update_pipette_nozzle_map(state_update)
149
157
  self._update_tip_state(state_update)
158
+ self._update_volumes(state_update)
150
159
 
151
- if isinstance(action, (SucceedCommandAction, FailCommandAction)):
152
- self._update_volumes(action)
153
-
154
- elif isinstance(action, SetPipetteMovementSpeedAction):
160
+ if isinstance(action, SetPipetteMovementSpeedAction):
155
161
  self._state.movement_speed_by_id[action.pipette_id] = action.speed
156
162
 
157
163
  def _set_load_pipette(self, state_update: update_types.StateUpdate) -> None:
@@ -166,7 +172,6 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
166
172
  self._state.liquid_presence_detection_by_id[pipette_id] = (
167
173
  state_update.loaded_pipette.liquid_presence_detection or False
168
174
  )
169
- self._state.aspirated_volume_by_id[pipette_id] = None
170
175
  self._state.movement_speed_by_id[pipette_id] = None
171
176
  self._state.attached_tip_by_id[pipette_id] = None
172
177
 
@@ -177,7 +182,6 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
177
182
  attached_tip = state_update.pipette_tip_state.tip_geometry
178
183
 
179
184
  self._state.attached_tip_by_id[pipette_id] = attached_tip
180
- self._state.aspirated_volume_by_id[pipette_id] = 0
181
185
 
182
186
  static_config = self._state.static_config_by_id.get(pipette_id)
183
187
  if static_config:
@@ -204,7 +208,6 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
204
208
 
205
209
  else:
206
210
  pipette_id = state_update.pipette_tip_state.pipette_id
207
- self._state.aspirated_volume_by_id[pipette_id] = None
208
211
  self._state.attached_tip_by_id[pipette_id] = None
209
212
 
210
213
  static_config = self._state.static_config_by_id.get(pipette_id)
@@ -292,6 +295,9 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
292
295
  ),
293
296
  default_nozzle_map=config.nozzle_map,
294
297
  lld_settings=config.pipette_lld_settings,
298
+ plunger_positions=config.plunger_positions,
299
+ shaft_ul_per_mm=config.shaft_ul_per_mm,
300
+ available_sensors=config.available_sensors,
295
301
  )
296
302
  self._state.flow_rates_by_id[
297
303
  state_update.pipette_config.pipette_id
@@ -308,54 +314,43 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
308
314
  state_update.pipette_nozzle_map.pipette_id
309
315
  ] = state_update.pipette_nozzle_map.nozzle_map
310
316
 
311
- def _update_volumes(
312
- 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
313
333
  ) -> None:
314
- # todo(mm, 2024-10-10): Port these isinstance checks to StateUpdate.
315
- # https://opentrons.atlassian.net/browse/EXEC-754
334
+ self._fluid_stack_log_if_empty(update.pipette_id).add_fluid(update.fluid)
316
335
 
317
- if isinstance(action, SucceedCommandAction) and isinstance(
318
- action.command.result,
319
- (commands.AspirateResult, commands.AspirateInPlaceResult),
320
- ):
321
- pipette_id = action.command.params.pipetteId
322
- previous_volume = self._state.aspirated_volume_by_id[pipette_id] or 0
323
- # PipetteHandler will have clamped action.command.result.volume for us, so
324
- # next_volume should always be in bounds.
325
- 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)
326
338
 
327
- 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()
328
341
 
329
- elif isinstance(action, SucceedCommandAction) and isinstance(
330
- action.command.result,
331
- (commands.DispenseResult, commands.DispenseInPlaceResult),
332
- ):
333
- pipette_id = action.command.params.pipetteId
334
- previous_volume = self._state.aspirated_volume_by_id[pipette_id] or 0
335
- # PipetteHandler will have clamped action.command.result.volume for us, so
336
- # next_volume should always be in bounds.
337
- next_volume = previous_volume - action.command.result.volume
338
- self._state.aspirated_volume_by_id[pipette_id] = next_volume
339
-
340
- elif isinstance(action, SucceedCommandAction) and isinstance(
341
- action.command.result,
342
- (
343
- commands.BlowOutResult,
344
- commands.BlowOutInPlaceResult,
345
- commands.unsafe.UnsafeBlowOutInPlaceResult,
346
- ),
347
- ):
348
- pipette_id = action.command.params.pipetteId
349
- 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
350
344
 
351
- elif isinstance(action, SucceedCommandAction) and isinstance(
352
- action.command.result, commands.PrepareToAspirateResult
353
- ):
354
- pipette_id = action.command.params.pipetteId
355
- 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
356
351
 
357
352
 
358
- class PipetteView(HasState[PipetteState]):
353
+ class PipetteView:
359
354
  """Read-only view of computed pipettes state."""
360
355
 
361
356
  _state: PipetteState
@@ -457,6 +452,10 @@ class PipetteView(HasState[PipetteState]):
457
452
  def get_aspirated_volume(self, pipette_id: str) -> Optional[float]:
458
453
  """Get the currently aspirated volume of a pipette by ID.
459
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
+
460
459
  Returns:
461
460
  The volume the pipette has aspirated.
462
461
  None, after blow-out and the plunger is in an unsafe position.
@@ -468,13 +467,50 @@ class PipetteView(HasState[PipetteState]):
468
467
  self.validate_tip_state(pipette_id, True)
469
468
 
470
469
  try:
471
- 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()
472
474
 
473
475
  except KeyError as e:
474
476
  raise errors.PipetteNotLoadedError(
475
477
  f"Pipette {pipette_id} not found; unable to get current volume."
476
478
  ) from e
477
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
+
478
514
  def get_working_volume(self, pipette_id: str) -> float:
479
515
  """Get the working maximum volume of a pipette by ID.
480
516
 
@@ -641,6 +677,10 @@ class PipetteView(HasState[PipetteState]):
641
677
  nozzle_map = self._state.nozzle_configuration_by_id[pipette_id]
642
678
  return nozzle_map.starting_nozzle
643
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
+
644
684
  def _get_critical_point_offset_without_tip(
645
685
  self, pipette_id: str, critical_point: Optional[CriticalPoint]
646
686
  ) -> Point:
@@ -723,6 +763,13 @@ class PipetteView(HasState[PipetteState]):
723
763
  pip_front_left_bound,
724
764
  )
725
765
 
766
+ def get_pipette_supports_pressure(self, pipette_id: str) -> bool:
767
+ """Return if this pipette supports a pressure sensor."""
768
+ return (
769
+ "pressure"
770
+ in self._state.static_config_by_id[pipette_id].available_sensors.sensors
771
+ )
772
+
726
773
  def get_liquid_presence_detection(self, pipette_id: str) -> bool:
727
774
  """Determine if liquid presence detection is enabled for this pipette."""
728
775
  try:
@@ -731,3 +778,42 @@ class PipetteView(HasState[PipetteState]):
731
778
  raise errors.PipetteNotLoadedError(
732
779
  f"Pipette {pipette_id} not found; unable to determine if pipette liquid presence detection enabled."
733
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]
@@ -9,7 +9,7 @@ from opentrons_shared_data.deck.types import DeckDefinitionV5
9
9
  from opentrons_shared_data.robot.types import RobotDefinition
10
10
 
11
11
  from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryPolicy
12
- from opentrons.protocol_engine.types import ModuleOffsetData
12
+ from opentrons.protocol_engine.types import LiquidClassRecordWithId, ModuleOffsetData
13
13
  from opentrons.util.change_notifier import ChangeNotifier
14
14
 
15
15
  from ..resources import DeckFixedLabware
@@ -25,6 +25,7 @@ from .labware import LabwareState, LabwareStore, LabwareView
25
25
  from .pipettes import PipetteState, PipetteStore, PipetteView
26
26
  from .modules import ModuleState, ModuleStore, ModuleView
27
27
  from .liquids import LiquidState, LiquidView, LiquidStore
28
+ from .liquid_classes import LiquidClassState, LiquidClassStore, LiquidClassView
28
29
  from .tips import TipState, TipView, TipStore
29
30
  from .wells import WellState, WellView, WellStore
30
31
  from .geometry import GeometryView
@@ -49,6 +50,7 @@ class State:
49
50
  pipettes: PipetteState
50
51
  modules: ModuleState
51
52
  liquids: LiquidState
53
+ liquid_classes: LiquidClassState
52
54
  tips: TipState
53
55
  wells: WellState
54
56
  files: FileState
@@ -64,6 +66,7 @@ class StateView(HasState[State]):
64
66
  _pipettes: PipetteView
65
67
  _modules: ModuleView
66
68
  _liquid: LiquidView
69
+ _liquid_classes: LiquidClassView
67
70
  _tips: TipView
68
71
  _wells: WellView
69
72
  _geometry: GeometryView
@@ -101,6 +104,11 @@ class StateView(HasState[State]):
101
104
  """Get state view selectors for liquid state."""
102
105
  return self._liquid
103
106
 
107
+ @property
108
+ def liquid_classes(self) -> LiquidClassView:
109
+ """Get state view selectors for liquid class state."""
110
+ return self._liquid_classes
111
+
104
112
  @property
105
113
  def tips(self) -> TipView:
106
114
  """Get state view selectors for tip state."""
@@ -135,7 +143,7 @@ class StateView(HasState[State]):
135
143
  """Get protocol run data."""
136
144
  error = self._commands.get_error()
137
145
  # TODO maybe add summary here for AA
138
- return StateSummary.construct(
146
+ return StateSummary.model_construct(
139
147
  status=self._commands.get_status(),
140
148
  errors=[] if error is None else [error],
141
149
  pipettes=self._pipettes.get_all(),
@@ -148,6 +156,12 @@ class StateView(HasState[State]):
148
156
  wells=self._wells.get_all(),
149
157
  hasEverEnteredErrorRecovery=self._commands.get_has_entered_recovery_mode(),
150
158
  files=self._state.files.file_ids,
159
+ liquidClasses=[
160
+ LiquidClassRecordWithId(
161
+ liquidClassId=liquid_class_id, **dict(liquid_class_record)
162
+ )
163
+ for liquid_class_id, liquid_class_record in self._liquid_classes.get_all().items()
164
+ ],
151
165
  )
152
166
 
153
167
 
@@ -213,6 +227,7 @@ class StateStore(StateView, ActionHandler):
213
227
  module_calibration_offsets=module_calibration_offsets,
214
228
  )
215
229
  self._liquid_store = LiquidStore()
230
+ self._liquid_class_store = LiquidClassStore()
216
231
  self._tip_store = TipStore()
217
232
  self._well_store = WellStore()
218
233
  self._file_store = FileStore()
@@ -224,6 +239,7 @@ class StateStore(StateView, ActionHandler):
224
239
  self._labware_store,
225
240
  self._module_store,
226
241
  self._liquid_store,
242
+ self._liquid_class_store,
227
243
  self._tip_store,
228
244
  self._well_store,
229
245
  self._file_store,
@@ -342,6 +358,7 @@ class StateStore(StateView, ActionHandler):
342
358
  pipettes=self._pipette_store.state,
343
359
  modules=self._module_store.state,
344
360
  liquids=self._liquid_store.state,
361
+ liquid_classes=self._liquid_class_store.state,
345
362
  tips=self._tip_store.state,
346
363
  wells=self._well_store.state,
347
364
  files=self._file_store.state,
@@ -359,6 +376,7 @@ class StateStore(StateView, ActionHandler):
359
376
  self._pipettes = PipetteView(state.pipettes)
360
377
  self._modules = ModuleView(state.modules)
361
378
  self._liquid = LiquidView(state.liquids)
379
+ self._liquid_classes = LiquidClassView(state.liquid_classes)
362
380
  self._tips = TipView(state.tips)
363
381
  self._wells = WellView(state.wells)
364
382
  self._files = FileView(state.files)
@@ -391,6 +409,7 @@ class StateStore(StateView, ActionHandler):
391
409
  self._pipettes._state = next_state.pipettes
392
410
  self._modules._state = next_state.modules
393
411
  self._liquid._state = next_state.liquids
412
+ self._liquid_classes._state = next_state.liquid_classes
394
413
  self._tips._state = next_state.tips
395
414
  self._wells._state = next_state.wells
396
415
  self._change_notifier.notify()
@@ -11,6 +11,7 @@ from ..types import (
11
11
  LoadedModule,
12
12
  LoadedPipette,
13
13
  Liquid,
14
+ LiquidClassRecordWithId,
14
15
  WellInfoSummary,
15
16
  )
16
17
 
@@ -27,8 +28,9 @@ class StateSummary(BaseModel):
27
28
  pipettes: List[LoadedPipette]
28
29
  modules: List[LoadedModule]
29
30
  labwareOffsets: List[LabwareOffset]
30
- startedAt: Optional[datetime]
31
- completedAt: Optional[datetime]
31
+ startedAt: Optional[datetime] = None
32
+ completedAt: Optional[datetime] = None
32
33
  liquids: List[Liquid] = Field(default_factory=list)
33
34
  wells: List[WellInfoSummary] = Field(default_factory=list)
34
35
  files: List[str] = Field(default_factory=list)
36
+ liquidClasses: List[LiquidClassRecordWithId] = Field(default_factory=list)
@@ -1,11 +1,14 @@
1
1
  """Tip state tracking."""
2
+
2
3
  from dataclasses import dataclass
3
4
  from enum import Enum
4
5
  from typing import Dict, Optional, List, Union
5
6
 
7
+ from opentrons.types import NozzleMapInterface
6
8
  from opentrons.protocol_engine.state import update_types
7
9
 
8
10
  from ._abstract_store import HasState, HandlesActions
11
+ from ._well_math import wells_covered_dense
9
12
  from ..actions import Action, ResetTipsAction, get_state_updates
10
13
 
11
14
  from opentrons.hardware_control.nozzle_manager import NozzleMap
@@ -108,49 +111,15 @@ class TipStore(HasState[TipState], HandlesActions):
108
111
  column for column in definition.ordering
109
112
  ]
110
113
 
111
- def _set_used_tips( # noqa: C901
112
- self, pipette_id: str, well_name: str, labware_id: str
113
- ) -> None:
114
+ def _set_used_tips(self, pipette_id: str, well_name: str, labware_id: str) -> None:
114
115
  columns = self._state.column_by_labware_id.get(labware_id, [])
115
116
  wells = self._state.tips_by_labware_id.get(labware_id, {})
116
117
  nozzle_map = self._state.pipette_info_by_pipette_id[pipette_id].nozzle_map
118
+ for well in wells_covered_dense(nozzle_map, well_name, columns):
119
+ wells[well] = TipRackWellState.USED
117
120
 
118
- # TODO (cb, 02-28-2024): Transition from using partial nozzle map to full instrument map for the set used logic
119
- num_nozzle_cols = len(nozzle_map.columns)
120
- num_nozzle_rows = len(nozzle_map.rows)
121
-
122
- critical_column = 0
123
- critical_row = 0
124
- for column in columns:
125
- if well_name in column:
126
- critical_row = column.index(well_name)
127
- critical_column = columns.index(column)
128
121
 
129
- for i in range(num_nozzle_cols):
130
- for j in range(num_nozzle_rows):
131
- if nozzle_map.starting_nozzle == "A1":
132
- if (critical_column + i < len(columns)) and (
133
- critical_row + j < len(columns[critical_column])
134
- ):
135
- well = columns[critical_column + i][critical_row + j]
136
- wells[well] = TipRackWellState.USED
137
- elif nozzle_map.starting_nozzle == "A12":
138
- if (critical_column - i >= 0) and (
139
- critical_row + j < len(columns[critical_column])
140
- ):
141
- well = columns[critical_column - i][critical_row + j]
142
- wells[well] = TipRackWellState.USED
143
- elif nozzle_map.starting_nozzle == "H1":
144
- if (critical_column + i < len(columns)) and (critical_row - j >= 0):
145
- well = columns[critical_column + i][critical_row - j]
146
- wells[well] = TipRackWellState.USED
147
- elif nozzle_map.starting_nozzle == "H12":
148
- if (critical_column - i >= 0) and (critical_row - j >= 0):
149
- well = columns[critical_column - i][critical_row - j]
150
- wells[well] = TipRackWellState.USED
151
-
152
-
153
- class TipView(HasState[TipState]):
122
+ class TipView:
154
123
  """Read-only tip state view."""
155
124
 
156
125
  _state: TipState
@@ -168,12 +137,13 @@ class TipView(HasState[TipState]):
168
137
  labware_id: str,
169
138
  num_tips: int,
170
139
  starting_tip_name: Optional[str],
171
- nozzle_map: Optional[NozzleMap],
140
+ nozzle_map: Optional[NozzleMapInterface],
172
141
  ) -> Optional[str]:
173
142
  """Get the next available clean tip. Does not support use of a starting tip if the pipette used is in a partial configuration."""
174
143
  wells = self._state.tips_by_labware_id.get(labware_id, {})
175
144
  columns = self._state.column_by_labware_id.get(labware_id, [])
176
145
 
146
+ # TODO(sf): I'm pretty sure this can be replaced with wells_covered_96 but I'm not quite sure how
177
147
  def _identify_tip_cluster(
178
148
  active_columns: int,
179
149
  active_rows: int,
@@ -224,10 +194,7 @@ class TipView(HasState[TipState]):
224
194
  return None
225
195
  else:
226
196
  # In the case of an 8ch pipette where a column has mixed state tips we may simply progress to the next column in our search
227
- if (
228
- nozzle_map is not None
229
- and len(nozzle_map.full_instrument_map_store) == 8
230
- ):
197
+ if nozzle_map is not None and nozzle_map.physical_nozzle_count == 8:
231
198
  return None
232
199
 
233
200
  # In the case of a 96ch we can attempt to index in by singular rows and columns assuming that indexed direction is safe
@@ -357,7 +324,7 @@ class TipView(HasState[TipState]):
357
324
  return None
358
325
 
359
326
  if starting_tip_name is None and nozzle_map is not None and columns:
360
- num_channels = len(nozzle_map.full_instrument_map_store)
327
+ num_channels = nozzle_map.physical_nozzle_count
361
328
  num_nozzle_cols = len(nozzle_map.columns)
362
329
  num_nozzle_rows = len(nozzle_map.rows)
363
330
  # Each pipette's cluster search is determined by the point of entry for a given pipette/configuration: