opentrons 8.2.0a3__py2.py3-none-any.whl → 8.3.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.

Potentially problematic release.


This version of opentrons might be problematic. Click here for more details.

Files changed (238) 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 +33 -21
  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 +78 -31
  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 +22 -1
  47. opentrons/hardware_control/protocols/motion_controller.py +7 -0
  48. opentrons/hardware_control/robot_calibration.py +1 -1
  49. opentrons/hardware_control/types.py +61 -0
  50. opentrons/legacy_commands/commands.py +37 -0
  51. opentrons/legacy_commands/types.py +39 -0
  52. opentrons/protocol_api/__init__.py +20 -1
  53. opentrons/protocol_api/_liquid.py +24 -49
  54. opentrons/protocol_api/_liquid_properties.py +754 -0
  55. opentrons/protocol_api/_types.py +24 -0
  56. opentrons/protocol_api/core/common.py +2 -0
  57. opentrons/protocol_api/core/engine/instrument.py +191 -10
  58. opentrons/protocol_api/core/engine/labware.py +29 -7
  59. opentrons/protocol_api/core/engine/protocol.py +130 -5
  60. opentrons/protocol_api/core/engine/robot.py +139 -0
  61. opentrons/protocol_api/core/engine/well.py +4 -1
  62. opentrons/protocol_api/core/instrument.py +73 -4
  63. opentrons/protocol_api/core/labware.py +13 -4
  64. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +87 -3
  65. opentrons/protocol_api/core/legacy/legacy_labware_core.py +13 -4
  66. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +32 -1
  67. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  68. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +61 -3
  69. opentrons/protocol_api/core/protocol.py +34 -1
  70. opentrons/protocol_api/core/robot.py +51 -0
  71. opentrons/protocol_api/instrument_context.py +299 -44
  72. opentrons/protocol_api/labware.py +248 -9
  73. opentrons/protocol_api/module_contexts.py +21 -17
  74. opentrons/protocol_api/protocol_context.py +125 -4
  75. opentrons/protocol_api/robot_context.py +204 -32
  76. opentrons/protocol_api/validation.py +262 -3
  77. opentrons/protocol_engine/__init__.py +4 -0
  78. opentrons/protocol_engine/actions/actions.py +2 -3
  79. opentrons/protocol_engine/clients/sync_client.py +18 -0
  80. opentrons/protocol_engine/commands/__init__.py +121 -0
  81. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +1 -3
  82. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +20 -6
  83. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +1 -2
  84. opentrons/protocol_engine/commands/absorbance_reader/read.py +40 -10
  85. opentrons/protocol_engine/commands/air_gap_in_place.py +160 -0
  86. opentrons/protocol_engine/commands/aspirate.py +103 -53
  87. opentrons/protocol_engine/commands/aspirate_in_place.py +55 -51
  88. opentrons/protocol_engine/commands/blow_out.py +44 -39
  89. opentrons/protocol_engine/commands/blow_out_in_place.py +21 -32
  90. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +13 -6
  91. opentrons/protocol_engine/commands/calibration/calibrate_module.py +1 -1
  92. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +3 -3
  93. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +1 -1
  94. opentrons/protocol_engine/commands/command.py +73 -66
  95. opentrons/protocol_engine/commands/command_unions.py +140 -1
  96. opentrons/protocol_engine/commands/comment.py +1 -1
  97. opentrons/protocol_engine/commands/configure_for_volume.py +10 -3
  98. opentrons/protocol_engine/commands/configure_nozzle_layout.py +6 -4
  99. opentrons/protocol_engine/commands/custom.py +6 -12
  100. opentrons/protocol_engine/commands/dispense.py +82 -48
  101. opentrons/protocol_engine/commands/dispense_in_place.py +71 -51
  102. opentrons/protocol_engine/commands/drop_tip.py +52 -31
  103. opentrons/protocol_engine/commands/drop_tip_in_place.py +79 -8
  104. opentrons/protocol_engine/commands/evotip_dispense.py +156 -0
  105. opentrons/protocol_engine/commands/evotip_seal_pipette.py +331 -0
  106. opentrons/protocol_engine/commands/evotip_unseal_pipette.py +160 -0
  107. opentrons/protocol_engine/commands/generate_command_schema.py +4 -11
  108. opentrons/protocol_engine/commands/get_next_tip.py +134 -0
  109. opentrons/protocol_engine/commands/get_tip_presence.py +1 -1
  110. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +1 -1
  111. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +1 -1
  112. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +1 -1
  113. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +1 -1
  114. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +1 -1
  115. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +1 -1
  116. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +10 -4
  117. opentrons/protocol_engine/commands/home.py +13 -4
  118. opentrons/protocol_engine/commands/liquid_probe.py +125 -31
  119. opentrons/protocol_engine/commands/load_labware.py +33 -6
  120. opentrons/protocol_engine/commands/load_lid.py +146 -0
  121. opentrons/protocol_engine/commands/load_lid_stack.py +189 -0
  122. opentrons/protocol_engine/commands/load_liquid.py +12 -4
  123. opentrons/protocol_engine/commands/load_liquid_class.py +144 -0
  124. opentrons/protocol_engine/commands/load_module.py +31 -10
  125. opentrons/protocol_engine/commands/load_pipette.py +19 -8
  126. opentrons/protocol_engine/commands/magnetic_module/disengage.py +1 -1
  127. opentrons/protocol_engine/commands/magnetic_module/engage.py +1 -1
  128. opentrons/protocol_engine/commands/move_labware.py +28 -6
  129. opentrons/protocol_engine/commands/move_relative.py +35 -25
  130. opentrons/protocol_engine/commands/move_to_addressable_area.py +40 -27
  131. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +53 -32
  132. opentrons/protocol_engine/commands/move_to_coordinates.py +36 -22
  133. opentrons/protocol_engine/commands/move_to_well.py +40 -24
  134. opentrons/protocol_engine/commands/movement_common.py +338 -0
  135. opentrons/protocol_engine/commands/pick_up_tip.py +49 -27
  136. opentrons/protocol_engine/commands/pipetting_common.py +169 -87
  137. opentrons/protocol_engine/commands/prepare_to_aspirate.py +24 -33
  138. opentrons/protocol_engine/commands/reload_labware.py +1 -1
  139. opentrons/protocol_engine/commands/retract_axis.py +1 -1
  140. opentrons/protocol_engine/commands/robot/__init__.py +69 -0
  141. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +86 -0
  142. opentrons/protocol_engine/commands/robot/common.py +18 -0
  143. opentrons/protocol_engine/commands/robot/move_axes_relative.py +101 -0
  144. opentrons/protocol_engine/commands/robot/move_axes_to.py +100 -0
  145. opentrons/protocol_engine/commands/robot/move_to.py +94 -0
  146. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +77 -0
  147. opentrons/protocol_engine/commands/save_position.py +14 -5
  148. opentrons/protocol_engine/commands/set_rail_lights.py +1 -1
  149. opentrons/protocol_engine/commands/set_status_bar.py +1 -1
  150. opentrons/protocol_engine/commands/temperature_module/deactivate.py +1 -1
  151. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +1 -1
  152. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +10 -4
  153. opentrons/protocol_engine/commands/thermocycler/close_lid.py +1 -1
  154. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +1 -1
  155. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +1 -1
  156. opentrons/protocol_engine/commands/thermocycler/open_lid.py +1 -1
  157. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +9 -3
  158. opentrons/protocol_engine/commands/thermocycler/run_profile.py +9 -3
  159. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +11 -4
  160. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +1 -1
  161. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +1 -1
  162. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +1 -1
  163. opentrons/protocol_engine/commands/touch_tip.py +65 -16
  164. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +5 -2
  165. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +13 -4
  166. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +2 -5
  167. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +1 -1
  168. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +4 -2
  169. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +2 -5
  170. opentrons/protocol_engine/commands/verify_tip_presence.py +11 -4
  171. opentrons/protocol_engine/commands/wait_for_duration.py +10 -3
  172. opentrons/protocol_engine/commands/wait_for_resume.py +10 -3
  173. opentrons/protocol_engine/errors/__init__.py +12 -0
  174. opentrons/protocol_engine/errors/error_occurrence.py +19 -20
  175. opentrons/protocol_engine/errors/exceptions.py +76 -0
  176. opentrons/protocol_engine/execution/command_executor.py +1 -1
  177. opentrons/protocol_engine/execution/equipment.py +73 -5
  178. opentrons/protocol_engine/execution/gantry_mover.py +369 -8
  179. opentrons/protocol_engine/execution/hardware_stopper.py +7 -7
  180. opentrons/protocol_engine/execution/movement.py +27 -0
  181. opentrons/protocol_engine/execution/pipetting.py +5 -1
  182. opentrons/protocol_engine/execution/tip_handler.py +34 -15
  183. opentrons/protocol_engine/notes/notes.py +1 -1
  184. opentrons/protocol_engine/protocol_engine.py +7 -6
  185. opentrons/protocol_engine/resources/labware_data_provider.py +1 -1
  186. opentrons/protocol_engine/resources/labware_validation.py +18 -0
  187. opentrons/protocol_engine/resources/module_data_provider.py +1 -1
  188. opentrons/protocol_engine/resources/pipette_data_provider.py +26 -0
  189. opentrons/protocol_engine/slot_standardization.py +9 -9
  190. opentrons/protocol_engine/state/_move_types.py +9 -5
  191. opentrons/protocol_engine/state/_well_math.py +193 -0
  192. opentrons/protocol_engine/state/addressable_areas.py +25 -61
  193. opentrons/protocol_engine/state/command_history.py +12 -0
  194. opentrons/protocol_engine/state/commands.py +22 -14
  195. opentrons/protocol_engine/state/files.py +10 -12
  196. opentrons/protocol_engine/state/fluid_stack.py +138 -0
  197. opentrons/protocol_engine/state/frustum_helpers.py +63 -69
  198. opentrons/protocol_engine/state/geometry.py +47 -1
  199. opentrons/protocol_engine/state/labware.py +92 -26
  200. opentrons/protocol_engine/state/liquid_classes.py +82 -0
  201. opentrons/protocol_engine/state/liquids.py +16 -4
  202. opentrons/protocol_engine/state/modules.py +56 -71
  203. opentrons/protocol_engine/state/motion.py +6 -1
  204. opentrons/protocol_engine/state/pipettes.py +149 -58
  205. opentrons/protocol_engine/state/state.py +21 -2
  206. opentrons/protocol_engine/state/state_summary.py +4 -2
  207. opentrons/protocol_engine/state/tips.py +11 -44
  208. opentrons/protocol_engine/state/update_types.py +343 -48
  209. opentrons/protocol_engine/state/wells.py +19 -11
  210. opentrons/protocol_engine/types.py +176 -28
  211. opentrons/protocol_reader/extract_labware_definitions.py +5 -2
  212. opentrons/protocol_reader/file_format_validator.py +5 -5
  213. opentrons/protocol_runner/json_file_reader.py +9 -3
  214. opentrons/protocol_runner/json_translator.py +51 -25
  215. opentrons/protocol_runner/legacy_command_mapper.py +66 -64
  216. opentrons/protocol_runner/protocol_runner.py +35 -4
  217. opentrons/protocol_runner/python_protocol_wrappers.py +1 -1
  218. opentrons/protocol_runner/run_orchestrator.py +13 -3
  219. opentrons/protocols/advanced_control/common.py +38 -0
  220. opentrons/protocols/advanced_control/mix.py +1 -1
  221. opentrons/protocols/advanced_control/transfers/__init__.py +0 -0
  222. opentrons/protocols/advanced_control/transfers/common.py +56 -0
  223. opentrons/protocols/advanced_control/{transfers.py → transfers/transfer.py} +10 -85
  224. opentrons/protocols/api_support/definitions.py +1 -1
  225. opentrons/protocols/api_support/instrument.py +1 -1
  226. opentrons/protocols/api_support/util.py +10 -0
  227. opentrons/protocols/labware.py +70 -8
  228. opentrons/protocols/models/json_protocol.py +5 -9
  229. opentrons/simulate.py +3 -1
  230. opentrons/types.py +162 -2
  231. opentrons/util/entrypoint_util.py +2 -5
  232. opentrons/util/logging_config.py +1 -1
  233. {opentrons-8.2.0a3.dist-info → opentrons-8.3.0.dist-info}/METADATA +16 -15
  234. {opentrons-8.2.0a3.dist-info → opentrons-8.3.0.dist-info}/RECORD +238 -208
  235. {opentrons-8.2.0a3.dist-info → opentrons-8.3.0.dist-info}/WHEEL +1 -1
  236. {opentrons-8.2.0a3.dist-info → opentrons-8.3.0.dist-info}/LICENSE +0 -0
  237. {opentrons-8.2.0a3.dist-info → opentrons-8.3.0.dist-info}/entry_points.txt +0 -0
  238. {opentrons-8.2.0a3.dist-info → opentrons-8.3.0.dist-info}/top_level.txt +0 -0
