opentrons 8.1.0a0__py2.py3-none-any.whl → 8.2.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. opentrons/cli/analyze.py +71 -7
  2. opentrons/config/__init__.py +9 -0
  3. opentrons/config/advanced_settings.py +22 -0
  4. opentrons/config/defaults_ot3.py +14 -36
  5. opentrons/config/feature_flags.py +4 -0
  6. opentrons/config/types.py +6 -17
  7. opentrons/drivers/absorbance_reader/abstract.py +27 -3
  8. opentrons/drivers/absorbance_reader/async_byonoy.py +208 -154
  9. opentrons/drivers/absorbance_reader/driver.py +24 -15
  10. opentrons/drivers/absorbance_reader/hid_protocol.py +79 -50
  11. opentrons/drivers/absorbance_reader/simulator.py +32 -6
  12. opentrons/drivers/types.py +23 -1
  13. opentrons/execute.py +2 -2
  14. opentrons/hardware_control/api.py +18 -10
  15. opentrons/hardware_control/backends/controller.py +3 -2
  16. opentrons/hardware_control/backends/flex_protocol.py +11 -5
  17. opentrons/hardware_control/backends/ot3controller.py +18 -50
  18. opentrons/hardware_control/backends/ot3simulator.py +7 -6
  19. opentrons/hardware_control/backends/ot3utils.py +1 -0
  20. opentrons/hardware_control/instruments/ot2/pipette_handler.py +22 -82
  21. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -2
  22. opentrons/hardware_control/module_control.py +43 -2
  23. opentrons/hardware_control/modules/__init__.py +7 -1
  24. opentrons/hardware_control/modules/absorbance_reader.py +232 -83
  25. opentrons/hardware_control/modules/errors.py +7 -0
  26. opentrons/hardware_control/modules/heater_shaker.py +8 -3
  27. opentrons/hardware_control/modules/magdeck.py +12 -3
  28. opentrons/hardware_control/modules/mod_abc.py +27 -2
  29. opentrons/hardware_control/modules/tempdeck.py +15 -7
  30. opentrons/hardware_control/modules/thermocycler.py +69 -3
  31. opentrons/hardware_control/modules/types.py +11 -5
  32. opentrons/hardware_control/modules/update.py +11 -5
  33. opentrons/hardware_control/modules/utils.py +3 -1
  34. opentrons/hardware_control/ot3_calibration.py +6 -6
  35. opentrons/hardware_control/ot3api.py +131 -94
  36. opentrons/hardware_control/poller.py +15 -11
  37. opentrons/hardware_control/protocols/__init__.py +1 -7
  38. opentrons/hardware_control/protocols/instrument_configurer.py +14 -2
  39. opentrons/hardware_control/protocols/liquid_handler.py +5 -0
  40. opentrons/hardware_control/protocols/position_estimator.py +3 -1
  41. opentrons/hardware_control/types.py +2 -0
  42. opentrons/legacy_commands/helpers.py +8 -2
  43. opentrons/motion_planning/__init__.py +2 -0
  44. opentrons/motion_planning/waypoints.py +32 -0
  45. opentrons/protocol_api/__init__.py +2 -1
  46. opentrons/protocol_api/_liquid.py +87 -1
  47. opentrons/protocol_api/_parameter_context.py +10 -1
  48. opentrons/protocol_api/core/engine/deck_conflict.py +0 -297
  49. opentrons/protocol_api/core/engine/instrument.py +29 -25
  50. opentrons/protocol_api/core/engine/labware.py +20 -4
  51. opentrons/protocol_api/core/engine/module_core.py +166 -17
  52. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +362 -0
  53. opentrons/protocol_api/core/engine/protocol.py +30 -2
  54. opentrons/protocol_api/core/instrument.py +2 -0
  55. opentrons/protocol_api/core/labware.py +4 -0
  56. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -0
  57. opentrons/protocol_api/core/legacy/legacy_labware_core.py +5 -0
  58. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +6 -2
  59. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -0
  60. opentrons/protocol_api/core/module.py +22 -4
  61. opentrons/protocol_api/core/protocol.py +6 -2
  62. opentrons/protocol_api/instrument_context.py +52 -20
  63. opentrons/protocol_api/labware.py +13 -1
  64. opentrons/protocol_api/module_contexts.py +115 -17
  65. opentrons/protocol_api/protocol_context.py +49 -5
  66. opentrons/protocol_api/validation.py +5 -3
  67. opentrons/protocol_engine/__init__.py +10 -9
  68. opentrons/protocol_engine/actions/__init__.py +3 -0
  69. opentrons/protocol_engine/actions/actions.py +30 -25
  70. opentrons/protocol_engine/actions/get_state_update.py +38 -0
  71. opentrons/protocol_engine/clients/sync_client.py +1 -1
  72. opentrons/protocol_engine/clients/transports.py +1 -1
  73. opentrons/protocol_engine/commands/__init__.py +0 -4
  74. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +41 -11
  75. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +148 -0
  76. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +65 -9
  77. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +148 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/read.py +200 -0
  79. opentrons/protocol_engine/commands/aspirate.py +29 -16
  80. opentrons/protocol_engine/commands/aspirate_in_place.py +33 -16
  81. opentrons/protocol_engine/commands/blow_out.py +63 -14
  82. opentrons/protocol_engine/commands/blow_out_in_place.py +55 -13
  83. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +2 -5
  84. opentrons/protocol_engine/commands/calibration/calibrate_module.py +3 -4
  85. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +2 -5
  86. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +6 -4
  87. opentrons/protocol_engine/commands/command.py +31 -18
  88. opentrons/protocol_engine/commands/command_unions.py +37 -24
  89. opentrons/protocol_engine/commands/comment.py +5 -3
  90. opentrons/protocol_engine/commands/configure_for_volume.py +11 -14
  91. opentrons/protocol_engine/commands/configure_nozzle_layout.py +9 -15
  92. opentrons/protocol_engine/commands/custom.py +5 -3
  93. opentrons/protocol_engine/commands/dispense.py +42 -20
  94. opentrons/protocol_engine/commands/dispense_in_place.py +32 -14
  95. opentrons/protocol_engine/commands/drop_tip.py +70 -16
  96. opentrons/protocol_engine/commands/drop_tip_in_place.py +59 -13
  97. opentrons/protocol_engine/commands/get_tip_presence.py +5 -3
  98. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +6 -6
  99. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +6 -6
  100. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +6 -6
  101. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +8 -6
  102. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +8 -4
  103. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +6 -4
  104. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +6 -6
  105. opentrons/protocol_engine/commands/home.py +11 -5
  106. opentrons/protocol_engine/commands/liquid_probe.py +146 -88
  107. opentrons/protocol_engine/commands/load_labware.py +28 -5
  108. opentrons/protocol_engine/commands/load_liquid.py +18 -7
  109. opentrons/protocol_engine/commands/load_module.py +4 -6
  110. opentrons/protocol_engine/commands/load_pipette.py +18 -17
  111. opentrons/protocol_engine/commands/magnetic_module/disengage.py +6 -6
  112. opentrons/protocol_engine/commands/magnetic_module/engage.py +6 -4
  113. opentrons/protocol_engine/commands/move_labware.py +155 -23
  114. opentrons/protocol_engine/commands/move_relative.py +15 -3
  115. opentrons/protocol_engine/commands/move_to_addressable_area.py +29 -4
  116. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +13 -4
  117. opentrons/protocol_engine/commands/move_to_coordinates.py +11 -5
  118. opentrons/protocol_engine/commands/move_to_well.py +37 -10
  119. opentrons/protocol_engine/commands/pick_up_tip.py +51 -30
  120. opentrons/protocol_engine/commands/pipetting_common.py +47 -16
  121. opentrons/protocol_engine/commands/prepare_to_aspirate.py +62 -15
  122. opentrons/protocol_engine/commands/reload_labware.py +13 -4
  123. opentrons/protocol_engine/commands/retract_axis.py +6 -3
  124. opentrons/protocol_engine/commands/save_position.py +2 -3
  125. opentrons/protocol_engine/commands/set_rail_lights.py +5 -3
  126. opentrons/protocol_engine/commands/set_status_bar.py +5 -3
  127. opentrons/protocol_engine/commands/temperature_module/deactivate.py +6 -4
  128. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +3 -4
  129. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +6 -6
  130. opentrons/protocol_engine/commands/thermocycler/__init__.py +19 -0
  131. opentrons/protocol_engine/commands/thermocycler/close_lid.py +8 -8
  132. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +6 -4
  133. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +6 -4
  134. opentrons/protocol_engine/commands/thermocycler/open_lid.py +8 -4
  135. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +165 -0
  136. opentrons/protocol_engine/commands/thermocycler/run_profile.py +6 -6
  137. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +3 -4
  138. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +3 -4
  139. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +6 -4
  140. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +6 -4
  141. opentrons/protocol_engine/commands/touch_tip.py +19 -7
  142. opentrons/protocol_engine/commands/unsafe/__init__.py +30 -0
  143. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +6 -4
  144. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -4
  145. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +5 -3
  146. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +208 -0
  147. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +77 -0
  148. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +10 -4
  149. opentrons/protocol_engine/commands/verify_tip_presence.py +5 -5
  150. opentrons/protocol_engine/commands/wait_for_duration.py +5 -3
  151. opentrons/protocol_engine/commands/wait_for_resume.py +5 -3
  152. opentrons/protocol_engine/create_protocol_engine.py +60 -10
  153. opentrons/protocol_engine/engine_support.py +2 -1
  154. opentrons/protocol_engine/error_recovery_policy.py +14 -3
  155. opentrons/protocol_engine/errors/__init__.py +20 -0
  156. opentrons/protocol_engine/errors/error_occurrence.py +8 -3
  157. opentrons/protocol_engine/errors/exceptions.py +127 -2
  158. opentrons/protocol_engine/execution/__init__.py +2 -0
  159. opentrons/protocol_engine/execution/command_executor.py +22 -13
  160. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  161. opentrons/protocol_engine/execution/door_watcher.py +1 -1
  162. opentrons/protocol_engine/execution/equipment.py +2 -1
  163. opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
  164. opentrons/protocol_engine/execution/gantry_mover.py +4 -2
  165. opentrons/protocol_engine/execution/hardware_stopper.py +3 -3
  166. opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +1 -4
  167. opentrons/protocol_engine/execution/labware_movement.py +73 -22
  168. opentrons/protocol_engine/execution/movement.py +17 -7
  169. opentrons/protocol_engine/execution/pipetting.py +7 -4
  170. opentrons/protocol_engine/execution/queue_worker.py +6 -2
  171. opentrons/protocol_engine/execution/run_control.py +1 -1
  172. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +1 -1
  173. opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +2 -1
  174. opentrons/protocol_engine/execution/tip_handler.py +77 -43
  175. opentrons/protocol_engine/notes/__init__.py +14 -2
  176. opentrons/protocol_engine/notes/notes.py +18 -1
  177. opentrons/protocol_engine/plugins.py +1 -1
  178. opentrons/protocol_engine/protocol_engine.py +47 -31
  179. opentrons/protocol_engine/resources/__init__.py +2 -0
  180. opentrons/protocol_engine/resources/deck_data_provider.py +19 -5
  181. opentrons/protocol_engine/resources/file_provider.py +161 -0
  182. opentrons/protocol_engine/resources/fixture_validation.py +11 -1
  183. opentrons/protocol_engine/resources/labware_validation.py +10 -0
  184. opentrons/protocol_engine/state/__init__.py +0 -70
  185. opentrons/protocol_engine/state/addressable_areas.py +1 -1
  186. opentrons/protocol_engine/state/command_history.py +21 -2
  187. opentrons/protocol_engine/state/commands.py +110 -31
  188. opentrons/protocol_engine/state/files.py +59 -0
  189. opentrons/protocol_engine/state/frustum_helpers.py +440 -0
  190. opentrons/protocol_engine/state/geometry.py +445 -59
  191. opentrons/protocol_engine/state/labware.py +264 -84
  192. opentrons/protocol_engine/state/liquids.py +1 -1
  193. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +21 -3
  194. opentrons/protocol_engine/state/modules.py +145 -90
  195. opentrons/protocol_engine/state/motion.py +33 -14
  196. opentrons/protocol_engine/state/pipettes.py +157 -317
  197. opentrons/protocol_engine/state/state.py +30 -1
  198. opentrons/protocol_engine/state/state_summary.py +3 -0
  199. opentrons/protocol_engine/state/tips.py +69 -114
  200. opentrons/protocol_engine/state/update_types.py +424 -0
  201. opentrons/protocol_engine/state/wells.py +236 -0
  202. opentrons/protocol_engine/types.py +90 -0
  203. opentrons/protocol_reader/file_format_validator.py +83 -15
  204. opentrons/protocol_runner/json_translator.py +21 -5
  205. opentrons/protocol_runner/legacy_command_mapper.py +27 -6
  206. opentrons/protocol_runner/legacy_context_plugin.py +27 -71
  207. opentrons/protocol_runner/protocol_runner.py +6 -3
  208. opentrons/protocol_runner/run_orchestrator.py +41 -6
  209. opentrons/protocols/advanced_control/mix.py +3 -5
  210. opentrons/protocols/advanced_control/transfers.py +125 -56
  211. opentrons/protocols/api_support/constants.py +1 -1
  212. opentrons/protocols/api_support/definitions.py +1 -1
  213. opentrons/protocols/api_support/labware_like.py +4 -4
  214. opentrons/protocols/api_support/tip_tracker.py +2 -2
  215. opentrons/protocols/api_support/types.py +15 -2
  216. opentrons/protocols/api_support/util.py +30 -42
  217. opentrons/protocols/duration/errors.py +1 -1
  218. opentrons/protocols/duration/estimator.py +50 -29
  219. opentrons/protocols/execution/dev_types.py +2 -2
  220. opentrons/protocols/execution/execute_json_v4.py +15 -10
  221. opentrons/protocols/execution/execute_python.py +8 -3
  222. opentrons/protocols/geometry/planning.py +12 -12
  223. opentrons/protocols/labware.py +17 -33
  224. opentrons/protocols/parameters/csv_parameter_interface.py +3 -1
  225. opentrons/simulate.py +3 -3
  226. opentrons/types.py +30 -3
  227. opentrons/util/logging_config.py +34 -0
  228. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/METADATA +5 -4
  229. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/RECORD +235 -223
  230. opentrons/protocol_engine/commands/absorbance_reader/measure.py +0 -94
  231. opentrons/protocol_engine/commands/configuring_common.py +0 -26
  232. opentrons/protocol_runner/thread_async_queue.py +0 -174
  233. /opentrons/protocol_engine/state/{abstract_store.py → _abstract_store.py} +0 -0
  234. /opentrons/protocol_engine/state/{move_types.py → _move_types.py} +0 -0
  235. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/LICENSE +0 -0
  236. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/WHEEL +0 -0
  237. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/entry_points.txt +0 -0
  238. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,424 @@
