opentrons 8.2.0a4__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.
- opentrons/calibration_storage/deck_configuration.py +3 -3
- opentrons/calibration_storage/file_operators.py +3 -3
- opentrons/calibration_storage/helpers.py +3 -1
- opentrons/calibration_storage/ot2/models/v1.py +16 -29
- opentrons/calibration_storage/ot2/tip_length.py +7 -4
- opentrons/calibration_storage/ot3/models/v1.py +14 -23
- opentrons/cli/analyze.py +18 -6
- opentrons/config/defaults_ot3.py +1 -0
- opentrons/drivers/asyncio/communication/__init__.py +2 -0
- opentrons/drivers/asyncio/communication/errors.py +16 -3
- opentrons/drivers/asyncio/communication/serial_connection.py +24 -9
- opentrons/drivers/command_builder.py +2 -2
- opentrons/drivers/flex_stacker/__init__.py +9 -0
- opentrons/drivers/flex_stacker/abstract.py +89 -0
- opentrons/drivers/flex_stacker/driver.py +260 -0
- opentrons/drivers/flex_stacker/simulator.py +109 -0
- opentrons/drivers/flex_stacker/types.py +138 -0
- opentrons/drivers/heater_shaker/driver.py +18 -3
- opentrons/drivers/temp_deck/driver.py +13 -3
- opentrons/drivers/thermocycler/driver.py +17 -3
- opentrons/execute.py +3 -1
- opentrons/hardware_control/__init__.py +1 -2
- opentrons/hardware_control/api.py +33 -21
- opentrons/hardware_control/backends/flex_protocol.py +17 -7
- opentrons/hardware_control/backends/ot3controller.py +213 -63
- opentrons/hardware_control/backends/ot3simulator.py +18 -9
- opentrons/hardware_control/backends/ot3utils.py +43 -15
- opentrons/hardware_control/dev_types.py +4 -0
- opentrons/hardware_control/emulation/heater_shaker.py +4 -0
- opentrons/hardware_control/emulation/module_server/client.py +1 -1
- opentrons/hardware_control/emulation/module_server/server.py +5 -3
- opentrons/hardware_control/emulation/settings.py +3 -4
- opentrons/hardware_control/instruments/ot2/instrument_calibration.py +2 -1
- opentrons/hardware_control/instruments/ot2/pipette.py +15 -22
- opentrons/hardware_control/instruments/ot2/pipette_handler.py +8 -1
- opentrons/hardware_control/instruments/ot3/gripper.py +2 -2
- opentrons/hardware_control/instruments/ot3/pipette.py +23 -22
- opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -1
- opentrons/hardware_control/modules/mod_abc.py +2 -2
- opentrons/hardware_control/motion_utilities.py +68 -0
- opentrons/hardware_control/nozzle_manager.py +39 -41
- opentrons/hardware_control/ot3_calibration.py +1 -1
- opentrons/hardware_control/ot3api.py +78 -31
- opentrons/hardware_control/protocols/gripper_controller.py +3 -0
- opentrons/hardware_control/protocols/hardware_manager.py +5 -1
- opentrons/hardware_control/protocols/liquid_handler.py +22 -1
- opentrons/hardware_control/protocols/motion_controller.py +7 -0
- opentrons/hardware_control/robot_calibration.py +1 -1
- opentrons/hardware_control/types.py +61 -0
- opentrons/legacy_commands/commands.py +37 -0
- opentrons/legacy_commands/types.py +39 -0
- opentrons/protocol_api/__init__.py +20 -1
- opentrons/protocol_api/_liquid.py +24 -49
- opentrons/protocol_api/_liquid_properties.py +754 -0
- opentrons/protocol_api/_types.py +24 -0
- opentrons/protocol_api/core/common.py +2 -0
- opentrons/protocol_api/core/engine/instrument.py +191 -10
- opentrons/protocol_api/core/engine/labware.py +29 -7
- opentrons/protocol_api/core/engine/protocol.py +130 -5
- opentrons/protocol_api/core/engine/robot.py +139 -0
- opentrons/protocol_api/core/engine/well.py +4 -1
- opentrons/protocol_api/core/instrument.py +73 -4
- opentrons/protocol_api/core/labware.py +13 -4
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +87 -3
- opentrons/protocol_api/core/legacy/legacy_labware_core.py +13 -4
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +32 -1
- opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +61 -3
- opentrons/protocol_api/core/protocol.py +34 -1
- opentrons/protocol_api/core/robot.py +51 -0
- opentrons/protocol_api/instrument_context.py +299 -44
- opentrons/protocol_api/labware.py +248 -9
- opentrons/protocol_api/module_contexts.py +21 -17
- opentrons/protocol_api/protocol_context.py +125 -4
- opentrons/protocol_api/robot_context.py +204 -32
- opentrons/protocol_api/validation.py +262 -3
- opentrons/protocol_engine/__init__.py +4 -0
- opentrons/protocol_engine/actions/actions.py +2 -3
- opentrons/protocol_engine/clients/sync_client.py +18 -0
- opentrons/protocol_engine/commands/__init__.py +121 -0
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +1 -3
- opentrons/protocol_engine/commands/absorbance_reader/initialize.py +20 -6
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +1 -2
- opentrons/protocol_engine/commands/absorbance_reader/read.py +36 -10
- opentrons/protocol_engine/commands/air_gap_in_place.py +160 -0
- opentrons/protocol_engine/commands/aspirate.py +103 -53
- opentrons/protocol_engine/commands/aspirate_in_place.py +55 -51
- opentrons/protocol_engine/commands/blow_out.py +44 -39
- opentrons/protocol_engine/commands/blow_out_in_place.py +21 -32
- opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +13 -6
- opentrons/protocol_engine/commands/calibration/calibrate_module.py +1 -1
- opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +3 -3
- opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +1 -1
- opentrons/protocol_engine/commands/command.py +73 -66
- opentrons/protocol_engine/commands/command_unions.py +140 -1
- opentrons/protocol_engine/commands/comment.py +1 -1
- opentrons/protocol_engine/commands/configure_for_volume.py +10 -3
- opentrons/protocol_engine/commands/configure_nozzle_layout.py +6 -4
- opentrons/protocol_engine/commands/custom.py +6 -12
- opentrons/protocol_engine/commands/dispense.py +82 -48
- opentrons/protocol_engine/commands/dispense_in_place.py +71 -51
- opentrons/protocol_engine/commands/drop_tip.py +52 -31
- opentrons/protocol_engine/commands/drop_tip_in_place.py +79 -8
- opentrons/protocol_engine/commands/evotip_dispense.py +156 -0
- opentrons/protocol_engine/commands/evotip_seal_pipette.py +331 -0
- opentrons/protocol_engine/commands/evotip_unseal_pipette.py +160 -0
- opentrons/protocol_engine/commands/generate_command_schema.py +4 -11
- opentrons/protocol_engine/commands/get_next_tip.py +134 -0
- opentrons/protocol_engine/commands/get_tip_presence.py +1 -1
- opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +1 -1
- opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +1 -1
- opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +1 -1
- opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +1 -1
- opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +1 -1
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +1 -1
- opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +10 -4
- opentrons/protocol_engine/commands/home.py +13 -4
- opentrons/protocol_engine/commands/liquid_probe.py +125 -31
- opentrons/protocol_engine/commands/load_labware.py +33 -6
- opentrons/protocol_engine/commands/load_lid.py +146 -0
- opentrons/protocol_engine/commands/load_lid_stack.py +189 -0
- opentrons/protocol_engine/commands/load_liquid.py +12 -4
- opentrons/protocol_engine/commands/load_liquid_class.py +144 -0
- opentrons/protocol_engine/commands/load_module.py +31 -10
- opentrons/protocol_engine/commands/load_pipette.py +19 -8
- opentrons/protocol_engine/commands/magnetic_module/disengage.py +1 -1
- opentrons/protocol_engine/commands/magnetic_module/engage.py +1 -1
- opentrons/protocol_engine/commands/move_labware.py +28 -6
- opentrons/protocol_engine/commands/move_relative.py +35 -25
- opentrons/protocol_engine/commands/move_to_addressable_area.py +40 -27
- opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +53 -32
- opentrons/protocol_engine/commands/move_to_coordinates.py +36 -22
- opentrons/protocol_engine/commands/move_to_well.py +40 -24
- opentrons/protocol_engine/commands/movement_common.py +338 -0
- opentrons/protocol_engine/commands/pick_up_tip.py +49 -27
- opentrons/protocol_engine/commands/pipetting_common.py +169 -87
- opentrons/protocol_engine/commands/prepare_to_aspirate.py +24 -33
- opentrons/protocol_engine/commands/reload_labware.py +1 -1
- opentrons/protocol_engine/commands/retract_axis.py +1 -1
- opentrons/protocol_engine/commands/robot/__init__.py +69 -0
- opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +86 -0
- opentrons/protocol_engine/commands/robot/common.py +18 -0
- opentrons/protocol_engine/commands/robot/move_axes_relative.py +101 -0
- opentrons/protocol_engine/commands/robot/move_axes_to.py +100 -0
- opentrons/protocol_engine/commands/robot/move_to.py +94 -0
- opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +77 -0
- opentrons/protocol_engine/commands/save_position.py +14 -5
- opentrons/protocol_engine/commands/set_rail_lights.py +1 -1
- opentrons/protocol_engine/commands/set_status_bar.py +1 -1
- opentrons/protocol_engine/commands/temperature_module/deactivate.py +1 -1
- opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +1 -1
- opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +10 -4
- opentrons/protocol_engine/commands/thermocycler/close_lid.py +1 -1
- opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +1 -1
- opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +1 -1
- opentrons/protocol_engine/commands/thermocycler/open_lid.py +1 -1
- opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +9 -3
- opentrons/protocol_engine/commands/thermocycler/run_profile.py +9 -3
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +11 -4
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +1 -1
- opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +1 -1
- opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +1 -1
- opentrons/protocol_engine/commands/touch_tip.py +65 -16
- opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +5 -2
- opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +13 -4
- opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +2 -5
- opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +1 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +1 -1
- opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +2 -5
- opentrons/protocol_engine/commands/verify_tip_presence.py +11 -4
- opentrons/protocol_engine/commands/wait_for_duration.py +10 -3
- opentrons/protocol_engine/commands/wait_for_resume.py +10 -3
- opentrons/protocol_engine/errors/__init__.py +12 -0
- opentrons/protocol_engine/errors/error_occurrence.py +19 -20
- opentrons/protocol_engine/errors/exceptions.py +76 -0
- opentrons/protocol_engine/execution/command_executor.py +1 -1
- opentrons/protocol_engine/execution/equipment.py +73 -5
- opentrons/protocol_engine/execution/gantry_mover.py +369 -8
- opentrons/protocol_engine/execution/hardware_stopper.py +7 -7
- opentrons/protocol_engine/execution/movement.py +27 -0
- opentrons/protocol_engine/execution/pipetting.py +5 -1
- opentrons/protocol_engine/execution/tip_handler.py +34 -15
- opentrons/protocol_engine/notes/notes.py +1 -1
- opentrons/protocol_engine/protocol_engine.py +7 -6
- opentrons/protocol_engine/resources/labware_data_provider.py +1 -1
- opentrons/protocol_engine/resources/labware_validation.py +18 -0
- opentrons/protocol_engine/resources/module_data_provider.py +1 -1
- opentrons/protocol_engine/resources/pipette_data_provider.py +26 -0
- opentrons/protocol_engine/slot_standardization.py +9 -9
- opentrons/protocol_engine/state/_move_types.py +9 -5
- opentrons/protocol_engine/state/_well_math.py +193 -0
- opentrons/protocol_engine/state/addressable_areas.py +25 -61
- opentrons/protocol_engine/state/command_history.py +12 -0
- opentrons/protocol_engine/state/commands.py +22 -14
- opentrons/protocol_engine/state/files.py +10 -12
- opentrons/protocol_engine/state/fluid_stack.py +138 -0
- opentrons/protocol_engine/state/frustum_helpers.py +63 -69
- opentrons/protocol_engine/state/geometry.py +47 -1
- opentrons/protocol_engine/state/labware.py +92 -26
- opentrons/protocol_engine/state/liquid_classes.py +82 -0
- opentrons/protocol_engine/state/liquids.py +16 -4
- opentrons/protocol_engine/state/modules.py +52 -70
- opentrons/protocol_engine/state/motion.py +6 -1
- opentrons/protocol_engine/state/pipettes.py +149 -58
- opentrons/protocol_engine/state/state.py +21 -2
- opentrons/protocol_engine/state/state_summary.py +4 -2
- opentrons/protocol_engine/state/tips.py +11 -44
- opentrons/protocol_engine/state/update_types.py +343 -48
- opentrons/protocol_engine/state/wells.py +19 -11
- opentrons/protocol_engine/types.py +176 -28
- opentrons/protocol_reader/extract_labware_definitions.py +5 -2
- opentrons/protocol_reader/file_format_validator.py +5 -5
- opentrons/protocol_runner/json_file_reader.py +9 -3
- opentrons/protocol_runner/json_translator.py +51 -25
- opentrons/protocol_runner/legacy_command_mapper.py +66 -64
- opentrons/protocol_runner/protocol_runner.py +35 -4
- opentrons/protocol_runner/python_protocol_wrappers.py +1 -1
- opentrons/protocol_runner/run_orchestrator.py +13 -3
- opentrons/protocols/advanced_control/common.py +38 -0
- opentrons/protocols/advanced_control/mix.py +1 -1
- opentrons/protocols/advanced_control/transfers/__init__.py +0 -0
- opentrons/protocols/advanced_control/transfers/common.py +56 -0
- opentrons/protocols/advanced_control/{transfers.py → transfers/transfer.py} +10 -85
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/instrument.py +1 -1
- opentrons/protocols/api_support/util.py +10 -0
- opentrons/protocols/labware.py +70 -8
- opentrons/protocols/models/json_protocol.py +5 -9
- opentrons/simulate.py +3 -1
- opentrons/types.py +162 -2
- opentrons/util/entrypoint_util.py +2 -5
- opentrons/util/logging_config.py +1 -1
- {opentrons-8.2.0a4.dist-info → opentrons-8.3.0.dist-info}/METADATA +16 -15
- {opentrons-8.2.0a4.dist-info → opentrons-8.3.0.dist-info}/RECORD +238 -208
- {opentrons-8.2.0a4.dist-info → opentrons-8.3.0.dist-info}/WHEEL +1 -1
- {opentrons-8.2.0a4.dist-info → opentrons-8.3.0.dist-info}/LICENSE +0 -0
- {opentrons-8.2.0a4.dist-info → opentrons-8.3.0.dist-info}/entry_points.txt +0 -0
- {opentrons-8.2.0a4.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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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=
|
|
1002
|
-
|
|
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=
|
|
1005
|
-
|
|
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
|
|
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:
|
|
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
|