opentrons 8.1.0__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.0.dist-info → opentrons-8.2.0.dist-info}/METADATA +5 -4
  229. {opentrons-8.1.0.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.0.dist-info → opentrons-8.2.0.dist-info}/LICENSE +0 -0
  236. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/WHEEL +0 -0
  237. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/entry_points.txt +0 -0
  238. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,440 @@
1
+ """Helper functions for liquid-level related calculations inside a given frustum."""
2
+ from typing import List, Tuple
3
+ from numpy import pi, iscomplex, roots, real
4
+ from math import isclose
5
+
6
+ from ..errors.exceptions import InvalidLiquidHeightFound
7
+
8
+ from opentrons_shared_data.labware.labware_definition import (
9
+ InnerWellGeometry,
10
+ WellSegment,
11
+ SphericalSegment,
12
+ ConicalFrustum,
13
+ CuboidalFrustum,
14
+ SquaredConeSegment,
15
+ )
16
+
17
+
18
+ def _reject_unacceptable_heights(
19
+ potential_heights: List[float], max_height: float
20
+ ) -> float:
21
+ """Reject any solutions to a polynomial equation that cannot be the height of a frustum."""
22
+ valid_heights: List[float] = []
23
+ for root in potential_heights:
24
+ # reject any heights that are negative or greater than the max height
25
+ if not iscomplex(root):
26
+ # take only the real component of the root and round to 4 decimal places
27
+ rounded_root = round(real(root), 4)
28
+ if (rounded_root <= max_height) and (rounded_root >= 0):
29
+ if not any([isclose(rounded_root, height) for height in valid_heights]):
30
+ valid_heights.append(rounded_root)
31
+ if len(valid_heights) != 1:
32
+ raise InvalidLiquidHeightFound(
33
+ message="Unable to estimate valid liquid height from volume."
34
+ )
35
+ return valid_heights[0]
36
+
37
+
38
+ def _cross_section_area_circular(diameter: float) -> float:
39
+ """Get the area of a circular cross-section."""
40
+ radius = diameter / 2
41
+ return pi * (radius**2)
42
+
43
+
44
+ def _cross_section_area_rectangular(x_dimension: float, y_dimension: float) -> float:
45
+ """Get the area of a rectangular cross-section."""
46
+ return x_dimension * y_dimension
47
+
48
+
49
+ def _rectangular_frustum_polynomial_roots(
50
+ bottom_length: float,
51
+ bottom_width: float,
52
+ top_length: float,
53
+ top_width: float,
54
+ total_frustum_height: float,
55
+ ) -> Tuple[float, float, float]:
56
+ """Polynomial representation of the volume of a rectangular frustum."""
57
+ # roots of the polynomial with shape ax^3 + bx^2 + cx
58
+ a = (
59
+ (top_length - bottom_length)
60
+ * (top_width - bottom_width)
61
+ / (3 * total_frustum_height**2)
62
+ )
63
+ b = (
64
+ (bottom_length * (top_width - bottom_width))
65
+ + (bottom_width * (top_length - bottom_length))
66
+ ) / (2 * total_frustum_height)
67
+ c = bottom_length * bottom_width
68
+ return a, b, c
69
+
70
+
71
+ def _circular_frustum_polynomial_roots(
72
+ bottom_radius: float,
73
+ top_radius: float,
74
+ total_frustum_height: float,
75
+ ) -> Tuple[float, float, float]:
76
+ """Polynomial representation of the volume of a circular frustum."""
77
+ # roots of the polynomial with shape ax^3 + bx^2 + cx
78
+ a = pi * ((top_radius - bottom_radius) ** 2) / (3 * total_frustum_height**2)
79
+ b = pi * bottom_radius * (top_radius - bottom_radius) / total_frustum_height
80
+ c = pi * bottom_radius**2
81
+ return a, b, c
82
+
83
+
84
+ def _volume_from_height_circular(
85
+ target_height: float,
86
+ total_frustum_height: float,
87
+ bottom_radius: float,
88
+ top_radius: float,
89
+ ) -> float:
90
+ """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
98
+
99
+
100
+ def _volume_from_height_rectangular(
101
+ target_height: float,
102
+ total_frustum_height: float,
103
+ bottom_length: float,
104
+ bottom_width: float,
105
+ top_length: float,
106
+ top_width: float,
107
+ ) -> float:
108
+ """Find the volume given a height within a rectangular frustum."""
109
+ a, b, c = _rectangular_frustum_polynomial_roots(
110
+ bottom_length=bottom_length,
111
+ bottom_width=bottom_width,
112
+ top_length=top_length,
113
+ top_width=top_width,
114
+ total_frustum_height=total_frustum_height,
115
+ )
116
+ volume = a * (target_height**3) + b * (target_height**2) + c * target_height
117
+ return volume
118
+
119
+
120
+ def _volume_from_height_spherical(
121
+ target_height: float,
122
+ radius_of_curvature: float,
123
+ ) -> float:
124
+ """Find the volume given a height within a spherical frustum."""
125
+ volume = (
126
+ (1 / 3) * pi * (target_height**2) * (3 * radius_of_curvature - target_height)
127
+ )
128
+ return volume
129
+
130
+
131
+ def _volume_from_height_squared_cone(
132
+ target_height: float, segment: SquaredConeSegment
133
+ ) -> float:
134
+ """Find the volume given a height within a squared cone segment."""
135
+ heights = segment.height_to_volume_table.keys()
136
+ best_fit_height = min(heights, key=lambda x: abs(x - target_height))
137
+ return segment.height_to_volume_table[best_fit_height]
138
+
139
+
140
+ def _height_from_volume_circular(
141
+ volume: float,
142
+ total_frustum_height: float,
143
+ bottom_radius: float,
144
+ top_radius: float,
145
+ ) -> 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
161
+
162
+
163
+ def _height_from_volume_rectangular(
164
+ volume: float,
165
+ total_frustum_height: float,
166
+ bottom_length: float,
167
+ bottom_width: float,
168
+ top_length: float,
169
+ top_width: float,
170
+ ) -> float:
171
+ """Find the height given a volume within a rectangular frustum."""
172
+ a, b, c = _rectangular_frustum_polynomial_roots(
173
+ bottom_length=bottom_length,
174
+ bottom_width=bottom_width,
175
+ top_length=top_length,
176
+ top_width=top_width,
177
+ total_frustum_height=total_frustum_height,
178
+ )
179
+ d = volume * -1
180
+ x_intercept_roots = (a, b, c, d)
181
+
182
+ height_from_volume_roots = roots(x_intercept_roots)
183
+ height = _reject_unacceptable_heights(
184
+ potential_heights=list(height_from_volume_roots),
185
+ max_height=total_frustum_height,
186
+ )
187
+ return height
188
+
189
+
190
+ def _height_from_volume_spherical(
191
+ volume: float,
192
+ radius_of_curvature: float,
193
+ total_frustum_height: float,
194
+ ) -> float:
195
+ """Find the height given a volume within a spherical frustum."""
196
+ a = -1 * pi / 3
197
+ b = pi * radius_of_curvature
198
+ c = 0.0
199
+ d = volume * -1
200
+ x_intercept_roots = (a, b, c, d)
201
+
202
+ height_from_volume_roots = roots(x_intercept_roots)
203
+ height = _reject_unacceptable_heights(
204
+ potential_heights=list(height_from_volume_roots),
205
+ max_height=total_frustum_height,
206
+ )
207
+ return height
208
+
209
+
210
+ def _height_from_volume_squared_cone(
211
+ target_volume: float, segment: SquaredConeSegment
212
+ ) -> float:
213
+ """Find the height given a volume within a squared cone segment."""
214
+ volumes = segment.volume_to_height_table.keys()
215
+ best_fit_volume = min(volumes, key=lambda x: abs(x - target_volume))
216
+ return segment.volume_to_height_table[best_fit_volume]
217
+
218
+
219
+ def _get_segment_capacity(segment: WellSegment) -> float:
220
+ section_height = segment.topHeight - segment.bottomHeight
221
+ match segment:
222
+ case SphericalSegment():
223
+ return _volume_from_height_spherical(
224
+ target_height=segment.topHeight,
225
+ radius_of_curvature=segment.radiusOfCurvature,
226
+ )
227
+ 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,
235
+ )
236
+ 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),
242
+ )
243
+ case SquaredConeSegment():
244
+ return _volume_from_height_squared_cone(section_height, segment)
245
+ case _:
246
+ # TODO: implement volume calculations for truncated circular and rounded rectangular segments
247
+ raise NotImplementedError(
248
+ f"volume calculation for shape: {segment.shape} not yet implemented."
249
+ )
250
+
251
+
252
+ def get_well_volumetric_capacity(
253
+ well_geometry: InnerWellGeometry,
254
+ ) -> List[Tuple[float, float]]:
255
+ """Return the total volumetric capacity of a well as a map of height borders to volume."""
256
+ # dictionary map of heights to volumetric capacities within their respective segment
257
+ # {top_height_0: volume_0, top_height_1: volume_1, top_height_2: volume_2}
258
+ well_volume = []
259
+
260
+ # get the well segments sorted in ascending order
261
+ sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight)
262
+
263
+ for segment in sorted_well:
264
+ section_volume = _get_segment_capacity(segment)
265
+ well_volume.append((segment.topHeight, section_volume))
266
+ return well_volume
267
+
268
+
269
+ def height_at_volume_within_section(
270
+ section: WellSegment,
271
+ target_volume_relative: float,
272
+ section_height: float,
273
+ ) -> float:
274
+ """Calculate a height within a bounded section according to geometry."""
275
+ match section:
276
+ case SphericalSegment():
277
+ return _height_from_volume_spherical(
278
+ volume=target_volume_relative,
279
+ total_frustum_height=section_height,
280
+ radius_of_curvature=section.radiusOfCurvature,
281
+ )
282
+ 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
+ )
289
+ case CuboidalFrustum():
290
+ return _height_from_volume_rectangular(
291
+ volume=target_volume_relative,
292
+ total_frustum_height=section_height,
293
+ bottom_width=section.bottomXDimension,
294
+ bottom_length=section.bottomYDimension,
295
+ top_width=section.topXDimension,
296
+ top_length=section.topYDimension,
297
+ )
298
+ case SquaredConeSegment():
299
+ return _height_from_volume_squared_cone(target_volume_relative, section)
300
+ case _:
301
+ raise NotImplementedError(
302
+ "Height from volume calculation not yet implemented for this well shape."
303
+ )
304
+
305
+
306
+ def volume_at_height_within_section(
307
+ section: WellSegment,
308
+ target_height_relative: float,
309
+ section_height: float,
310
+ ) -> float:
311
+ """Calculate a volume within a bounded section according to geometry."""
312
+ match section:
313
+ case SphericalSegment():
314
+ return _volume_from_height_spherical(
315
+ target_height=target_height_relative,
316
+ radius_of_curvature=section.radiusOfCurvature,
317
+ )
318
+ 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),
324
+ )
325
+ 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,
333
+ )
334
+ case SquaredConeSegment():
335
+ return _volume_from_height_squared_cone(target_height_relative, section)
336
+ case _:
337
+ # TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712
338
+ # we need to input the math attached to that issue
339
+ raise NotImplementedError(
340
+ "Height from volume calculation not yet implemented for this well shape."
341
+ )
342
+
343
+
344
+ def _find_volume_in_partial_frustum(
345
+ sorted_well: List[WellSegment],
346
+ target_height: float,
347
+ ) -> float:
348
+ """Look through a sorted list of frusta for a target height, and find the volume at that height."""
349
+ for segment in sorted_well:
350
+ if segment.bottomHeight < target_height < segment.topHeight:
351
+ relative_target_height = target_height - segment.bottomHeight
352
+ section_height = segment.topHeight - segment.bottomHeight
353
+ return volume_at_height_within_section(
354
+ section=segment,
355
+ target_height_relative=relative_target_height,
356
+ section_height=section_height,
357
+ )
358
+ # if we've looked through all sections and can't find the target volume, raise an error
359
+ raise InvalidLiquidHeightFound(
360
+ f"Unable to find volume at given well-height {target_height}."
361
+ )
362
+
363
+
364
+ def find_volume_at_well_height(
365
+ target_height: float, well_geometry: InnerWellGeometry
366
+ ) -> float:
367
+ """Find the volume within a well, at a known height."""
368
+ volumetric_capacity = get_well_volumetric_capacity(well_geometry)
369
+ max_height = volumetric_capacity[-1][0]
370
+ if target_height < 0 or target_height > max_height:
371
+ raise InvalidLiquidHeightFound("Invalid target height.")
372
+ # volumes in volumetric_capacity are relative to each frustum,
373
+ # so we have to find the volume of all the full sections enclosed
374
+ # beneath the target height
375
+ closed_section_volume = 0.0
376
+ for boundary_height, section_volume in volumetric_capacity:
377
+ if boundary_height > target_height:
378
+ break
379
+ closed_section_volume += section_volume
380
+ # if target height is a boundary cross-section, we already know the volume
381
+ if target_height == boundary_height:
382
+ return closed_section_volume
383
+ # find the section the target height is in and compute the volume
384
+
385
+ sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight)
386
+ partial_volume = _find_volume_in_partial_frustum(
387
+ sorted_well=sorted_well,
388
+ target_height=target_height,
389
+ )
390
+ return partial_volume + closed_section_volume
391
+
392
+
393
+ def _find_height_in_partial_frustum(
394
+ sorted_well: List[WellSegment],
395
+ volumetric_capacity: List[Tuple[float, float]],
396
+ target_volume: float,
397
+ ) -> float:
398
+ """Look through a sorted list of frusta for a target volume, and find the height at that volume."""
399
+ bottom_section_volume = 0.0
400
+ for section, capacity in zip(sorted_well, volumetric_capacity):
401
+ section_top_height, section_volume = capacity
402
+ if (
403
+ bottom_section_volume
404
+ < target_volume
405
+ < (bottom_section_volume + section_volume)
406
+ ):
407
+ relative_target_volume = target_volume - bottom_section_volume
408
+ section_height = section.topHeight - section.bottomHeight
409
+ partial_height = height_at_volume_within_section(
410
+ section=section,
411
+ target_volume_relative=relative_target_volume,
412
+ section_height=section_height,
413
+ )
414
+ return partial_height + section.bottomHeight
415
+ # bottom section volume should always be the volume enclosed in the previously
416
+ # viewed section
417
+ bottom_section_volume += section_volume
418
+
419
+ # if we've looked through all sections and can't find the target volume, raise an error
420
+ raise InvalidLiquidHeightFound(
421
+ f"Unable to find height at given volume {target_volume}."
422
+ )
423
+
424
+
425
+ def find_height_at_well_volume(
426
+ target_volume: float, well_geometry: InnerWellGeometry
427
+ ) -> float:
428
+ """Find the height within a well, at a known volume."""
429
+ volumetric_capacity = get_well_volumetric_capacity(well_geometry)
430
+ max_volume = sum(row[1] for row in volumetric_capacity)
431
+ if target_volume < 0 or target_volume > max_volume:
432
+ raise InvalidLiquidHeightFound("Invalid target volume.")
433
+
434
+ sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight)
435
+ # find the section the target volume is in and compute the height
436
+ return _find_height_in_partial_frustum(
437
+ sorted_well=sorted_well,
438
+ volumetric_capacity=volumetric_capacity,
439
+ target_volume=target_volume,
440
+ )