opentrons 8.3.0a0__py2.py3-none-any.whl → 8.3.0a1__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/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 +28 -20
- opentrons/hardware_control/backends/flex_protocol.py +4 -6
- opentrons/hardware_control/backends/ot3controller.py +177 -59
- opentrons/hardware_control/backends/ot3simulator.py +10 -8
- opentrons/hardware_control/backends/ot3utils.py +3 -13
- opentrons/hardware_control/dev_types.py +2 -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 +9 -21
- 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 +13 -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 +34 -22
- 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 +18 -0
- opentrons/hardware_control/protocols/motion_controller.py +6 -0
- opentrons/hardware_control/robot_calibration.py +1 -1
- opentrons/hardware_control/types.py +61 -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 +67 -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 +42 -4
- opentrons/protocol_api/core/labware.py +13 -4
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +34 -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 +34 -3
- opentrons/protocol_api/core/protocol.py +34 -1
- opentrons/protocol_api/core/robot.py +51 -0
- opentrons/protocol_api/instrument_context.py +145 -43
- opentrons/protocol_api/labware.py +231 -7
- 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 +261 -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 +81 -0
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +0 -2
- opentrons/protocol_engine/commands/absorbance_reader/initialize.py +19 -5
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +0 -1
- opentrons/protocol_engine/commands/absorbance_reader/read.py +32 -9
- 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 +101 -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 +13 -3
- 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 +60 -25
- opentrons/protocol_engine/commands/load_labware.py +29 -7
- 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 +19 -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 +8 -2
- 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 +4 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -3
- opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +1 -4
- opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +1 -4
- 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 +8 -0
- opentrons/protocol_engine/errors/error_occurrence.py +19 -20
- opentrons/protocol_engine/errors/exceptions.py +50 -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 +364 -8
- opentrons/protocol_engine/execution/movement.py +27 -0
- opentrons/protocol_engine/execution/pipetting.py +5 -1
- opentrons/protocol_engine/execution/tip_handler.py +4 -6
- 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 +5 -0
- opentrons/protocol_engine/resources/module_data_provider.py +1 -1
- opentrons/protocol_engine/resources/pipette_data_provider.py +12 -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 +17 -13
- opentrons/protocol_engine/state/files.py +10 -12
- opentrons/protocol_engine/state/fluid_stack.py +138 -0
- opentrons/protocol_engine/state/frustum_helpers.py +57 -32
- opentrons/protocol_engine/state/geometry.py +47 -1
- opentrons/protocol_engine/state/labware.py +79 -25
- 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 +135 -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 +39 -6
- opentrons/protocols/models/json_protocol.py +5 -9
- opentrons/simulate.py +3 -1
- opentrons/types.py +162 -2
- opentrons/util/logging_config.py +1 -1
- {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a1.dist-info}/METADATA +16 -15
- {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a1.dist-info}/RECORD +228 -201
- {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a1.dist-info}/WHEEL +1 -1
- {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a1.dist-info}/LICENSE +0 -0
- {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a1.dist-info}/entry_points.txt +0 -0
- {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a1.dist-info}/top_level.txt +0 -0
|
@@ -228,9 +228,6 @@ class CommandState:
|
|
|
228
228
|
This value can be used to generate future hashes.
|
|
229
229
|
"""
|
|
230
230
|
|
|
231
|
-
failed_command_errors: List[ErrorOccurrence]
|
|
232
|
-
"""List of command errors that occurred during run execution."""
|
|
233
|
-
|
|
234
231
|
has_entered_error_recovery: bool
|
|
235
232
|
"""Whether the run has entered error recovery."""
|
|
236
233
|
|
|
@@ -269,7 +266,6 @@ class CommandStore(HasState[CommandState], HandlesActions):
|
|
|
269
266
|
run_started_at=None,
|
|
270
267
|
latest_protocol_command_hash=None,
|
|
271
268
|
stopped_by_estop=False,
|
|
272
|
-
failed_command_errors=[],
|
|
273
269
|
error_recovery_policy=error_recovery_policy,
|
|
274
270
|
has_entered_error_recovery=False,
|
|
275
271
|
)
|
|
@@ -308,7 +304,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
|
|
|
308
304
|
# TODO(mc, 2021-06-22): mypy has trouble with this automatic
|
|
309
305
|
# request > command mapping, figure out how to type precisely
|
|
310
306
|
# (or wait for a future mypy version that can figure it out).
|
|
311
|
-
queued_command = action.request._CommandCls.
|
|
307
|
+
queued_command = action.request._CommandCls.model_construct( # type: ignore[call-arg]
|
|
312
308
|
id=action.command_id,
|
|
313
309
|
key=(
|
|
314
310
|
action.request.key
|
|
@@ -330,7 +326,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
|
|
|
330
326
|
def _handle_run_command_action(self, action: RunCommandAction) -> None:
|
|
331
327
|
prev_entry = self._state.command_history.get(action.command_id)
|
|
332
328
|
|
|
333
|
-
running_command = prev_entry.command.
|
|
329
|
+
running_command = prev_entry.command.model_copy(
|
|
334
330
|
update={
|
|
335
331
|
"status": CommandStatus.RUNNING,
|
|
336
332
|
"startedAt": action.started_at,
|
|
@@ -366,7 +362,6 @@ class CommandStore(HasState[CommandState], HandlesActions):
|
|
|
366
362
|
notes=action.notes,
|
|
367
363
|
)
|
|
368
364
|
self._state.failed_command = self._state.command_history.get(action.command_id)
|
|
369
|
-
self._state.failed_command_errors.append(public_error_occurrence)
|
|
370
365
|
|
|
371
366
|
if (
|
|
372
367
|
prev_entry.command.intent in (CommandIntent.PROTOCOL, None)
|
|
@@ -530,7 +525,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
|
|
|
530
525
|
notes: Optional[List[CommandNote]],
|
|
531
526
|
) -> None:
|
|
532
527
|
prev_entry = self._state.command_history.get(command_id)
|
|
533
|
-
failed_command = prev_entry.command.
|
|
528
|
+
failed_command = prev_entry.command.model_copy(
|
|
534
529
|
update={
|
|
535
530
|
"completedAt": failed_at,
|
|
536
531
|
"status": CommandStatus.FAILED,
|
|
@@ -584,7 +579,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
|
|
|
584
579
|
)
|
|
585
580
|
|
|
586
581
|
|
|
587
|
-
class CommandView
|
|
582
|
+
class CommandView:
|
|
588
583
|
"""Read-only command state view."""
|
|
589
584
|
|
|
590
585
|
_state: CommandState
|
|
@@ -684,7 +679,7 @@ class CommandView(HasState[CommandState]):
|
|
|
684
679
|
finish_error = self._state.finish_error
|
|
685
680
|
|
|
686
681
|
if run_error and finish_error:
|
|
687
|
-
combined_error = ErrorOccurrence
|
|
682
|
+
combined_error = ErrorOccurrence(
|
|
688
683
|
id=finish_error.id,
|
|
689
684
|
createdAt=finish_error.createdAt,
|
|
690
685
|
errorType="RunAndFinishFailed",
|
|
@@ -706,7 +701,12 @@ class CommandView(HasState[CommandState]):
|
|
|
706
701
|
|
|
707
702
|
def get_all_errors(self) -> List[ErrorOccurrence]:
|
|
708
703
|
"""Get the run's full error list, if there was none, returns an empty list."""
|
|
709
|
-
|
|
704
|
+
failed_commands = self._state.command_history.get_all_failed_commands()
|
|
705
|
+
return [
|
|
706
|
+
command_error.error
|
|
707
|
+
for command_error in failed_commands
|
|
708
|
+
if command_error.error is not None
|
|
709
|
+
]
|
|
710
710
|
|
|
711
711
|
def get_has_entered_recovery_mode(self) -> bool:
|
|
712
712
|
"""Get whether the run has entered recovery mode."""
|
|
@@ -916,7 +916,7 @@ class CommandView(HasState[CommandState]):
|
|
|
916
916
|
fatal error of the overall run coming from anywhere in the Python script,
|
|
917
917
|
including in between commands.
|
|
918
918
|
"""
|
|
919
|
-
failed_command = self.
|
|
919
|
+
failed_command = self._state.failed_command
|
|
920
920
|
if (
|
|
921
921
|
failed_command
|
|
922
922
|
and failed_command.command.error
|
|
@@ -932,12 +932,16 @@ class CommandView(HasState[CommandState]):
|
|
|
932
932
|
|
|
933
933
|
The command ID is assumed to point to a failed command.
|
|
934
934
|
"""
|
|
935
|
-
return self.
|
|
935
|
+
return self._state.command_error_recovery_types[command_id]
|
|
936
936
|
|
|
937
937
|
def get_is_stopped(self) -> bool:
|
|
938
938
|
"""Get whether an engine stop has completed."""
|
|
939
939
|
return self._state.run_completed_at is not None
|
|
940
940
|
|
|
941
|
+
def get_is_stopped_by_estop(self) -> bool:
|
|
942
|
+
"""Return whether the engine was stopped specifically by an E-stop."""
|
|
943
|
+
return self._state.stopped_by_estop
|
|
944
|
+
|
|
941
945
|
def has_been_played(self) -> bool:
|
|
942
946
|
"""Get whether engine has started."""
|
|
943
947
|
return self._state.run_started_at is not None
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from typing import List
|
|
4
4
|
|
|
5
|
+
from opentrons.protocol_engine.actions.get_state_update import get_state_updates
|
|
6
|
+
from opentrons.protocol_engine.state import update_types
|
|
7
|
+
|
|
5
8
|
from ._abstract_store import HasState, HandlesActions
|
|
6
|
-
from ..actions import Action
|
|
7
|
-
from ..commands import (
|
|
8
|
-
Command,
|
|
9
|
-
absorbance_reader,
|
|
10
|
-
)
|
|
9
|
+
from ..actions import Action
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
@dataclass
|
|
@@ -28,16 +27,15 @@ class FileStore(HasState[FileState], HandlesActions):
|
|
|
28
27
|
|
|
29
28
|
def handle_action(self, action: Action) -> None:
|
|
30
29
|
"""Modify state in reaction to an action."""
|
|
31
|
-
|
|
32
|
-
self.
|
|
30
|
+
for state_update in get_state_updates(action):
|
|
31
|
+
self._handle_state_update(state_update)
|
|
33
32
|
|
|
34
|
-
def
|
|
35
|
-
if
|
|
36
|
-
|
|
37
|
-
self._state.file_ids.extend(command.result.fileIds)
|
|
33
|
+
def _handle_state_update(self, state_update: update_types.StateUpdate) -> None:
|
|
34
|
+
if state_update.files_added != update_types.NO_CHANGE:
|
|
35
|
+
self._state.file_ids.extend(state_update.files_added.file_ids)
|
|
38
36
|
|
|
39
37
|
|
|
40
|
-
class FileView
|
|
38
|
+
class FileView:
|
|
41
39
|
"""Read-only engine created file state view."""
|
|
42
40
|
|
|
43
41
|
_state: FileState
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Implements fluid stack tracking for pipettes.
|
|
2
|
+
|
|
3
|
+
Inside a pipette's tip, there can be a mix of kinds of fluids - here, "fluid" means "liquid" (i.e. a protocol-relevant
|
|
4
|
+
working liquid that is aspirated or dispensed from wells) or "air" (i.e. because there was an air gap). Since sometimes
|
|
5
|
+
you want air gaps in different places - physically-below liquid to prevent dripping, physically-above liquid to provide
|
|
6
|
+
extra room to push the plunger - we need to support some notion of at least phsyical ordinal position of air and liquid,
|
|
7
|
+
and we do so as a logical stack because that's physically relevant.
|
|
8
|
+
"""
|
|
9
|
+
from logging import getLogger
|
|
10
|
+
from numpy import isclose
|
|
11
|
+
from ..types import AspiratedFluid, FluidKind
|
|
12
|
+
|
|
13
|
+
_LOG = getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FluidStack:
|
|
17
|
+
"""A FluidStack data structure is a list of AspiratedFluids, with stack-style (last-in-first-out) ordering.
|
|
18
|
+
|
|
19
|
+
The front of the list is the physical-top of the liquid stack (logical-bottom of the stack data structure)
|
|
20
|
+
and the back of the list is the physical-bottom of the liquid stack (logical-top of the stack data structure).
|
|
21
|
+
The state is internal and the interaction surface is the methods. This is a mutating API.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_FluidStack = list[AspiratedFluid]
|
|
25
|
+
|
|
26
|
+
_fluid_stack: _FluidStack
|
|
27
|
+
|
|
28
|
+
def __init__(self, _fluid_stack: _FluidStack | None = None) -> None:
|
|
29
|
+
"""Build a FluidStack.
|
|
30
|
+
|
|
31
|
+
The argument is provided for testing and shouldn't be generally used.
|
|
32
|
+
"""
|
|
33
|
+
self._fluid_stack = _fluid_stack or []
|
|
34
|
+
|
|
35
|
+
def add_fluid(self, new: AspiratedFluid) -> None:
|
|
36
|
+
"""Add fluid to a stack.
|
|
37
|
+
|
|
38
|
+
If the new fluid is of a different kind than what's on the physical-bottom of the stack, add a new record.
|
|
39
|
+
If the new fluid is of the same kind as what's on the physical-bottom of the stack, add the new volume to
|
|
40
|
+
the same record.
|
|
41
|
+
"""
|
|
42
|
+
if len(self._fluid_stack) == 0 or self._fluid_stack[-1].kind != new.kind:
|
|
43
|
+
# this is a new kind of fluid, append the record
|
|
44
|
+
self._fluid_stack.append(new)
|
|
45
|
+
else:
|
|
46
|
+
# this is more of the same kind of fluid, add the volumes
|
|
47
|
+
old_fluid = self._fluid_stack.pop(-1)
|
|
48
|
+
self._fluid_stack.append(
|
|
49
|
+
AspiratedFluid(kind=new.kind, volume=old_fluid.volume + new.volume)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def _alter_fluid_records(
|
|
53
|
+
self, remove: int, new_last: AspiratedFluid | None
|
|
54
|
+
) -> None:
|
|
55
|
+
if remove >= len(self._fluid_stack) or len(self._fluid_stack) == 0:
|
|
56
|
+
self._fluid_stack = []
|
|
57
|
+
return
|
|
58
|
+
if remove != 0:
|
|
59
|
+
removed = self._fluid_stack[:-remove]
|
|
60
|
+
else:
|
|
61
|
+
removed = self._fluid_stack
|
|
62
|
+
if new_last:
|
|
63
|
+
removed[-1] = new_last
|
|
64
|
+
self._fluid_stack = removed
|
|
65
|
+
|
|
66
|
+
def remove_fluid(self, volume: float) -> None:
|
|
67
|
+
"""Remove a specific amount of fluid from the physical-bottom of the stack.
|
|
68
|
+
|
|
69
|
+
This will consume records that are wholly included in the provided volume and alter the remaining
|
|
70
|
+
final records (if any) to decrement the amount of volume removed from it.
|
|
71
|
+
|
|
72
|
+
This function is designed to be used inside pipette store action handlers, which are generally not
|
|
73
|
+
exception-safe, and therefore swallows and logs errors.
|
|
74
|
+
"""
|
|
75
|
+
self._fluid_stack_iterator = reversed(self._fluid_stack)
|
|
76
|
+
removed_elements: list[AspiratedFluid] = []
|
|
77
|
+
while volume > 0:
|
|
78
|
+
try:
|
|
79
|
+
last_stack_element = next(self._fluid_stack_iterator)
|
|
80
|
+
except StopIteration:
|
|
81
|
+
_LOG.error(
|
|
82
|
+
f"Attempting to remove more fluid than present, {volume}uL left over"
|
|
83
|
+
)
|
|
84
|
+
self._alter_fluid_records(len(removed_elements), None)
|
|
85
|
+
return
|
|
86
|
+
if last_stack_element.volume < volume:
|
|
87
|
+
removed_elements.append(last_stack_element)
|
|
88
|
+
volume -= last_stack_element.volume
|
|
89
|
+
elif isclose(last_stack_element.volume, volume):
|
|
90
|
+
self._alter_fluid_records(len(removed_elements) + 1, None)
|
|
91
|
+
return
|
|
92
|
+
else:
|
|
93
|
+
self._alter_fluid_records(
|
|
94
|
+
len(removed_elements),
|
|
95
|
+
AspiratedFluid(
|
|
96
|
+
kind=last_stack_element.kind,
|
|
97
|
+
volume=last_stack_element.volume - volume,
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
_LOG.error(f"Failed to handle removing {volume}uL from {self._fluid_stack}")
|
|
103
|
+
|
|
104
|
+
def aspirated_volume(self, kind: FluidKind | None = None) -> float:
|
|
105
|
+
"""Measure the total amount of fluid (optionally filtered by kind) in the stack."""
|
|
106
|
+
volume = 0.0
|
|
107
|
+
for el in self._fluid_stack:
|
|
108
|
+
if kind is not None and el.kind != kind:
|
|
109
|
+
continue
|
|
110
|
+
volume += el.volume
|
|
111
|
+
return volume
|
|
112
|
+
|
|
113
|
+
def liquid_part_of_dispense_volume(self, volume: float) -> float:
|
|
114
|
+
"""Get the amount of liquid in the specified volume starting at the physical-bottom of the stack."""
|
|
115
|
+
liquid_volume = 0.0
|
|
116
|
+
for el in reversed(self._fluid_stack):
|
|
117
|
+
if el.kind == FluidKind.LIQUID:
|
|
118
|
+
liquid_volume += min(volume, el.volume)
|
|
119
|
+
volume -= min(el.volume, volume)
|
|
120
|
+
if isclose(volume, 0.0):
|
|
121
|
+
return liquid_volume
|
|
122
|
+
return liquid_volume
|
|
123
|
+
|
|
124
|
+
def __eq__(self, other: object) -> bool:
|
|
125
|
+
"""Equality."""
|
|
126
|
+
if isinstance(other, type(self)):
|
|
127
|
+
return other._fluid_stack == self._fluid_stack
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
def __repr__(self) -> str:
|
|
131
|
+
"""String representation of a fluid stack."""
|
|
132
|
+
if self._fluid_stack:
|
|
133
|
+
stringified_stack = (
|
|
134
|
+
f'(top) {", ".join([str(item) for item in self._fluid_stack])} (bottom)'
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
stringified_stack = "empty"
|
|
138
|
+
return f"<{self.__class__.__name__}: {stringified_stack}>"
|
|
@@ -220,28 +220,40 @@ def _get_segment_capacity(segment: WellSegment) -> float:
|
|
|
220
220
|
section_height = segment.topHeight - segment.bottomHeight
|
|
221
221
|
match segment:
|
|
222
222
|
case SphericalSegment():
|
|
223
|
-
return
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
return (
|
|
224
|
+
_volume_from_height_spherical(
|
|
225
|
+
target_height=segment.topHeight,
|
|
226
|
+
radius_of_curvature=segment.radiusOfCurvature,
|
|
227
|
+
)
|
|
228
|
+
* segment.count
|
|
226
229
|
)
|
|
227
230
|
case CuboidalFrustum():
|
|
228
|
-
return
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
231
|
+
return (
|
|
232
|
+
_volume_from_height_rectangular(
|
|
233
|
+
target_height=section_height,
|
|
234
|
+
bottom_length=segment.bottomYDimension,
|
|
235
|
+
bottom_width=segment.bottomXDimension,
|
|
236
|
+
top_length=segment.topYDimension,
|
|
237
|
+
top_width=segment.topXDimension,
|
|
238
|
+
total_frustum_height=section_height,
|
|
239
|
+
)
|
|
240
|
+
* segment.count
|
|
235
241
|
)
|
|
236
242
|
case ConicalFrustum():
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
243
|
+
return (
|
|
244
|
+
_volume_from_height_circular(
|
|
245
|
+
target_height=section_height,
|
|
246
|
+
total_frustum_height=section_height,
|
|
247
|
+
bottom_radius=(segment.bottomDiameter / 2),
|
|
248
|
+
top_radius=(segment.topDiameter / 2),
|
|
249
|
+
)
|
|
250
|
+
* segment.count
|
|
242
251
|
)
|
|
243
252
|
case SquaredConeSegment():
|
|
244
|
-
return
|
|
253
|
+
return (
|
|
254
|
+
_volume_from_height_squared_cone(section_height, segment)
|
|
255
|
+
* segment.count
|
|
256
|
+
)
|
|
245
257
|
case _:
|
|
246
258
|
# TODO: implement volume calculations for truncated circular and rounded rectangular segments
|
|
247
259
|
raise NotImplementedError(
|
|
@@ -272,6 +284,7 @@ def height_at_volume_within_section(
|
|
|
272
284
|
section_height: float,
|
|
273
285
|
) -> float:
|
|
274
286
|
"""Calculate a height within a bounded section according to geometry."""
|
|
287
|
+
target_volume_relative = target_volume_relative / section.count
|
|
275
288
|
match section:
|
|
276
289
|
case SphericalSegment():
|
|
277
290
|
return _height_from_volume_spherical(
|
|
@@ -311,28 +324,40 @@ def volume_at_height_within_section(
|
|
|
311
324
|
"""Calculate a volume within a bounded section according to geometry."""
|
|
312
325
|
match section:
|
|
313
326
|
case SphericalSegment():
|
|
314
|
-
return
|
|
315
|
-
|
|
316
|
-
|
|
327
|
+
return (
|
|
328
|
+
_volume_from_height_spherical(
|
|
329
|
+
target_height=target_height_relative,
|
|
330
|
+
radius_of_curvature=section.radiusOfCurvature,
|
|
331
|
+
)
|
|
332
|
+
* section.count
|
|
317
333
|
)
|
|
318
334
|
case ConicalFrustum():
|
|
319
|
-
return
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
335
|
+
return (
|
|
336
|
+
_volume_from_height_circular(
|
|
337
|
+
target_height=target_height_relative,
|
|
338
|
+
total_frustum_height=section_height,
|
|
339
|
+
bottom_radius=(section.bottomDiameter / 2),
|
|
340
|
+
top_radius=(section.topDiameter / 2),
|
|
341
|
+
)
|
|
342
|
+
* section.count
|
|
324
343
|
)
|
|
325
344
|
case CuboidalFrustum():
|
|
326
|
-
return
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
345
|
+
return (
|
|
346
|
+
_volume_from_height_rectangular(
|
|
347
|
+
target_height=target_height_relative,
|
|
348
|
+
total_frustum_height=section_height,
|
|
349
|
+
bottom_width=section.bottomXDimension,
|
|
350
|
+
bottom_length=section.bottomYDimension,
|
|
351
|
+
top_width=section.topXDimension,
|
|
352
|
+
top_length=section.topYDimension,
|
|
353
|
+
)
|
|
354
|
+
* section.count
|
|
333
355
|
)
|
|
334
356
|
case SquaredConeSegment():
|
|
335
|
-
return
|
|
357
|
+
return (
|
|
358
|
+
_volume_from_height_squared_cone(target_height_relative, section)
|
|
359
|
+
* section.count
|
|
360
|
+
)
|
|
336
361
|
case _:
|
|
337
362
|
# TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712
|
|
338
363
|
# we need to input the math attached to that issue
|
|
@@ -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.
|
|
@@ -815,6 +860,11 @@ class LabwareView(HasState[LabwareState]):
|
|
|
815
860
|
return self.raise_if_labware_inaccessible_by_pipette(
|
|
816
861
|
labware_location.labwareId
|
|
817
862
|
)
|
|
863
|
+
elif labware.lid_id is not None:
|
|
864
|
+
raise errors.LocationNotAccessibleByPipetteError(
|
|
865
|
+
f"Cannot move pipette to {labware.loadName} "
|
|
866
|
+
"because labware is currently covered by a lid."
|
|
867
|
+
)
|
|
818
868
|
elif isinstance(labware_location, AddressableAreaLocation):
|
|
819
869
|
if fixture_validation.is_staging_slot(labware_location.addressableAreaName):
|
|
820
870
|
raise errors.LocationNotAccessibleByPipetteError(
|
|
@@ -998,11 +1048,15 @@ class LabwareView(HasState[LabwareState]):
|
|
|
998
1048
|
return None
|
|
999
1049
|
else:
|
|
1000
1050
|
return LabwareMovementOffsetData(
|
|
1001
|
-
pickUpOffset=
|
|
1002
|
-
|
|
1051
|
+
pickUpOffset=LabwareOffsetVector.model_construct(
|
|
1052
|
+
x=parsed_offsets[offset_key].pickUpOffset.x,
|
|
1053
|
+
y=parsed_offsets[offset_key].pickUpOffset.y,
|
|
1054
|
+
z=parsed_offsets[offset_key].pickUpOffset.z,
|
|
1003
1055
|
),
|
|
1004
|
-
dropOffset=
|
|
1005
|
-
|
|
1056
|
+
dropOffset=LabwareOffsetVector.model_construct(
|
|
1057
|
+
x=parsed_offsets[offset_key].dropOffset.x,
|
|
1058
|
+
y=parsed_offsets[offset_key].dropOffset.y,
|
|
1059
|
+
z=parsed_offsets[offset_key].dropOffset.z,
|
|
1006
1060
|
),
|
|
1007
1061
|
)
|
|
1008
1062
|
|