opentrons 8.1.0a0__py2.py3-none-any.whl → 8.2.0a0__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.
- opentrons/cli/analyze.py +71 -7
- opentrons/config/__init__.py +9 -0
- opentrons/config/advanced_settings.py +22 -0
- opentrons/config/defaults_ot3.py +14 -36
- opentrons/config/feature_flags.py +4 -0
- opentrons/config/types.py +6 -17
- opentrons/drivers/absorbance_reader/abstract.py +27 -3
- opentrons/drivers/absorbance_reader/async_byonoy.py +207 -154
- opentrons/drivers/absorbance_reader/driver.py +24 -15
- opentrons/drivers/absorbance_reader/hid_protocol.py +79 -50
- opentrons/drivers/absorbance_reader/simulator.py +32 -6
- opentrons/drivers/types.py +23 -1
- opentrons/execute.py +2 -2
- opentrons/hardware_control/api.py +18 -10
- opentrons/hardware_control/backends/controller.py +3 -2
- opentrons/hardware_control/backends/flex_protocol.py +11 -5
- opentrons/hardware_control/backends/ot3controller.py +18 -50
- opentrons/hardware_control/backends/ot3simulator.py +7 -6
- opentrons/hardware_control/instruments/ot2/pipette_handler.py +22 -82
- opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -2
- opentrons/hardware_control/module_control.py +43 -2
- opentrons/hardware_control/modules/__init__.py +7 -1
- opentrons/hardware_control/modules/absorbance_reader.py +230 -83
- opentrons/hardware_control/modules/errors.py +7 -0
- opentrons/hardware_control/modules/heater_shaker.py +8 -3
- opentrons/hardware_control/modules/magdeck.py +12 -3
- opentrons/hardware_control/modules/mod_abc.py +27 -2
- opentrons/hardware_control/modules/tempdeck.py +15 -7
- opentrons/hardware_control/modules/thermocycler.py +69 -3
- opentrons/hardware_control/modules/types.py +11 -5
- opentrons/hardware_control/modules/update.py +11 -5
- opentrons/hardware_control/modules/utils.py +3 -1
- opentrons/hardware_control/ot3_calibration.py +6 -6
- opentrons/hardware_control/ot3api.py +126 -89
- opentrons/hardware_control/poller.py +15 -11
- opentrons/hardware_control/protocols/__init__.py +1 -7
- opentrons/hardware_control/protocols/instrument_configurer.py +14 -2
- opentrons/hardware_control/protocols/liquid_handler.py +5 -0
- opentrons/motion_planning/__init__.py +2 -0
- opentrons/motion_planning/waypoints.py +32 -0
- opentrons/protocol_api/__init__.py +2 -1
- opentrons/protocol_api/_liquid.py +87 -1
- opentrons/protocol_api/_parameter_context.py +10 -1
- opentrons/protocol_api/core/engine/deck_conflict.py +0 -297
- opentrons/protocol_api/core/engine/instrument.py +29 -25
- opentrons/protocol_api/core/engine/labware.py +10 -2
- opentrons/protocol_api/core/engine/module_core.py +129 -17
- opentrons/protocol_api/core/engine/pipette_movement_conflict.py +355 -0
- opentrons/protocol_api/core/engine/protocol.py +55 -2
- opentrons/protocol_api/core/instrument.py +2 -0
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -0
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +5 -2
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -0
- opentrons/protocol_api/core/module.py +22 -4
- opentrons/protocol_api/core/protocol.py +5 -2
- opentrons/protocol_api/instrument_context.py +52 -20
- opentrons/protocol_api/labware.py +13 -1
- opentrons/protocol_api/module_contexts.py +68 -13
- opentrons/protocol_api/protocol_context.py +38 -4
- opentrons/protocol_api/validation.py +5 -3
- opentrons/protocol_engine/__init__.py +10 -9
- opentrons/protocol_engine/actions/__init__.py +5 -0
- opentrons/protocol_engine/actions/actions.py +42 -25
- opentrons/protocol_engine/actions/get_state_update.py +38 -0
- opentrons/protocol_engine/clients/sync_client.py +7 -1
- opentrons/protocol_engine/clients/transports.py +1 -1
- opentrons/protocol_engine/commands/__init__.py +0 -4
- opentrons/protocol_engine/commands/absorbance_reader/__init__.py +41 -11
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +161 -0
- opentrons/protocol_engine/commands/absorbance_reader/initialize.py +53 -9
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +160 -0
- opentrons/protocol_engine/commands/absorbance_reader/read.py +196 -0
- opentrons/protocol_engine/commands/aspirate.py +29 -16
- opentrons/protocol_engine/commands/aspirate_in_place.py +32 -15
- opentrons/protocol_engine/commands/blow_out.py +63 -14
- opentrons/protocol_engine/commands/blow_out_in_place.py +55 -13
- opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +2 -5
- opentrons/protocol_engine/commands/calibration/calibrate_module.py +3 -4
- opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +2 -5
- opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +6 -4
- opentrons/protocol_engine/commands/command.py +28 -17
- opentrons/protocol_engine/commands/command_unions.py +37 -24
- opentrons/protocol_engine/commands/comment.py +5 -3
- opentrons/protocol_engine/commands/configure_for_volume.py +11 -14
- opentrons/protocol_engine/commands/configure_nozzle_layout.py +9 -15
- opentrons/protocol_engine/commands/custom.py +5 -3
- opentrons/protocol_engine/commands/dispense.py +42 -20
- opentrons/protocol_engine/commands/dispense_in_place.py +32 -14
- opentrons/protocol_engine/commands/drop_tip.py +68 -15
- opentrons/protocol_engine/commands/drop_tip_in_place.py +52 -11
- opentrons/protocol_engine/commands/get_tip_presence.py +5 -3
- opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +6 -6
- opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +6 -6
- opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +6 -6
- opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +8 -6
- opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +8 -4
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +6 -4
- opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +6 -6
- opentrons/protocol_engine/commands/home.py +11 -5
- opentrons/protocol_engine/commands/liquid_probe.py +146 -88
- opentrons/protocol_engine/commands/load_labware.py +19 -5
- opentrons/protocol_engine/commands/load_liquid.py +18 -7
- opentrons/protocol_engine/commands/load_module.py +43 -6
- opentrons/protocol_engine/commands/load_pipette.py +18 -17
- opentrons/protocol_engine/commands/magnetic_module/disengage.py +6 -6
- opentrons/protocol_engine/commands/magnetic_module/engage.py +6 -4
- opentrons/protocol_engine/commands/move_labware.py +106 -19
- opentrons/protocol_engine/commands/move_relative.py +15 -3
- opentrons/protocol_engine/commands/move_to_addressable_area.py +29 -4
- opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +13 -4
- opentrons/protocol_engine/commands/move_to_coordinates.py +11 -5
- opentrons/protocol_engine/commands/move_to_well.py +37 -10
- opentrons/protocol_engine/commands/pick_up_tip.py +50 -29
- opentrons/protocol_engine/commands/pipetting_common.py +39 -15
- opentrons/protocol_engine/commands/prepare_to_aspirate.py +62 -15
- opentrons/protocol_engine/commands/reload_labware.py +13 -4
- opentrons/protocol_engine/commands/retract_axis.py +6 -3
- opentrons/protocol_engine/commands/save_position.py +2 -3
- opentrons/protocol_engine/commands/set_rail_lights.py +5 -3
- opentrons/protocol_engine/commands/set_status_bar.py +5 -3
- opentrons/protocol_engine/commands/temperature_module/deactivate.py +6 -4
- opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +3 -4
- opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +6 -6
- opentrons/protocol_engine/commands/thermocycler/__init__.py +19 -0
- opentrons/protocol_engine/commands/thermocycler/close_lid.py +8 -8
- opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +6 -4
- opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +6 -4
- opentrons/protocol_engine/commands/thermocycler/open_lid.py +8 -4
- opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +165 -0
- opentrons/protocol_engine/commands/thermocycler/run_profile.py +6 -6
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +3 -4
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +3 -4
- opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +6 -4
- opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +6 -4
- opentrons/protocol_engine/commands/touch_tip.py +19 -7
- opentrons/protocol_engine/commands/unsafe/__init__.py +30 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +6 -4
- opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -4
- opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +5 -3
- opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +194 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +75 -0
- opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +5 -3
- opentrons/protocol_engine/commands/verify_tip_presence.py +5 -5
- opentrons/protocol_engine/commands/wait_for_duration.py +5 -3
- opentrons/protocol_engine/commands/wait_for_resume.py +5 -3
- opentrons/protocol_engine/create_protocol_engine.py +41 -8
- opentrons/protocol_engine/engine_support.py +2 -1
- opentrons/protocol_engine/error_recovery_policy.py +14 -3
- opentrons/protocol_engine/errors/__init__.py +18 -0
- opentrons/protocol_engine/errors/exceptions.py +114 -2
- opentrons/protocol_engine/execution/__init__.py +2 -0
- opentrons/protocol_engine/execution/command_executor.py +22 -13
- opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
- opentrons/protocol_engine/execution/door_watcher.py +1 -1
- opentrons/protocol_engine/execution/equipment.py +2 -1
- opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
- opentrons/protocol_engine/execution/gantry_mover.py +4 -2
- opentrons/protocol_engine/execution/hardware_stopper.py +3 -3
- opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +1 -4
- opentrons/protocol_engine/execution/labware_movement.py +6 -3
- opentrons/protocol_engine/execution/movement.py +8 -3
- opentrons/protocol_engine/execution/pipetting.py +7 -4
- opentrons/protocol_engine/execution/queue_worker.py +6 -2
- opentrons/protocol_engine/execution/run_control.py +1 -1
- opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +1 -1
- opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +2 -1
- opentrons/protocol_engine/execution/tip_handler.py +77 -43
- opentrons/protocol_engine/notes/__init__.py +14 -2
- opentrons/protocol_engine/notes/notes.py +18 -1
- opentrons/protocol_engine/plugins.py +1 -1
- opentrons/protocol_engine/protocol_engine.py +54 -31
- opentrons/protocol_engine/resources/__init__.py +2 -0
- opentrons/protocol_engine/resources/deck_data_provider.py +58 -5
- opentrons/protocol_engine/resources/file_provider.py +157 -0
- opentrons/protocol_engine/resources/fixture_validation.py +5 -0
- opentrons/protocol_engine/resources/labware_validation.py +10 -0
- opentrons/protocol_engine/state/__init__.py +0 -70
- opentrons/protocol_engine/state/addressable_areas.py +1 -1
- opentrons/protocol_engine/state/command_history.py +21 -2
- opentrons/protocol_engine/state/commands.py +110 -31
- opentrons/protocol_engine/state/files.py +59 -0
- opentrons/protocol_engine/state/frustum_helpers.py +440 -0
- opentrons/protocol_engine/state/geometry.py +359 -15
- opentrons/protocol_engine/state/labware.py +166 -63
- opentrons/protocol_engine/state/liquids.py +1 -1
- opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +19 -3
- opentrons/protocol_engine/state/modules.py +167 -85
- opentrons/protocol_engine/state/motion.py +16 -9
- opentrons/protocol_engine/state/pipettes.py +157 -317
- opentrons/protocol_engine/state/state.py +30 -1
- opentrons/protocol_engine/state/state_summary.py +3 -0
- opentrons/protocol_engine/state/tips.py +69 -114
- opentrons/protocol_engine/state/update_types.py +408 -0
- opentrons/protocol_engine/state/wells.py +236 -0
- opentrons/protocol_engine/types.py +90 -0
- opentrons/protocol_reader/file_format_validator.py +83 -15
- opentrons/protocol_runner/json_translator.py +21 -5
- opentrons/protocol_runner/legacy_command_mapper.py +27 -6
- opentrons/protocol_runner/legacy_context_plugin.py +27 -71
- opentrons/protocol_runner/protocol_runner.py +6 -3
- opentrons/protocol_runner/run_orchestrator.py +26 -6
- opentrons/protocols/advanced_control/mix.py +3 -5
- opentrons/protocols/advanced_control/transfers.py +125 -56
- opentrons/protocols/api_support/constants.py +1 -1
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/labware_like.py +4 -4
- opentrons/protocols/api_support/tip_tracker.py +2 -2
- opentrons/protocols/api_support/types.py +15 -2
- opentrons/protocols/api_support/util.py +30 -42
- opentrons/protocols/duration/errors.py +1 -1
- opentrons/protocols/duration/estimator.py +50 -29
- opentrons/protocols/execution/dev_types.py +2 -2
- opentrons/protocols/execution/execute_json_v4.py +15 -10
- opentrons/protocols/execution/execute_python.py +8 -3
- opentrons/protocols/geometry/planning.py +12 -12
- opentrons/protocols/labware.py +17 -33
- opentrons/simulate.py +3 -3
- opentrons/types.py +30 -3
- opentrons/util/logging_config.py +34 -0
- {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/METADATA +5 -4
- {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/RECORD +227 -215
- opentrons/protocol_engine/commands/absorbance_reader/measure.py +0 -94
- opentrons/protocol_engine/commands/configuring_common.py +0 -26
- opentrons/protocol_runner/thread_async_queue.py +0 -174
- /opentrons/protocol_engine/state/{abstract_store.py → _abstract_store.py} +0 -0
- /opentrons/protocol_engine/state/{move_types.py → _move_types.py} +0 -0
- {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/LICENSE +0 -0
- {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/WHEEL +0 -0
- {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/entry_points.txt +0 -0
- {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.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
|
+
)
|