@@ -82,19 +82,12 @@ def _circular_frustum_polynomial_roots(
82
82
 
83
83
 
84
84
  def _volume_from_height_circular(
85
- target_height: float,
86
- total_frustum_height: float,
87
- bottom_radius: float,
88
- top_radius: float,
85
+ target_height: float, segment: ConicalFrustum
89
86
  ) -> float:
90
87
  """Find the volume given a height within a circular frustum."""
91
- a, b, c = _circular_frustum_polynomial_roots(
92
- bottom_radius=bottom_radius,
93
- top_radius=top_radius,
94
- total_frustum_height=total_frustum_height,
95
- )
96
- volume = a * (target_height**3) + b * (target_height**2) + c * target_height
97
- return volume
88
+ heights = segment.height_to_volume_table.keys()
89
+ best_fit_height = min(heights, key=lambda x: abs(x - target_height))
90
+ return segment.height_to_volume_table[best_fit_height]
98
91
 
99
92
 
100
93
  def _volume_from_height_rectangular(
@@ -138,26 +131,12 @@ def _volume_from_height_squared_cone(
138
131
 
139
132
 
140
133
  def _height_from_volume_circular(
141
- volume: float,
142
- total_frustum_height: float,
143
- bottom_radius: float,
144
- top_radius: float,
134
+ target_volume: float, segment: ConicalFrustum
145
135
  ) -> float:
146
- """Find the height given a volume within a circular frustum."""
147
- a, b, c = _circular_frustum_polynomial_roots(
148
- bottom_radius=bottom_radius,
149
- top_radius=top_radius,
150
- total_frustum_height=total_frustum_height,
151
- )
152
- d = volume * -1
153
- x_intercept_roots = (a, b, c, d)
154
-
155
- height_from_volume_roots = roots(x_intercept_roots)
156
- height = _reject_unacceptable_heights(
157
- potential_heights=list(height_from_volume_roots),
158
- max_height=total_frustum_height,
159
- )
160
- return height
136
+ """Find the height given a volume within a squared cone segment."""
137
+ volumes = segment.volume_to_height_table.keys()
138
+ best_fit_volume = min(volumes, key=lambda x: abs(x - target_volume))
139
+ return segment.volume_to_height_table[best_fit_volume]
161
140
 
162
141
 
163
142
  def _height_from_volume_rectangular(
@@ -220,28 +199,38 @@ def _get_segment_capacity(segment: WellSegment) -> float:
220
199
  section_height = segment.topHeight - segment.bottomHeight
221
200
  match segment:
222
201
  case SphericalSegment():
223
- return _volume_from_height_spherical(
224
- target_height=segment.topHeight,
225
- radius_of_curvature=segment.radiusOfCurvature,
202
+ return (
203
+ _volume_from_height_spherical(
204
+ target_height=segment.topHeight,
205
+ radius_of_curvature=segment.radiusOfCurvature,
206
+ )
207
+ * segment.count
226
208
  )
227
209
  case CuboidalFrustum():
228
- return _volume_from_height_rectangular(
229
- target_height=section_height,
230
- bottom_length=segment.bottomYDimension,
231
- bottom_width=segment.bottomXDimension,
232
- top_length=segment.topYDimension,
233
- top_width=segment.topXDimension,
234
- total_frustum_height=section_height,
210
+ return (
211
+ _volume_from_height_rectangular(
212
+ target_height=section_height,
213
+ bottom_length=segment.bottomYDimension,
214
+ bottom_width=segment.bottomXDimension,
215
+ top_length=segment.topYDimension,
216
+ top_width=segment.topXDimension,
217
+ total_frustum_height=section_height,
218
+ )
219
+ * segment.count
235
220
  )
236
221
  case ConicalFrustum():
237
- return _volume_from_height_circular(
238
- target_height=section_height,
239
- total_frustum_height=section_height,
240
- bottom_radius=(segment.bottomDiameter / 2),
241
- top_radius=(segment.topDiameter / 2),
222
+ return (
223
+ _volume_from_height_circular(
224
+ target_height=section_height,
225
+ segment=segment,
226
+ )
227
+ * segment.count
242
228
  )
243
229
  case SquaredConeSegment():
244
- return _volume_from_height_squared_cone(section_height, segment)
230
+ return (
231
+ _volume_from_height_squared_cone(section_height, segment)
232
+ * segment.count
233
+ )
245
234
  case _:
246
235
  # TODO: implement volume calculations for truncated circular and rounded rectangular segments
247
236
  raise NotImplementedError(
@@ -272,6 +261,7 @@ def height_at_volume_within_section(
272
261
  section_height: float,
273
262
  ) -> float:
274
263
  """Calculate a height within a bounded section according to geometry."""
264
+ target_volume_relative = target_volume_relative / section.count
275
265
  match section:
276
266
  case SphericalSegment():
277
267
  return _height_from_volume_spherical(
@@ -280,12 +270,7 @@ def height_at_volume_within_section(
280
270
  radius_of_curvature=section.radiusOfCurvature,
281
271
  )
282
272
  case ConicalFrustum():
283
- return _height_from_volume_circular(
284
- volume=target_volume_relative,
285
- top_radius=(section.bottomDiameter / 2),
286
- bottom_radius=(section.topDiameter / 2),
287
- total_frustum_height=section_height,
288
- )
273
+ return _height_from_volume_circular(target_volume_relative, section)
289
274
  case CuboidalFrustum():
290
275
  return _height_from_volume_rectangular(
291
276
  volume=target_volume_relative,
@@ -311,28 +296,37 @@ def volume_at_height_within_section(
311
296
  """Calculate a volume within a bounded section according to geometry."""
312
297
  match section:
313
298
  case SphericalSegment():
314
- return _volume_from_height_spherical(
315
- target_height=target_height_relative,
316
- radius_of_curvature=section.radiusOfCurvature,
299
+ return (
300
+ _volume_from_height_spherical(
301
+ target_height=target_height_relative,
302
+ radius_of_curvature=section.radiusOfCurvature,
303
+ )
304
+ * section.count
317
305
  )
318
306
  case ConicalFrustum():
319
- return _volume_from_height_circular(
320
- target_height=target_height_relative,
321
- total_frustum_height=section_height,
322
- bottom_radius=(section.bottomDiameter / 2),
323
- top_radius=(section.topDiameter / 2),
307
+ return (
308
+ _volume_from_height_circular(
309
+ target_height=target_height_relative, segment=section
310
+ )
311
+ * section.count
324
312
  )
325
313
  case CuboidalFrustum():
326
- return _volume_from_height_rectangular(
327
- target_height=target_height_relative,
328
- total_frustum_height=section_height,
329
- bottom_width=section.bottomXDimension,
330
- bottom_length=section.bottomYDimension,
331
- top_width=section.topXDimension,
332
- top_length=section.topYDimension,
314
+ return (
315
+ _volume_from_height_rectangular(
316
+ target_height=target_height_relative,
317
+ total_frustum_height=section_height,
318
+ bottom_width=section.bottomXDimension,
319
+ bottom_length=section.bottomYDimension,
320
+ top_width=section.topXDimension,
321
+ top_length=section.topYDimension,
322
+ )
323
+ * section.count
333
324
  )
334
325
  case SquaredConeSegment():
335
- return _volume_from_height_squared_cone(target_height_relative, section)
326
+ return (
327
+ _volume_from_height_squared_cone(target_height_relative, section)
328
+ * section.count
329
+ )
336
330
  case _:
337
331
  # TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712
338
332
  # we need to input the math attached to that issue
@@ -402,7 +396,7 @@ def _find_height_in_partial_frustum(
402
396
  if (
403
397
  bottom_section_volume
404
398
  < target_volume
405
- < (bottom_section_volume + section_volume)
399
+ <= (bottom_section_volume + section_volume)
406
400
  ):
407
401
  relative_target_volume = target_volume - bottom_section_volume
408
402
  section_height = section.topHeight - section.bottomHeight
@@ -1,4 +1,5 @@
1
1
  """Geometry state getters."""
2
+
2
3
  import enum
3
4
  from numpy import array, dot, double as npdouble
4
5
  from numpy.typing import NDArray
@@ -8,6 +9,7 @@ from functools import cached_property
8
9
 
9
10
  from opentrons.types import Point, DeckSlotName, StagingSlotName, MountType
10
11
 
12
+ from opentrons_shared_data.errors.exceptions import InvalidStoredData
11
13
  from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN
12
14
  from opentrons_shared_data.deck.types import CutoutFixture
13
15
  from opentrons_shared_data.pipette import PIPETTE_X_SPAN
@@ -61,6 +63,7 @@ from .frustum_helpers import (
61
63
  find_volume_at_well_height,
62
64
  find_height_at_well_volume,
63
65
  )
66
+ from ._well_math import wells_covered_by_pipette_configuration, nozzles_per_well
64
67
 
65
68
 
66
69
  SLOT_WIDTH = 128
@@ -486,7 +489,7 @@ class GeometryView:
486
489
  well_depth=well_depth,
487
490
  operation_volume=operation_volume,
488
491
  )
489
- offset = offset.copy(update={"z": offset.z + offset_adjustment})
492
+ offset = offset.model_copy(update={"z": offset.z + offset_adjustment})
490
493
  self.validate_well_position(
491
494
  well_location=well_location, z_offset=offset.z, pipette_id=pipette_id
492
495
  )
@@ -1559,3 +1562,46 @@ class GeometryView:
1559
1562
  raise errors.InvalidDispenseVolumeError(
1560
1563
  f"Attempting to dispense {volume}µL of liquid into a well that can only hold {well_volumetric_capacity}µL (well {well_name} in labware_id: {labware_id})"
1561
1564
  )
1565
+
1566
+ def get_wells_covered_by_pipette_with_active_well(
1567
+ self, labware_id: str, target_well_name: str, pipette_id: str
1568
+ ) -> list[str]:
1569
+ """Get a flat list of wells that are covered by a pipette when moved to a specified well.
1570
+
1571
+ When you move a pipette in a multichannel configuration to a specific well - the target well -
1572
+ the pipette will operate on other wells as well.
1573
+
1574
+ For instance, a pipette with a COLUMN configuration with well A1 of an SBS standard labware target
1575
+ will also "cover", under this definition, wells B1-H1. That same pipette, when C5 is the target well, will "cover"
1576
+ wells C5-H5.
1577
+
1578
+ This math only works, and may only be applied, if one of the following is true:
1579
+ - The pipette is in a SINGLE configuration
1580
+ - The pipette is in a non-SINGLE configuration, and the labware is an SBS-format 96 or 384 well plate (and is so
1581
+ marked in its definition's parameters.format key, as 96Standard or 384Standard)
1582
+
1583
+ If all of the following do not apply, regardless of the nozzle configuration of the pipette this function will
1584
+ return only the labware covered by the primary well.
1585
+ """
1586
+ pipette_nozzle_map = self._pipettes.get_nozzle_configuration(pipette_id)
1587
+ labware_columns = [
1588
+ column for column in self._labware.get_definition(labware_id).ordering
1589
+ ]
1590
+ try:
1591
+ return list(
1592
+ wells_covered_by_pipette_configuration(
1593
+ pipette_nozzle_map, target_well_name, labware_columns
1594
+ )
1595
+ )
1596
+ except InvalidStoredData:
1597
+ return [target_well_name]
1598
+
1599
+ def get_nozzles_per_well(
1600
+ self, labware_id: str, target_well_name: str, pipette_id: str
1601
+ ) -> int:
1602
+ """Get the number of nozzles that will interact with each well."""
1603
+ return nozzles_per_well(
1604
+ self._pipettes.get_nozzle_configuration(pipette_id),
1605
+ target_well_name,
1606
+ self._labware.get_definition(labware_id).ordering,
1607
+ )
@@ -131,7 +131,7 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
131
131
  for fixed_labware in deck_fixed_labware
132
132
  }
133
133
  labware_by_id = {
134
- fixed_labware.labware_id: LoadedLabware.construct(
134
+ fixed_labware.labware_id: LoadedLabware.model_construct(
135
135
  id=fixed_labware.labware_id,
136
136
  location=fixed_labware.location,
137
137
  loadName=fixed_labware.definition.parameters.loadName,
@@ -156,10 +156,12 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
156
156
  """Modify state in reaction to an action."""
157
157
  for state_update in get_state_updates(action):
158
158
  self._add_loaded_labware(state_update)
159
+ self._add_loaded_lid_stack(state_update)
159
160
  self._set_labware_location(state_update)
161
+ self._set_labware_lid(state_update)
160
162
 
161
163
  if isinstance(action, AddLabwareOffsetAction):
162
- labware_offset = LabwareOffset.construct(
164
+ labware_offset = LabwareOffset.model_construct(
163
165
  id=action.labware_offset_id,
164
166
  createdAt=action.created_at,
165
167
  definitionUri=action.request.definitionUri,
@@ -212,7 +214,7 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
212
214
 
213
215
  self._state.labware_by_id[
214
216
  loaded_labware_update.labware_id
215
- ] = LoadedLabware.construct(
217
+ ] = LoadedLabware.model_construct(
216
218
  id=loaded_labware_update.labware_id,
217
219
  location=location,
218
220
  loadName=loaded_labware_update.definition.parameters.loadName,
@@ -221,6 +223,63 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
221
223
  displayName=display_name,
222
224
  )
223
225
 
226
+ def _add_loaded_lid_stack(self, state_update: update_types.StateUpdate) -> None:
227
+ loaded_lid_stack_update = state_update.loaded_lid_stack
228
+ if loaded_lid_stack_update != update_types.NO_CHANGE:
229
+ # Add the stack object
230
+ stack_definition_uri = uri_from_details(
231
+ namespace=loaded_lid_stack_update.stack_object_definition.namespace,
232
+ load_name=loaded_lid_stack_update.stack_object_definition.parameters.loadName,
233
+ version=loaded_lid_stack_update.stack_object_definition.version,
234
+ )
235
+ self.state.definitions_by_uri[
236
+ stack_definition_uri
237
+ ] = loaded_lid_stack_update.stack_object_definition
238
+ self._state.labware_by_id[
239
+ loaded_lid_stack_update.stack_id
240
+ ] = LoadedLabware.construct(
241
+ id=loaded_lid_stack_update.stack_id,
242
+ location=loaded_lid_stack_update.stack_location,
243
+ loadName=loaded_lid_stack_update.stack_object_definition.parameters.loadName,
244
+ definitionUri=stack_definition_uri,
245
+ offsetId=None,
246
+ displayName=None,
247
+ )
248
+
249
+ # Add the Lids on top of the stack object
250
+ for i in range(len(loaded_lid_stack_update.labware_ids)):
251
+ definition_uri = uri_from_details(
252
+ namespace=loaded_lid_stack_update.definition.namespace,
253
+ load_name=loaded_lid_stack_update.definition.parameters.loadName,
254
+ version=loaded_lid_stack_update.definition.version,
255
+ )
256
+
257
+ self._state.definitions_by_uri[
258
+ definition_uri
259
+ ] = loaded_lid_stack_update.definition
260
+
261
+ location = loaded_lid_stack_update.new_locations_by_id[
262
+ loaded_lid_stack_update.labware_ids[i]
263
+ ]
264
+
265
+ self._state.labware_by_id[
266
+ loaded_lid_stack_update.labware_ids[i]
267
+ ] = LoadedLabware.construct(
268
+ id=loaded_lid_stack_update.labware_ids[i],
269
+ location=location,
270
+ loadName=loaded_lid_stack_update.definition.parameters.loadName,
271
+ definitionUri=definition_uri,
272
+ offsetId=None,
273
+ displayName=None,
274
+ )
275
+
276
+ def _set_labware_lid(self, state_update: update_types.StateUpdate) -> None:
277
+ labware_lid_update = state_update.labware_lid
278
+ if labware_lid_update != update_types.NO_CHANGE:
279
+ parent_labware_id = labware_lid_update.parent_labware_id
280
+ lid_id = labware_lid_update.lid_id
281
+ self._state.labware_by_id[parent_labware_id].lid_id = lid_id
282
+
224
283
  def _set_labware_location(self, state_update: update_types.StateUpdate) -> None:
225
284
  labware_location_update = state_update.labware_location
226
285
  if labware_location_update != update_types.NO_CHANGE:
@@ -244,7 +303,7 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
244
303
  self._state.labware_by_id[labware_id].location = new_location
245
304
 
246
305
 
247
- class LabwareView(HasState[LabwareState]):
306
+ class LabwareView:
248
307
  """Read-only labware state view."""
249
308
 
250
309
  _state: LabwareState
@@ -268,7 +327,7 @@ class LabwareView(HasState[LabwareState]):
268
327
 
269
328
  def get_id_by_module(self, module_id: str) -> str:
270
329
  """Return the ID of the labware loaded on the given module."""
271
- for labware_id, labware in self.state.labware_by_id.items():
330
+ for labware_id, labware in self._state.labware_by_id.items():
272
331
  if (
273
332
  isinstance(labware.location, ModuleLocation)
274
333
  and labware.location.moduleId == module_id
@@ -281,7 +340,7 @@ class LabwareView(HasState[LabwareState]):
281
340
 
282
341
  def get_id_by_labware(self, labware_id: str) -> str:
283
342
  """Return the ID of the labware loaded on the given labware."""
284
- for labware in self.state.labware_by_id.values():
343
+ for labware in self._state.labware_by_id.values():
285
344
  if (
286
345
  isinstance(labware.location, OnLabwareLocation)
287
346
  and labware.location.labwareId == labware_id
@@ -441,21 +500,7 @@ class LabwareView(HasState[LabwareState]):
441
500
 
442
501
  If not defined within a labware, defaults to one.
443
502
  """
444
- stacking_quirks = {
445
- "stackingMaxFive": 5,
446
- "stackingMaxFour": 4,
447
- "stackingMaxThree": 3,
448
- "stackingMaxTwo": 2,
449
- "stackingMaxOne": 1,
450
- "stackingMaxZero": 0,
451
- }
452
- for quirk in stacking_quirks.keys():
453
- if (
454
- labware.parameters.quirks is not None
455
- and quirk in labware.parameters.quirks
456
- ):
457
- return stacking_quirks[quirk]
458
- return 1
503
+ return labware.stackLimit if labware.stackLimit is not None else 1
459
504
 
460
505
  def get_should_center_pipette_on_target_well(self, labware_id: str) -> bool:
461
506
  """True if a pipette moving to a well of this labware should center its body on the target.
@@ -479,7 +524,6 @@ class LabwareView(HasState[LabwareState]):
479
524
  will be used.
480
525
  """
481
526
  definition = self.get_definition(labware_id)
482
-
483
527
  if well_name is None:
484
528
  well_name = definition.ordering[0][0]
485
529
 
@@ -815,6 +859,11 @@ class LabwareView(HasState[LabwareState]):
815
859
  return self.raise_if_labware_inaccessible_by_pipette(
816
860
  labware_location.labwareId
817
861
  )
862
+ elif labware.lid_id is not None:
863
+ raise errors.LocationNotAccessibleByPipetteError(
864
+ f"Cannot move pipette to {labware.loadName} "
865
+ "because labware is currently covered by a lid."
866
+ )
818
867
  elif isinstance(labware_location, AddressableAreaLocation):
819
868
  if fixture_validation.is_staging_slot(labware_location.addressableAreaName):
820
869
  raise errors.LocationNotAccessibleByPipetteError(
@@ -837,6 +886,19 @@ class LabwareView(HasState[LabwareState]):
837
886
  f"Labware {labware.loadName} is already present at {location}."
838
887
  )
839
888
 
889
+ def raise_if_labware_cannot_be_ondeck(
890
+ self,
891
+ location: LabwareLocation,
892
+ labware_definition: LabwareDefinition,
893
+ ) -> None:
894
+ """Raise an error if the labware cannot be in the specified location."""
895
+ if isinstance(
896
+ location, (DeckSlotLocation, AddressableAreaLocation)
897
+ ) and not labware_validation.validate_labware_can_be_ondeck(labware_definition):
898
+ raise errors.LabwareCannotSitOnDeckError(
899
+ f"{labware_definition.parameters.loadName} cannot sit in a slot by itself."
900
+ )
901
+
840
902
  def raise_if_labware_incompatible_with_plate_reader(
841
903
  self,
842
904
  labware_definition: LabwareDefinition,
@@ -998,11 +1060,15 @@ class LabwareView(HasState[LabwareState]):
998
1060
  return None
999
1061
  else:
1000
1062
  return LabwareMovementOffsetData(
1001
- pickUpOffset=cast(
1002
- LabwareOffsetVector, parsed_offsets[offset_key].pickUpOffset
1063
+ pickUpOffset=LabwareOffsetVector.model_construct(
1064
+ x=parsed_offsets[offset_key].pickUpOffset.x,
1065
+ y=parsed_offsets[offset_key].pickUpOffset.y,
1066
+ z=parsed_offsets[offset_key].pickUpOffset.z,
1003
1067
  ),
1004
- dropOffset=cast(
1005
- LabwareOffsetVector, parsed_offsets[offset_key].dropOffset
1068
+ dropOffset=LabwareOffsetVector.model_construct(
1069
+ x=parsed_offsets[offset_key].dropOffset.x,
1070
+ y=parsed_offsets[offset_key].dropOffset.y,
1071
+ z=parsed_offsets[offset_key].dropOffset.z,
1006
1072
  ),
1007
1073
  )
1008
1074
 
@@ -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