1
+ """Structures to represent changes that commands want to make to engine state."""
2
+
3
+
4
+ import dataclasses
5
+ import enum
6
+ import typing
7
+ from datetime import datetime
8
+
9
+ from opentrons.hardware_control.nozzle_manager import NozzleMap
10
+ from opentrons.protocol_engine.resources import pipette_data_provider
11
+ from opentrons.protocol_engine.types import DeckPoint, LabwareLocation, TipGeometry
12
+ from opentrons.types import MountType
13
+ from opentrons_shared_data.labware.labware_definition import LabwareDefinition
14
+ from opentrons_shared_data.pipette.types import PipetteNameType
15
+
16
+
17
+ class _NoChangeEnum(enum.Enum):
18
+ NO_CHANGE = enum.auto()
19
+
20
+
21
+ NO_CHANGE: typing.Final = _NoChangeEnum.NO_CHANGE
22
+ """A sentinel value to indicate that a value shouldn't be changed.
23
+
24
+ Useful when `None` is semantically unclear or already has some other meaning.
25
+ """
26
+
27
+
28
+ NoChangeType: typing.TypeAlias = typing.Literal[_NoChangeEnum.NO_CHANGE]
29
+ """The type of `NO_CHANGE`, as `NoneType` is to `None`.
30
+
31
+ Unfortunately, mypy doesn't let us write `Literal[NO_CHANGE]`. Use this instead.
32
+ """
33
+
34
+
35
+ class _ClearEnum(enum.Enum):
36
+ CLEAR = enum.auto()
37
+
38
+
39
+ CLEAR: typing.Final = _ClearEnum.CLEAR
40
+ """A sentinel value to indicate that a value should be cleared.
41
+
42
+ Useful when `None` is semantically unclear or has some other meaning.
43
+ """
44
+
45
+
46
+ ClearType: typing.TypeAlias = typing.Literal[_ClearEnum.CLEAR]
47
+ """The type of `CLEAR`, as `NoneType` is to `None`.
48
+
49
+ Unfortunately, mypy doesn't let us write `Literal[CLEAR]`. Use this instead.
50
+ """
51
+
52
+
53
+ @dataclasses.dataclass(frozen=True)
54
+ class Well:
55
+ """Designates a well in a labware."""
56
+
57
+ labware_id: str
58
+ well_name: str
59
+
60
+
61
+ @dataclasses.dataclass(frozen=True)
62
+ class AddressableArea:
63
+ """Designates an addressable area."""
64
+
65
+ addressable_area_name: str
66
+
67
+
68
+ @dataclasses.dataclass
69
+ class PipetteLocationUpdate:
70
+ """An update to a pipette's location."""
71
+
72
+ pipette_id: str
73
+ """The ID of the already-loaded pipette."""
74
+
75
+ new_location: Well | AddressableArea | None | NoChangeType
76
+ """The pipette's new logical location.
77
+
78
+ Note: `new_location=None` means "change the location to `None` (unknown)",
79
+ not "do not change the location".
80
+ """
81
+
82
+ new_deck_point: DeckPoint | NoChangeType
83
+
84
+
85
+ @dataclasses.dataclass
86
+ class LabwareLocationUpdate:
87
+ """An update to a labware's location."""
88
+
89
+ labware_id: str
90
+ """The ID of the already-loaded labware."""
91
+
92
+ new_location: LabwareLocation
93
+ """The labware's new location."""
94
+
95
+ offset_id: typing.Optional[str]
96
+ """The ID of the labware's new offset, for its new location."""
97
+
98
+
99
+ @dataclasses.dataclass
100
+ class LoadedLabwareUpdate:
101
+ """An update that loads a new labware."""
102
+
103
+ labware_id: str
104
+ """The unique ID of the new labware."""
105
+
106
+ new_location: LabwareLocation
107
+ """The labware's initial location."""
108
+
109
+ offset_id: typing.Optional[str]
110
+ """The ID of the labware's offset."""
111
+
112
+ display_name: typing.Optional[str]
113
+
114
+ definition: LabwareDefinition
115
+
116
+
117
+ @dataclasses.dataclass
118
+ class LoadPipetteUpdate:
119
+ """An update that loads a new pipette.
120
+
121
+ NOTE: Currently, if this is provided, a PipetteConfigUpdate must always be
122
+ provided alongside it to fully initialize everything.
123
+ """
124
+
125
+ pipette_id: str
126
+ """The unique ID of the new pipette."""
127
+
128
+ pipette_name: PipetteNameType
129
+ mount: MountType
130
+ liquid_presence_detection: typing.Optional[bool]
131
+
132
+
133
+ @dataclasses.dataclass
134
+ class PipetteConfigUpdate:
135
+ """An update to a pipette's config."""
136
+
137
+ pipette_id: str
138
+ """The ID of the already-loaded pipette."""
139
+
140
+ # todo(mm, 2024-10-14): Does serial_number belong in LoadPipetteUpdate?
141
+ serial_number: str
142
+
143
+ config: pipette_data_provider.LoadedStaticPipetteData
144
+
145
+
146
+ @dataclasses.dataclass
147
+ class PipetteNozzleMapUpdate:
148
+ """Update pipette nozzle map."""
149
+
150
+ pipette_id: str
151
+ nozzle_map: NozzleMap
152
+
153
+
154
+ @dataclasses.dataclass
155
+ class PipetteTipStateUpdate:
156
+ """Update pipette tip state."""
157
+
158
+ pipette_id: str
159
+ tip_geometry: typing.Optional[TipGeometry]
160
+
161
+
162
+ @dataclasses.dataclass
163
+ class TipsUsedUpdate:
164
+ """Represents an update that marks tips in a tip rack as used."""
165
+
166
+ pipette_id: str
167
+ """The pipette that did the tip pickup."""
168
+
169
+ labware_id: str
170
+
171
+ well_name: str
172
+ """The well that the pipette's primary nozzle targeted.
173
+
174
+ Wells in addition to this one will also be marked as used, depending on the
175
+ pipette's nozzle layout.
176
+ """
177
+
178
+
179
+ @dataclasses.dataclass
180
+ class LiquidLoadedUpdate:
181
+ """An update from loading a liquid."""
182
+
183
+ labware_id: str
184
+ volumes: typing.Dict[str, float]
185
+ last_loaded: datetime
186
+
187
+
188
+ @dataclasses.dataclass
189
+ class LiquidProbedUpdate:
190
+ """An update from probing a liquid."""
191
+
192
+ labware_id: str
193
+ well_name: str
194
+ last_probed: datetime
195
+ height: float | ClearType
196
+ volume: float | ClearType
197
+
198
+
199
+ @dataclasses.dataclass
200
+ class LiquidOperatedUpdate:
201
+ """An update from operating a liquid."""
202
+
203
+ labware_id: str
204
+ well_name: str
205
+ volume_added: float | ClearType
206
+
207
+
208
+ @dataclasses.dataclass
209
+ class AbsorbanceReaderLidUpdate:
210
+ """An update to an absorbance reader's lid location."""
211
+
212
+ module_id: str
213
+ is_lid_on: bool
214
+
215
+
216
+ @dataclasses.dataclass
217
+ class StateUpdate:
218
+ """Represents an update to perform on engine state."""
219
+
220
+ pipette_location: PipetteLocationUpdate | NoChangeType | ClearType = NO_CHANGE
221
+
222
+ loaded_pipette: LoadPipetteUpdate | NoChangeType = NO_CHANGE
223
+
224
+ pipette_config: PipetteConfigUpdate | NoChangeType = NO_CHANGE
225
+
226
+ pipette_nozzle_map: PipetteNozzleMapUpdate | NoChangeType = NO_CHANGE
227
+
228
+ pipette_tip_state: PipetteTipStateUpdate | NoChangeType = NO_CHANGE
229
+
230
+ labware_location: LabwareLocationUpdate | NoChangeType = NO_CHANGE
231
+
232
+ loaded_labware: LoadedLabwareUpdate | NoChangeType = NO_CHANGE
233
+
234
+ tips_used: TipsUsedUpdate | NoChangeType = NO_CHANGE
235
+
236
+ liquid_loaded: LiquidLoadedUpdate | NoChangeType = NO_CHANGE
237
+
238
+ liquid_probed: LiquidProbedUpdate | NoChangeType = NO_CHANGE
239
+
240
+ liquid_operated: LiquidOperatedUpdate | NoChangeType = NO_CHANGE
241
+
242
+ absorbance_reader_lid: AbsorbanceReaderLidUpdate | NoChangeType = NO_CHANGE
243
+
244
+ # These convenience functions let the caller avoid the boilerplate of constructing a
245
+ # complicated dataclass tree.
246
+
247
+ @typing.overload
248
+ def set_pipette_location(
249
+ self,
250
+ *,
251
+ pipette_id: str,
252
+ new_labware_id: str,
253
+ new_well_name: str,
254
+ new_deck_point: DeckPoint,
255
+ ) -> None:
256
+ """Schedule a pipette's location to be set to a well."""
257
+
258
+ @typing.overload
259
+ def set_pipette_location(
260
+ self,
261
+ *,
262
+ pipette_id: str,
263
+ new_addressable_area_name: str,
264
+ new_deck_point: DeckPoint,
265
+ ) -> None:
266
+ """Schedule a pipette's location to be set to an addressable area."""
267
+ pass
268
+
269
+ def set_pipette_location( # noqa: D102
270
+ self,
271
+ *,
272
+ pipette_id: str,
273
+ new_labware_id: str | NoChangeType = NO_CHANGE,
274
+ new_well_name: str | NoChangeType = NO_CHANGE,
275
+ new_addressable_area_name: str | NoChangeType = NO_CHANGE,
276
+ new_deck_point: DeckPoint,
277
+ ) -> None:
278
+ if new_addressable_area_name != NO_CHANGE:
279
+ self.pipette_location = PipetteLocationUpdate(
280
+ pipette_id=pipette_id,
281
+ new_location=AddressableArea(
282
+ addressable_area_name=new_addressable_area_name
283
+ ),
284
+ new_deck_point=new_deck_point,
285
+ )
286
+ else:
287
+ # These asserts should always pass because of the overloads.
288
+ assert new_labware_id != NO_CHANGE
289
+ assert new_well_name != NO_CHANGE
290
+
291
+ self.pipette_location = PipetteLocationUpdate(
292
+ pipette_id=pipette_id,
293
+ new_location=Well(labware_id=new_labware_id, well_name=new_well_name),
294
+ new_deck_point=new_deck_point,
295
+ )
296
+
297
+ def clear_all_pipette_locations(self) -> None:
298
+ """Mark all pipettes as having an unknown location."""
299
+ self.pipette_location = CLEAR
300
+
301
+ def set_labware_location(
302
+ self,
303
+ *,
304
+ labware_id: str,
305
+ new_location: LabwareLocation,
306
+ new_offset_id: str | None,
307
+ ) -> None:
308
+ """Set a labware's location. See `LabwareLocationUpdate`."""
309
+ self.labware_location = LabwareLocationUpdate(
310
+ labware_id=labware_id,
311
+ new_location=new_location,
312
+ offset_id=new_offset_id,
313
+ )
314
+
315
+ def set_loaded_labware(
316
+ self,
317
+ definition: LabwareDefinition,
318
+ labware_id: str,
319
+ offset_id: typing.Optional[str],
320
+ display_name: typing.Optional[str],
321
+ location: LabwareLocation,
322
+ ) -> None:
323
+ """Add a new labware to state. See `LoadedLabwareUpdate`."""
324
+ self.loaded_labware = LoadedLabwareUpdate(
325
+ definition=definition,
326
+ labware_id=labware_id,
327
+ offset_id=offset_id,
328
+ new_location=location,
329
+ display_name=display_name,
330
+ )
331
+
332
+ def set_load_pipette(
333
+ self,
334
+ pipette_id: str,
335
+ pipette_name: PipetteNameType,
336
+ mount: MountType,
337
+ liquid_presence_detection: typing.Optional[bool],
338
+ ) -> None:
339
+ """Add a new pipette to state. See `LoadPipetteUpdate`."""
340
+ self.loaded_pipette = LoadPipetteUpdate(
341
+ pipette_id=pipette_id,
342
+ pipette_name=pipette_name,
343
+ mount=mount,
344
+ liquid_presence_detection=liquid_presence_detection,
345
+ )
346
+
347
+ def update_pipette_config(
348
+ self,
349
+ pipette_id: str,
350
+ config: pipette_data_provider.LoadedStaticPipetteData,
351
+ serial_number: str,
352
+ ) -> None:
353
+ """Update a pipette's config. See `PipetteConfigUpdate`."""
354
+ self.pipette_config = PipetteConfigUpdate(
355
+ pipette_id=pipette_id, config=config, serial_number=serial_number
356
+ )
357
+
358
+ def update_pipette_nozzle(self, pipette_id: str, nozzle_map: NozzleMap) -> None:
359
+ """Update a pipette's nozzle map. See `PipetteNozzleMapUpdate`."""
360
+ self.pipette_nozzle_map = PipetteNozzleMapUpdate(
361
+ pipette_id=pipette_id, nozzle_map=nozzle_map
362
+ )
363
+
364
+ def update_pipette_tip_state(
365
+ self, pipette_id: str, tip_geometry: typing.Optional[TipGeometry]
366
+ ) -> None:
367
+ """Update a pipette's tip state. See `PipetteTipStateUpdate`."""
368
+ self.pipette_tip_state = PipetteTipStateUpdate(
369
+ pipette_id=pipette_id, tip_geometry=tip_geometry
370
+ )
371
+
372
+ def mark_tips_as_used(
373
+ self, pipette_id: str, labware_id: str, well_name: str
374
+ ) -> None:
375
+ """Mark tips in a tip rack as used. See `TipsUsedUpdate`."""
376
+ self.tips_used = TipsUsedUpdate(
377
+ pipette_id=pipette_id, labware_id=labware_id, well_name=well_name
378
+ )
379
+
380
+ def set_liquid_loaded(
381
+ self,
382
+ labware_id: str,
383
+ volumes: typing.Dict[str, float],
384
+ last_loaded: datetime,
385
+ ) -> None:
386
+ """Add liquid volumes to well state. See `LoadLiquidUpdate`."""
387
+ self.liquid_loaded = LiquidLoadedUpdate(
388
+ labware_id=labware_id,
389
+ volumes=volumes,
390
+ last_loaded=last_loaded,
391
+ )
392
+
393
+ def set_liquid_probed(
394
+ self,
395
+ labware_id: str,
396
+ well_name: str,
397
+ last_probed: datetime,
398
+ height: float | ClearType,
399
+ volume: float | ClearType,
400
+ ) -> None:
401
+ """Add a liquid height and volume to well state. See `ProbeLiquidUpdate`."""
402
+ self.liquid_probed = LiquidProbedUpdate(
403
+ labware_id=labware_id,
404
+ well_name=well_name,
405
+ height=height,
406
+ volume=volume,
407
+ last_probed=last_probed,
408
+ )
409
+
410
+ def set_liquid_operated(
411
+ self, labware_id: str, well_name: str, volume_added: float | ClearType
412
+ ) -> None:
413
+ """Update liquid volumes in well state. See `OperateLiquidUpdate`."""
414
+ self.liquid_operated = LiquidOperatedUpdate(
415
+ labware_id=labware_id,
416
+ well_name=well_name,
417
+ volume_added=volume_added,
418
+ )
419
+
420
+ def set_absorbance_reader_lid(self, module_id: str, is_lid_on: bool) -> None:
421
+ """Update an absorbance reader's lid location. See `AbsorbanceReaderLidUpdate`."""
422
+ self.absorbance_reader_lid = AbsorbanceReaderLidUpdate(
423
+ module_id=module_id, is_lid_on=is_lid_on
424
+ )
@@ -0,0 +1,236 @@
1
+ """Basic well data state and store."""
2
+ from dataclasses import dataclass
3
+ from typing import Dict, List, Union, Iterator, Optional, Tuple, overload, TypeVar
4
+
5
+ from opentrons.protocol_engine.types import (
6
+ ProbedHeightInfo,
7
+ ProbedVolumeInfo,
8
+ LoadedVolumeInfo,
9
+ WellInfoSummary,
10
+ WellLiquidInfo,
11
+ )
12
+
13
+ from . import update_types
14
+ from ._abstract_store import HasState, HandlesActions
15
+ from ..actions import Action
16
+ from ..actions.get_state_update import get_state_updates
17
+
18
+
19
+ LabwareId = str
20
+ WellName = str
21
+
22
+
23
+ @dataclass
24
+ class WellState:
25
+ """State of all wells."""
26
+
27
+ loaded_volumes: Dict[LabwareId, Dict[WellName, LoadedVolumeInfo]]
28
+ probed_heights: Dict[LabwareId, Dict[WellName, ProbedHeightInfo]]
29
+ probed_volumes: Dict[LabwareId, Dict[WellName, ProbedVolumeInfo]]
30
+
31
+
32
+ class WellStore(HasState[WellState], HandlesActions):
33
+ """Well state container."""
34
+
35
+ _state: WellState
36
+
37
+ def __init__(self) -> None:
38
+ """Initialize a well store and its state."""
39
+ self._state = WellState(loaded_volumes={}, probed_heights={}, probed_volumes={})
40
+
41
+ def handle_action(self, action: Action) -> None:
42
+ """Modify state in reaction to an action."""
43
+ for state_update in get_state_updates(action):
44
+ if state_update.liquid_loaded != update_types.NO_CHANGE:
45
+ self._handle_liquid_loaded_update(state_update.liquid_loaded)
46
+ if state_update.liquid_probed != update_types.NO_CHANGE:
47
+ self._handle_liquid_probed_update(state_update.liquid_probed)
48
+ if state_update.liquid_operated != update_types.NO_CHANGE:
49
+ self._handle_liquid_operated_update(state_update.liquid_operated)
50
+
51
+ def _handle_liquid_loaded_update(
52
+ self, state_update: update_types.LiquidLoadedUpdate
53
+ ) -> None:
54
+ labware_id = state_update.labware_id
55
+ if labware_id not in self._state.loaded_volumes:
56
+ self._state.loaded_volumes[labware_id] = {}
57
+ for (well, volume) in state_update.volumes.items():
58
+ self._state.loaded_volumes[labware_id][well] = LoadedVolumeInfo(
59
+ volume=_none_from_clear(volume),
60
+ last_loaded=state_update.last_loaded,
61
+ operations_since_load=0,
62
+ )
63
+
64
+ def _handle_liquid_probed_update(
65
+ self, state_update: update_types.LiquidProbedUpdate
66
+ ) -> None:
67
+ labware_id = state_update.labware_id
68
+ well_name = state_update.well_name
69
+ if labware_id not in self._state.probed_heights:
70
+ self._state.probed_heights[labware_id] = {}
71
+ if labware_id not in self._state.probed_volumes:
72
+ self._state.probed_volumes[labware_id] = {}
73
+ self._state.probed_heights[labware_id][well_name] = ProbedHeightInfo(
74
+ height=_none_from_clear(state_update.height),
75
+ last_probed=state_update.last_probed,
76
+ )
77
+ self._state.probed_volumes[labware_id][well_name] = ProbedVolumeInfo(
78
+ volume=_none_from_clear(state_update.volume),
79
+ last_probed=state_update.last_probed,
80
+ operations_since_probe=0,
81
+ )
82
+
83
+ def _handle_liquid_operated_update(
84
+ self, state_update: update_types.LiquidOperatedUpdate
85
+ ) -> None:
86
+ labware_id = state_update.labware_id
87
+ well_name = state_update.well_name
88
+ if (
89
+ labware_id in self._state.loaded_volumes
90
+ and well_name in self._state.loaded_volumes[labware_id]
91
+ ):
92
+ if state_update.volume_added is update_types.CLEAR:
93
+ del self._state.loaded_volumes[labware_id][well_name]
94
+ else:
95
+ prev_loaded_vol_info = self._state.loaded_volumes[labware_id][well_name]
96
+ assert prev_loaded_vol_info.volume is not None
97
+ self._state.loaded_volumes[labware_id][well_name] = LoadedVolumeInfo(
98
+ volume=prev_loaded_vol_info.volume + state_update.volume_added,
99
+ last_loaded=prev_loaded_vol_info.last_loaded,
100
+ operations_since_load=prev_loaded_vol_info.operations_since_load
101
+ + 1,
102
+ )
103
+ if (
104
+ labware_id in self._state.probed_heights
105
+ and well_name in self._state.probed_heights[labware_id]
106
+ ):
107
+ del self._state.probed_heights[labware_id][well_name]
108
+ if (
109
+ labware_id in self._state.probed_volumes
110
+ and well_name in self._state.probed_volumes[labware_id]
111
+ ):
112
+ if state_update.volume_added is update_types.CLEAR:
113
+ del self._state.probed_volumes[labware_id][well_name]
114
+ else:
115
+ prev_probed_vol_info = self._state.probed_volumes[labware_id][well_name]
116
+ if prev_probed_vol_info.volume is None:
117
+ new_vol_info: float | None = None
118
+ else:
119
+ new_vol_info = (
120
+ prev_probed_vol_info.volume + state_update.volume_added
121
+ )
122
+ self._state.probed_volumes[labware_id][well_name] = ProbedVolumeInfo(
123
+ volume=new_vol_info,
124
+ last_probed=prev_probed_vol_info.last_probed,
125
+ operations_since_probe=prev_probed_vol_info.operations_since_probe
126
+ + 1,
127
+ )
128
+
129
+
130
+ class WellView(HasState[WellState]):
131
+ """Read-only well state view."""
132
+
133
+ _state: WellState
134
+
135
+ def __init__(self, state: WellState) -> None:
136
+ """Initialize the computed view of well state.
137
+
138
+ Arguments:
139
+ state: Well state dataclass used for all calculations.
140
+ """
141
+ self._state = state
142
+
143
+ def get_well_liquid_info(self, labware_id: str, well_name: str) -> WellLiquidInfo:
144
+ """Return all the liquid info for a well."""
145
+ if (
146
+ labware_id not in self._state.loaded_volumes
147
+ or well_name not in self._state.loaded_volumes[labware_id]
148
+ ):
149
+ loaded_volume_info = None
150
+ else:
151
+ loaded_volume_info = self._state.loaded_volumes[labware_id][well_name]
152
+ if (
153
+ labware_id not in self._state.probed_heights
154
+ or well_name not in self._state.probed_heights[labware_id]
155
+ ):
156
+ probed_height_info = None
157
+ else:
158
+ probed_height_info = self._state.probed_heights[labware_id][well_name]
159
+ if (
160
+ labware_id not in self._state.probed_volumes
161
+ or well_name not in self._state.probed_volumes[labware_id]
162
+ ):
163
+ probed_volume_info = None
164
+ else:
165
+ probed_volume_info = self._state.probed_volumes[labware_id][well_name]
166
+ return WellLiquidInfo(
167
+ loaded_volume=loaded_volume_info,
168
+ probed_height=probed_height_info,
169
+ probed_volume=probed_volume_info,
170
+ )
171
+
172
+ def get_all(self) -> List[WellInfoSummary]:
173
+ """Get all well liquid info summaries."""
174
+
175
+ def _all_well_combos() -> Iterator[Tuple[str, str, str]]:
176
+ for labware, lv_wells in self._state.loaded_volumes.items():
177
+ for well_name in lv_wells.keys():
178
+ yield f"{labware}{well_name}", labware, well_name
179
+ for labware, ph_wells in self._state.probed_heights.items():
180
+ for well_name in ph_wells.keys():
181
+ yield f"{labware}{well_name}", labware, well_name
182
+ for labware, pv_wells in self._state.probed_volumes.items():
183
+ for well_name in pv_wells.keys():
184
+ yield f"{labware}{well_name}", labware, well_name
185
+
186
+ wells = {
187
+ key: (labware_id, well_name)
188
+ for key, labware_id, well_name in _all_well_combos()
189
+ }
190
+ return [
191
+ self._summarize_well(labware_id, well_name)
192
+ for labware_id, well_name in wells.values()
193
+ ]
194
+
195
+ def _summarize_well(self, labware_id: str, well_name: str) -> WellInfoSummary:
196
+ well_liquid_info = self.get_well_liquid_info(labware_id, well_name)
197
+ return WellInfoSummary(
198
+ labware_id=labware_id,
199
+ well_name=well_name,
200
+ loaded_volume=_volume_from_info(well_liquid_info.loaded_volume),
201
+ probed_volume=_volume_from_info(well_liquid_info.probed_volume),
202
+ probed_height=_height_from_info(well_liquid_info.probed_height),
203
+ )
204
+
205
+
206
+ @overload
207
+ def _volume_from_info(info: Optional[ProbedVolumeInfo]) -> Optional[float]:
208
+ ...
209
+
210
+
211
+ @overload
212
+ def _volume_from_info(info: Optional[LoadedVolumeInfo]) -> Optional[float]:
213
+ ...
214
+
215
+
216
+ def _volume_from_info(
217
+ info: Union[ProbedVolumeInfo, LoadedVolumeInfo, None]
218
+ ) -> Optional[float]:
219
+ if info is None:
220
+ return None
221
+ return info.volume
222
+
223
+
224
+ def _height_from_info(info: Optional[ProbedHeightInfo]) -> Optional[float]:
225
+ if info is None:
226
+ return None
227
+ return info.height
228
+
229
+
230
+ MaybeClear = TypeVar("MaybeClear")
231
+
232
+
233
+ def _none_from_clear(inval: MaybeClear | update_types.ClearType) -> MaybeClear | None:
234
+ if inval == update_types.CLEAR:
235
+ return None
236
+ return inval