opentrons 8.1.0a0__py2.py3-none-any.whl → 8.2.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +208 -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/backends/ot3utils.py +1 -0
- 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 +232 -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 +131 -94
- 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/hardware_control/protocols/position_estimator.py +3 -1
- opentrons/hardware_control/types.py +2 -0
- opentrons/legacy_commands/helpers.py +8 -2
- 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 +20 -4
- opentrons/protocol_api/core/engine/module_core.py +166 -17
- opentrons/protocol_api/core/engine/pipette_movement_conflict.py +362 -0
- opentrons/protocol_api/core/engine/protocol.py +30 -2
- opentrons/protocol_api/core/instrument.py +2 -0
- opentrons/protocol_api/core/labware.py +4 -0
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -0
- opentrons/protocol_api/core/legacy/legacy_labware_core.py +5 -0
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +6 -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 +6 -2
- opentrons/protocol_api/instrument_context.py +52 -20
- opentrons/protocol_api/labware.py +13 -1
- opentrons/protocol_api/module_contexts.py +115 -17
- opentrons/protocol_api/protocol_context.py +49 -5
- opentrons/protocol_api/validation.py +5 -3
- opentrons/protocol_engine/__init__.py +10 -9
- opentrons/protocol_engine/actions/__init__.py +3 -0
- opentrons/protocol_engine/actions/actions.py +30 -25
- opentrons/protocol_engine/actions/get_state_update.py +38 -0
- opentrons/protocol_engine/clients/sync_client.py +1 -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 +148 -0
- opentrons/protocol_engine/commands/absorbance_reader/initialize.py +65 -9
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +148 -0
- opentrons/protocol_engine/commands/absorbance_reader/read.py +200 -0
- opentrons/protocol_engine/commands/aspirate.py +29 -16
- opentrons/protocol_engine/commands/aspirate_in_place.py +33 -16
- 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 +31 -18
- 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 +70 -16
- opentrons/protocol_engine/commands/drop_tip_in_place.py +59 -13
- 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 +28 -5
- opentrons/protocol_engine/commands/load_liquid.py +18 -7
- opentrons/protocol_engine/commands/load_module.py +4 -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 +155 -23
- 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 +51 -30
- opentrons/protocol_engine/commands/pipetting_common.py +47 -16
- 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 +208 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +77 -0
- opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +10 -4
- 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 +60 -10
- opentrons/protocol_engine/engine_support.py +2 -1
- opentrons/protocol_engine/error_recovery_policy.py +14 -3
- opentrons/protocol_engine/errors/__init__.py +20 -0
- opentrons/protocol_engine/errors/error_occurrence.py +8 -3
- opentrons/protocol_engine/errors/exceptions.py +127 -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 +73 -22
- opentrons/protocol_engine/execution/movement.py +17 -7
- 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 +47 -31
- opentrons/protocol_engine/resources/__init__.py +2 -0
- opentrons/protocol_engine/resources/deck_data_provider.py +19 -5
- opentrons/protocol_engine/resources/file_provider.py +161 -0
- opentrons/protocol_engine/resources/fixture_validation.py +11 -1
- 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 +445 -59
- opentrons/protocol_engine/state/labware.py +264 -84
- opentrons/protocol_engine/state/liquids.py +1 -1
- opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +21 -3
- opentrons/protocol_engine/state/modules.py +145 -90
- opentrons/protocol_engine/state/motion.py +33 -14
- 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 +424 -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 +41 -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/protocols/parameters/csv_parameter_interface.py +3 -1
- 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.0.dist-info}/METADATA +5 -4
- {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/RECORD +235 -223
- 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.0.dist-info}/LICENSE +0 -0
- {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/WHEEL +0 -0
- {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/entry_points.txt +0 -0
- {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
"""Protocol API module implementation logic."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from typing import Optional, List
|
|
4
|
+
from typing import Optional, List, Dict, Union
|
|
5
5
|
|
|
6
6
|
from opentrons.hardware_control import SynchronousAdapter, modules as hw_modules
|
|
7
7
|
from opentrons.hardware_control.modules.types import (
|
|
8
8
|
ModuleModel,
|
|
9
9
|
TemperatureStatus,
|
|
10
10
|
MagneticStatus,
|
|
11
|
-
ThermocyclerStep,
|
|
12
11
|
SpeedStatus,
|
|
13
12
|
module_model_from_string,
|
|
14
13
|
)
|
|
@@ -16,15 +15,18 @@ from opentrons.drivers.types import (
|
|
|
16
15
|
HeaterShakerLabwareLatchStatus,
|
|
17
16
|
ThermocyclerLidStatus,
|
|
18
17
|
)
|
|
19
|
-
|
|
18
|
+
|
|
20
19
|
from opentrons.protocol_engine import commands as cmd
|
|
20
|
+
from opentrons.protocol_engine.types import ABSMeasureMode
|
|
21
|
+
from opentrons.types import DeckSlotName
|
|
21
22
|
from opentrons.protocol_engine.clients import SyncClient as ProtocolEngineClient
|
|
22
23
|
from opentrons.protocol_engine.errors.exceptions import (
|
|
23
24
|
LabwareNotLoadedOnModuleError,
|
|
24
25
|
NoMagnetEngageHeightError,
|
|
26
|
+
CannotPerformModuleAction,
|
|
25
27
|
)
|
|
26
28
|
|
|
27
|
-
from opentrons.protocols.api_support.types import APIVersion
|
|
29
|
+
from opentrons.protocols.api_support.types import APIVersion, ThermocyclerStep
|
|
28
30
|
|
|
29
31
|
from ... import validation
|
|
30
32
|
from ..module import (
|
|
@@ -39,6 +41,11 @@ from ..module import (
|
|
|
39
41
|
from .exceptions import InvalidMagnetEngageHeightError
|
|
40
42
|
|
|
41
43
|
|
|
44
|
+
# Valid wavelength range for absorbance reader
|
|
45
|
+
ABS_WAVELENGTH_MIN = 350
|
|
46
|
+
ABS_WAVELENGTH_MAX = 1000
|
|
47
|
+
|
|
48
|
+
|
|
42
49
|
class ModuleCore(AbstractModuleCore):
|
|
43
50
|
"""Module core logic implementation for Python protocols.
|
|
44
51
|
Args:
|
|
@@ -324,15 +331,13 @@ class ThermocyclerModuleCore(ModuleCore, AbstractThermocyclerCore):
|
|
|
324
331
|
cmd.thermocycler.WaitForLidTemperatureParams(moduleId=self.module_id)
|
|
325
332
|
)
|
|
326
333
|
|
|
327
|
-
def
|
|
334
|
+
def _execute_profile_pre_221(
|
|
328
335
|
self,
|
|
329
336
|
steps: List[ThermocyclerStep],
|
|
330
337
|
repetitions: int,
|
|
331
|
-
block_max_volume: Optional[float]
|
|
338
|
+
block_max_volume: Optional[float],
|
|
332
339
|
) -> None:
|
|
333
|
-
"""Execute a
|
|
334
|
-
self._repetitions = repetitions
|
|
335
|
-
self._step_count = len(steps)
|
|
340
|
+
"""Execute a thermocycler profile using thermocycler/runProfile and flattened steps."""
|
|
336
341
|
engine_steps = [
|
|
337
342
|
cmd.thermocycler.RunProfileStepParams(
|
|
338
343
|
celsius=step["temperature"],
|
|
@@ -349,6 +354,49 @@ class ThermocyclerModuleCore(ModuleCore, AbstractThermocyclerCore):
|
|
|
349
354
|
)
|
|
350
355
|
)
|
|
351
356
|
|
|
357
|
+
def _execute_profile_post_221(
|
|
358
|
+
self,
|
|
359
|
+
steps: List[ThermocyclerStep],
|
|
360
|
+
repetitions: int,
|
|
361
|
+
block_max_volume: Optional[float],
|
|
362
|
+
) -> None:
|
|
363
|
+
"""Execute a thermocycler profile using thermocycler/runExtendedProfile."""
|
|
364
|
+
engine_steps: List[
|
|
365
|
+
Union[cmd.thermocycler.ProfileCycle, cmd.thermocycler.ProfileStep]
|
|
366
|
+
] = [
|
|
367
|
+
cmd.thermocycler.ProfileCycle(
|
|
368
|
+
repetitions=repetitions,
|
|
369
|
+
steps=[
|
|
370
|
+
cmd.thermocycler.ProfileStep(
|
|
371
|
+
celsius=step["temperature"],
|
|
372
|
+
holdSeconds=step["hold_time_seconds"],
|
|
373
|
+
)
|
|
374
|
+
for step in steps
|
|
375
|
+
],
|
|
376
|
+
)
|
|
377
|
+
]
|
|
378
|
+
self._engine_client.execute_command(
|
|
379
|
+
cmd.thermocycler.RunExtendedProfileParams(
|
|
380
|
+
moduleId=self.module_id,
|
|
381
|
+
profileElements=engine_steps,
|
|
382
|
+
blockMaxVolumeUl=block_max_volume,
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def execute_profile(
|
|
387
|
+
self,
|
|
388
|
+
steps: List[ThermocyclerStep],
|
|
389
|
+
repetitions: int,
|
|
390
|
+
block_max_volume: Optional[float] = None,
|
|
391
|
+
) -> None:
|
|
392
|
+
"""Execute a Thermocycler Profile."""
|
|
393
|
+
self._repetitions = repetitions
|
|
394
|
+
self._step_count = len(steps)
|
|
395
|
+
if self.api_version >= APIVersion(2, 21):
|
|
396
|
+
return self._execute_profile_post_221(steps, repetitions, block_max_volume)
|
|
397
|
+
else:
|
|
398
|
+
return self._execute_profile_pre_221(steps, repetitions, block_max_volume)
|
|
399
|
+
|
|
352
400
|
def deactivate_lid(self) -> None:
|
|
353
401
|
"""Turn off the heated lid."""
|
|
354
402
|
self._engine_client.execute_command(
|
|
@@ -523,23 +571,124 @@ class AbsorbanceReaderCore(ModuleCore, AbstractAbsorbanceReaderCore):
|
|
|
523
571
|
"""Absorbance Reader core logic implementation for Python protocols."""
|
|
524
572
|
|
|
525
573
|
_sync_module_hardware: SynchronousAdapter[hw_modules.AbsorbanceReader]
|
|
526
|
-
_initialized_value: Optional[int] = None
|
|
574
|
+
_initialized_value: Optional[List[int]] = None
|
|
575
|
+
_ready_to_initialize: bool = False
|
|
527
576
|
|
|
528
|
-
def initialize(
|
|
577
|
+
def initialize(
|
|
578
|
+
self,
|
|
579
|
+
mode: ABSMeasureMode,
|
|
580
|
+
wavelengths: List[int],
|
|
581
|
+
reference_wavelength: Optional[int] = None,
|
|
582
|
+
) -> None:
|
|
529
583
|
"""Initialize the Absorbance Reader by taking zero reading."""
|
|
584
|
+
if not self._ready_to_initialize:
|
|
585
|
+
raise CannotPerformModuleAction(
|
|
586
|
+
"Cannot perform Initialize action on Absorbance Reader without calling `.close_lid()` first."
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
wavelength_len = len(wavelengths)
|
|
590
|
+
if mode == "single" and wavelength_len != 1:
|
|
591
|
+
raise ValueError(
|
|
592
|
+
f"Single mode can only be initialized with 1 wavelength"
|
|
593
|
+
f" {wavelength_len} wavelengths provided instead."
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
if mode == "multi" and (wavelength_len < 1 or wavelength_len > 6):
|
|
597
|
+
raise ValueError(
|
|
598
|
+
f"Multi mode can only be initialized with 1 - 6 wavelengths."
|
|
599
|
+
f" {wavelength_len} wavelengths provided instead."
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
if reference_wavelength is not None and (
|
|
603
|
+
reference_wavelength < ABS_WAVELENGTH_MIN
|
|
604
|
+
or reference_wavelength > ABS_WAVELENGTH_MAX
|
|
605
|
+
):
|
|
606
|
+
raise ValueError(
|
|
607
|
+
f"Unsupported reference wavelength: ({reference_wavelength}) needs"
|
|
608
|
+
f" to between {ABS_WAVELENGTH_MIN} and {ABS_WAVELENGTH_MAX} nm."
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
for wavelength in wavelengths:
|
|
612
|
+
if (
|
|
613
|
+
not isinstance(wavelength, int)
|
|
614
|
+
or wavelength < ABS_WAVELENGTH_MIN
|
|
615
|
+
or wavelength > ABS_WAVELENGTH_MAX
|
|
616
|
+
):
|
|
617
|
+
raise ValueError(
|
|
618
|
+
f"Unsupported sample wavelength: ({wavelength}) needs"
|
|
619
|
+
f" to between {ABS_WAVELENGTH_MIN} and {ABS_WAVELENGTH_MAX} nm."
|
|
620
|
+
)
|
|
621
|
+
|
|
530
622
|
self._engine_client.execute_command(
|
|
531
623
|
cmd.absorbance_reader.InitializeParams(
|
|
532
624
|
moduleId=self.module_id,
|
|
533
|
-
|
|
625
|
+
measureMode=mode,
|
|
626
|
+
sampleWavelengths=wavelengths,
|
|
627
|
+
referenceWavelength=reference_wavelength,
|
|
534
628
|
),
|
|
535
629
|
)
|
|
536
|
-
self._initialized_value =
|
|
630
|
+
self._initialized_value = wavelengths
|
|
537
631
|
|
|
538
|
-
def
|
|
539
|
-
"""Initiate read on the Absorbance Reader."""
|
|
632
|
+
def read(self, filename: Optional[str] = None) -> Dict[int, Dict[str, float]]:
|
|
633
|
+
"""Initiate a read on the Absorbance Reader, and return the results. During Analysis, this will return a measurement of zero for all wells."""
|
|
634
|
+
wavelengths = self._engine_client.state.modules.get_absorbance_reader_substate(
|
|
635
|
+
self.module_id
|
|
636
|
+
).configured_wavelengths
|
|
637
|
+
if wavelengths is None:
|
|
638
|
+
raise CannotPerformModuleAction(
|
|
639
|
+
"Cannot perform Read action on Absorbance Reader without calling `.initialize(...)` first."
|
|
640
|
+
)
|
|
540
641
|
if self._initialized_value:
|
|
541
642
|
self._engine_client.execute_command(
|
|
542
|
-
cmd.absorbance_reader.
|
|
543
|
-
moduleId=self.module_id,
|
|
643
|
+
cmd.absorbance_reader.ReadAbsorbanceParams(
|
|
644
|
+
moduleId=self.module_id, fileName=filename
|
|
544
645
|
)
|
|
545
646
|
)
|
|
647
|
+
if not self._engine_client.state.config.use_virtual_modules:
|
|
648
|
+
read_result = (
|
|
649
|
+
self._engine_client.state.modules.get_absorbance_reader_substate(
|
|
650
|
+
self.module_id
|
|
651
|
+
).data
|
|
652
|
+
)
|
|
653
|
+
if read_result is not None:
|
|
654
|
+
return read_result
|
|
655
|
+
raise CannotPerformModuleAction(
|
|
656
|
+
"Absorbance Reader failed to return expected read result."
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
# When using virtual modules, return all zeroes
|
|
660
|
+
virtual_asbsorbance_result: Dict[int, Dict[str, float]] = {}
|
|
661
|
+
for wavelength in wavelengths:
|
|
662
|
+
converted_values = (
|
|
663
|
+
self._engine_client.state.modules.convert_absorbance_reader_data_points(
|
|
664
|
+
data=[0] * 96
|
|
665
|
+
)
|
|
666
|
+
)
|
|
667
|
+
virtual_asbsorbance_result[wavelength] = converted_values
|
|
668
|
+
return virtual_asbsorbance_result
|
|
669
|
+
|
|
670
|
+
def close_lid(
|
|
671
|
+
self,
|
|
672
|
+
) -> None:
|
|
673
|
+
"""Close the Absorbance Reader's lid."""
|
|
674
|
+
self._engine_client.execute_command(
|
|
675
|
+
cmd.absorbance_reader.CloseLidParams(
|
|
676
|
+
moduleId=self.module_id,
|
|
677
|
+
)
|
|
678
|
+
)
|
|
679
|
+
self._ready_to_initialize = True
|
|
680
|
+
|
|
681
|
+
def open_lid(self) -> None:
|
|
682
|
+
"""Close the Absorbance Reader's lid."""
|
|
683
|
+
self._engine_client.execute_command(
|
|
684
|
+
cmd.absorbance_reader.OpenLidParams(
|
|
685
|
+
moduleId=self.module_id,
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
def is_lid_on(self) -> bool:
|
|
690
|
+
"""Returns True if the Absorbance Reader's lid is currently on the Reader slot."""
|
|
691
|
+
abs_state = self._engine_client.state.modules.get_absorbance_reader_substate(
|
|
692
|
+
self.module_id
|
|
693
|
+
)
|
|
694
|
+
return abs_state.is_lid_on
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""A Protocol-Engine-friendly wrapper for opentrons.motion_planning.deck_conflict."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import logging
|
|
4
|
+
from typing import (
|
|
5
|
+
Optional,
|
|
6
|
+
Tuple,
|
|
7
|
+
Union,
|
|
8
|
+
List,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from opentrons_shared_data.errors.exceptions import MotionPlanningFailureError
|
|
12
|
+
from opentrons.protocol_engine.errors import LocationIsStagingSlotError
|
|
13
|
+
from opentrons_shared_data.module import FLEX_TC_LID_COLLISION_ZONE
|
|
14
|
+
|
|
15
|
+
from opentrons.hardware_control import CriticalPoint
|
|
16
|
+
from opentrons.motion_planning import adjacent_slots_getters
|
|
17
|
+
|
|
18
|
+
from opentrons.protocol_engine import (
|
|
19
|
+
StateView,
|
|
20
|
+
DeckSlotLocation,
|
|
21
|
+
OnLabwareLocation,
|
|
22
|
+
WellLocation,
|
|
23
|
+
LiquidHandlingWellLocation,
|
|
24
|
+
PickUpTipWellLocation,
|
|
25
|
+
DropTipWellLocation,
|
|
26
|
+
)
|
|
27
|
+
from opentrons.protocol_engine.types import (
|
|
28
|
+
StagingSlotLocation,
|
|
29
|
+
)
|
|
30
|
+
from opentrons.types import DeckSlotName, StagingSlotName, Point
|
|
31
|
+
from . import point_calculations
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PartialTipMovementNotAllowedError(MotionPlanningFailureError):
|
|
35
|
+
"""Error raised when trying to perform a partial tip movement to an illegal location."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, message: str) -> None:
|
|
38
|
+
super().__init__(
|
|
39
|
+
message=message,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UnsuitableTiprackForPipetteMotion(MotionPlanningFailureError):
|
|
44
|
+
"""Error raised when trying to perform a pipette movement to a tip rack, based on adapter status."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, message: str) -> None:
|
|
47
|
+
super().__init__(
|
|
48
|
+
message=message,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_log = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
_FLEX_TC_LID_BACK_LEFT_PT = Point(
|
|
55
|
+
x=FLEX_TC_LID_COLLISION_ZONE["back_left"]["x"],
|
|
56
|
+
y=FLEX_TC_LID_COLLISION_ZONE["back_left"]["y"],
|
|
57
|
+
z=FLEX_TC_LID_COLLISION_ZONE["back_left"]["z"],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
_FLEX_TC_LID_FRONT_RIGHT_PT = Point(
|
|
61
|
+
x=FLEX_TC_LID_COLLISION_ZONE["front_right"]["x"],
|
|
62
|
+
y=FLEX_TC_LID_COLLISION_ZONE["front_right"]["y"],
|
|
63
|
+
z=FLEX_TC_LID_COLLISION_ZONE["front_right"]["z"],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def check_safe_for_pipette_movement( # noqa: C901
|
|
68
|
+
engine_state: StateView,
|
|
69
|
+
pipette_id: str,
|
|
70
|
+
labware_id: str,
|
|
71
|
+
well_name: str,
|
|
72
|
+
well_location: Union[
|
|
73
|
+
WellLocation,
|
|
74
|
+
LiquidHandlingWellLocation,
|
|
75
|
+
PickUpTipWellLocation,
|
|
76
|
+
DropTipWellLocation,
|
|
77
|
+
],
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Check if the labware is safe to move to with a pipette in partial tip configuration.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
engine_state: engine state view
|
|
83
|
+
pipette_id: ID of the pipette to be moved
|
|
84
|
+
labware_id: ID of the labware we are moving to
|
|
85
|
+
well_name: Name of the well to move to
|
|
86
|
+
well_location: exact location within the well to move to
|
|
87
|
+
"""
|
|
88
|
+
# TODO (spp, 2023-02-06): remove this check after thorough testing.
|
|
89
|
+
# This function is capable of checking for movement conflict regardless of
|
|
90
|
+
# nozzle configuration.
|
|
91
|
+
if not engine_state.pipettes.get_is_partially_configured(pipette_id):
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if isinstance(well_location, DropTipWellLocation):
|
|
95
|
+
# convert to WellLocation
|
|
96
|
+
well_location = engine_state.geometry.get_checked_tip_drop_location(
|
|
97
|
+
pipette_id=pipette_id,
|
|
98
|
+
labware_id=labware_id,
|
|
99
|
+
well_location=well_location,
|
|
100
|
+
partially_configured=True,
|
|
101
|
+
)
|
|
102
|
+
well_location_point = engine_state.geometry.get_well_position(
|
|
103
|
+
labware_id=labware_id, well_name=well_name, well_location=well_location
|
|
104
|
+
)
|
|
105
|
+
primary_nozzle = engine_state.pipettes.get_primary_nozzle(pipette_id)
|
|
106
|
+
|
|
107
|
+
destination_cp = _get_critical_point_to_use(engine_state, labware_id)
|
|
108
|
+
|
|
109
|
+
pipette_bounds_at_well_location = (
|
|
110
|
+
engine_state.pipettes.get_pipette_bounds_at_specified_move_to_position(
|
|
111
|
+
pipette_id=pipette_id,
|
|
112
|
+
destination_position=well_location_point,
|
|
113
|
+
critical_point=destination_cp,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
if not _is_within_pipette_extents(
|
|
117
|
+
engine_state=engine_state,
|
|
118
|
+
pipette_id=pipette_id,
|
|
119
|
+
pipette_bounding_box_at_loc=pipette_bounds_at_well_location,
|
|
120
|
+
):
|
|
121
|
+
raise PartialTipMovementNotAllowedError(
|
|
122
|
+
f"Requested motion with the {primary_nozzle} nozzle partial configuration"
|
|
123
|
+
f" is outside of robot bounds for the pipette."
|
|
124
|
+
)
|
|
125
|
+
ancestor = engine_state.geometry.get_ancestor_slot_name(labware_id)
|
|
126
|
+
if isinstance(ancestor, StagingSlotName):
|
|
127
|
+
raise LocationIsStagingSlotError(
|
|
128
|
+
"Cannot perform pipette actions on labware in Staging Area Slot."
|
|
129
|
+
)
|
|
130
|
+
labware_slot = ancestor
|
|
131
|
+
|
|
132
|
+
surrounding_slots = adjacent_slots_getters.get_surrounding_slots(
|
|
133
|
+
slot=labware_slot.as_int(), robot_type=engine_state.config.robot_type
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if _will_collide_with_thermocycler_lid(
|
|
137
|
+
engine_state=engine_state,
|
|
138
|
+
pipette_bounds=pipette_bounds_at_well_location,
|
|
139
|
+
surrounding_regular_slots=surrounding_slots.regular_slots,
|
|
140
|
+
):
|
|
141
|
+
raise PartialTipMovementNotAllowedError(
|
|
142
|
+
f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
|
|
143
|
+
f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
|
|
144
|
+
f" will result in collision with thermocycler lid in deck slot A1."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
for regular_slot in surrounding_slots.regular_slots:
|
|
148
|
+
if _slot_has_potential_colliding_object(
|
|
149
|
+
engine_state=engine_state,
|
|
150
|
+
pipette_bounds=pipette_bounds_at_well_location,
|
|
151
|
+
surrounding_slot=regular_slot,
|
|
152
|
+
):
|
|
153
|
+
raise PartialTipMovementNotAllowedError(
|
|
154
|
+
f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
|
|
155
|
+
f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
|
|
156
|
+
f" will result in collision with items in deck slot {regular_slot}."
|
|
157
|
+
)
|
|
158
|
+
for staging_slot in surrounding_slots.staging_slots:
|
|
159
|
+
if _slot_has_potential_colliding_object(
|
|
160
|
+
engine_state=engine_state,
|
|
161
|
+
pipette_bounds=pipette_bounds_at_well_location,
|
|
162
|
+
surrounding_slot=staging_slot,
|
|
163
|
+
):
|
|
164
|
+
raise PartialTipMovementNotAllowedError(
|
|
165
|
+
f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
|
|
166
|
+
f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
|
|
167
|
+
f" will result in collision with items in staging slot {staging_slot}."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _get_critical_point_to_use(
|
|
172
|
+
engine_state: StateView, labware_id: str
|
|
173
|
+
) -> Optional[CriticalPoint]:
|
|
174
|
+
"""Return the critical point to use when accessing the given labware."""
|
|
175
|
+
# TODO (spp, 2024-09-17): looks like Y_CENTER of column is the same as its XY_CENTER.
|
|
176
|
+
# I'm using this if-else ladder to be consistent with what we do in
|
|
177
|
+
# `MotionPlanning.get_movement_waypoints_to_well()`.
|
|
178
|
+
# We should probably use only XY_CENTER in both places.
|
|
179
|
+
if engine_state.labware.get_should_center_column_on_target_well(labware_id):
|
|
180
|
+
return CriticalPoint.Y_CENTER
|
|
181
|
+
elif engine_state.labware.get_should_center_pipette_on_target_well(labware_id):
|
|
182
|
+
return CriticalPoint.XY_CENTER
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _slot_has_potential_colliding_object(
|
|
187
|
+
engine_state: StateView,
|
|
188
|
+
pipette_bounds: Tuple[Point, Point, Point, Point],
|
|
189
|
+
surrounding_slot: Union[DeckSlotName, StagingSlotName],
|
|
190
|
+
) -> bool:
|
|
191
|
+
"""Return the slot, if any, that has an item that the pipette might collide into."""
|
|
192
|
+
# Check if slot overlaps with pipette position
|
|
193
|
+
slot_pos = engine_state.addressable_areas.get_addressable_area_position(
|
|
194
|
+
addressable_area_name=surrounding_slot.id,
|
|
195
|
+
do_compatibility_check=False,
|
|
196
|
+
)
|
|
197
|
+
slot_bounds = engine_state.addressable_areas.get_addressable_area_bounding_box(
|
|
198
|
+
addressable_area_name=surrounding_slot.id,
|
|
199
|
+
do_compatibility_check=False,
|
|
200
|
+
)
|
|
201
|
+
slot_back_left_coords = Point(slot_pos.x, slot_pos.y + slot_bounds.y, slot_pos.z)
|
|
202
|
+
slot_front_right_coords = Point(slot_pos.x + slot_bounds.x, slot_pos.y, slot_pos.z)
|
|
203
|
+
|
|
204
|
+
# If slot overlaps with pipette bounds
|
|
205
|
+
if point_calculations.are_overlapping_rectangles(
|
|
206
|
+
rectangle1=(pipette_bounds[0], pipette_bounds[1]),
|
|
207
|
+
rectangle2=(slot_back_left_coords, slot_front_right_coords),
|
|
208
|
+
):
|
|
209
|
+
# Check z-height of items in overlapping slot
|
|
210
|
+
if isinstance(surrounding_slot, DeckSlotName):
|
|
211
|
+
slot_highest_z = engine_state.geometry.get_highest_z_in_slot(
|
|
212
|
+
DeckSlotLocation(slotName=surrounding_slot)
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
slot_highest_z = engine_state.geometry.get_highest_z_in_slot(
|
|
216
|
+
StagingSlotLocation(slotName=surrounding_slot)
|
|
217
|
+
)
|
|
218
|
+
return slot_highest_z >= pipette_bounds[0].z
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _will_collide_with_thermocycler_lid(
|
|
223
|
+
engine_state: StateView,
|
|
224
|
+
pipette_bounds: Tuple[Point, Point, Point, Point],
|
|
225
|
+
surrounding_regular_slots: List[DeckSlotName],
|
|
226
|
+
) -> bool:
|
|
227
|
+
"""Return whether the pipette might collide with thermocycler's lid/clips on a Flex.
|
|
228
|
+
|
|
229
|
+
If any of the pipette's bounding vertices lie inside the no-go zone of the thermocycler-
|
|
230
|
+
which is the area that's to the left, back and below the thermocycler's lid's
|
|
231
|
+
protruding clips, then we will mark the movement for possible collision.
|
|
232
|
+
|
|
233
|
+
This could cause false raises for the case where an 8-channel is accessing the
|
|
234
|
+
thermocycler labware in a location such that the pipette is in the area between
|
|
235
|
+
the clips but not touching either clips. But that's a tradeoff we'll need to make
|
|
236
|
+
between a complicated check involving accurate positions of all entities involved
|
|
237
|
+
and a crude check that disallows all partial tip movements around the thermocycler.
|
|
238
|
+
"""
|
|
239
|
+
# TODO (spp, 2024-02-27): Improvements:
|
|
240
|
+
# - make the check dynamic according to lid state:
|
|
241
|
+
# - if lid is open, check if pipette is in no-go zone
|
|
242
|
+
# - if lid is closed, use the closed lid height to check for conflict
|
|
243
|
+
if (
|
|
244
|
+
DeckSlotName.SLOT_A1 in surrounding_regular_slots
|
|
245
|
+
and engine_state.modules.is_flex_deck_with_thermocycler()
|
|
246
|
+
):
|
|
247
|
+
return (
|
|
248
|
+
point_calculations.are_overlapping_rectangles(
|
|
249
|
+
rectangle1=(_FLEX_TC_LID_BACK_LEFT_PT, _FLEX_TC_LID_FRONT_RIGHT_PT),
|
|
250
|
+
rectangle2=(pipette_bounds[0], pipette_bounds[1]),
|
|
251
|
+
)
|
|
252
|
+
and pipette_bounds[0].z <= _FLEX_TC_LID_BACK_LEFT_PT.z
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def check_safe_for_tip_pickup_and_return(
|
|
259
|
+
engine_state: StateView,
|
|
260
|
+
pipette_id: str,
|
|
261
|
+
labware_id: str,
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Check if the presence or absence of a tiprack adapter might cause any pipette movement issues.
|
|
264
|
+
|
|
265
|
+
A 96 channel pipette will pick up tips using cam action when it's configured
|
|
266
|
+
to use ALL nozzles. For this, the tiprack needs to be on the Flex 96 channel tiprack adapter
|
|
267
|
+
or similar or the tips will not be picked up.
|
|
268
|
+
|
|
269
|
+
On the other hand, if the pipette is configured with partial nozzle configuration,
|
|
270
|
+
it uses the usual pipette presses to pick the tips up, in which case, having the tiprack
|
|
271
|
+
on the Flex 96 channel tiprack adapter (or similar) will cause the pipette to
|
|
272
|
+
crash against the adapter posts.
|
|
273
|
+
|
|
274
|
+
In order to check if the 96-channel can move and pickup/drop tips safely, this method
|
|
275
|
+
checks for the height attribute of the tiprack adapter rather than checking for the
|
|
276
|
+
specific official adapter since users might create custom labware &/or definitions
|
|
277
|
+
compatible with the official adapter.
|
|
278
|
+
"""
|
|
279
|
+
if not engine_state.pipettes.get_channels(pipette_id) == 96:
|
|
280
|
+
# Adapters only matter to 96 ch.
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
is_partial_config = engine_state.pipettes.get_is_partially_configured(pipette_id)
|
|
284
|
+
tiprack_name = engine_state.labware.get_display_name(labware_id)
|
|
285
|
+
tiprack_parent = engine_state.labware.get_location(labware_id)
|
|
286
|
+
if isinstance(tiprack_parent, OnLabwareLocation): # tiprack is on an adapter
|
|
287
|
+
is_96_ch_tiprack_adapter = engine_state.labware.get_has_quirk(
|
|
288
|
+
labware_id=tiprack_parent.labwareId, quirk="tiprackAdapterFor96Channel"
|
|
289
|
+
)
|
|
290
|
+
tiprack_height = engine_state.labware.get_dimensions(labware_id=labware_id).z
|
|
291
|
+
adapter_height = engine_state.labware.get_dimensions(
|
|
292
|
+
labware_id=tiprack_parent.labwareId
|
|
293
|
+
).z
|
|
294
|
+
if is_partial_config and tiprack_height < adapter_height:
|
|
295
|
+
raise PartialTipMovementNotAllowedError(
|
|
296
|
+
f"{tiprack_name} cannot be on an adapter taller than the tip rack"
|
|
297
|
+
f" when picking up fewer than 96 tips."
|
|
298
|
+
)
|
|
299
|
+
elif not is_partial_config and not is_96_ch_tiprack_adapter:
|
|
300
|
+
raise UnsuitableTiprackForPipetteMotion(
|
|
301
|
+
f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter"
|
|
302
|
+
f" in order to pick up or return all 96 tips simultaneously."
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
elif (
|
|
306
|
+
not is_partial_config
|
|
307
|
+
): # tiprack is not on adapter and pipette is in full config
|
|
308
|
+
raise UnsuitableTiprackForPipetteMotion(
|
|
309
|
+
f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter"
|
|
310
|
+
f" in order to pick up or return all 96 tips simultaneously."
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _is_within_pipette_extents(
|
|
315
|
+
engine_state: StateView,
|
|
316
|
+
pipette_id: str,
|
|
317
|
+
pipette_bounding_box_at_loc: Tuple[Point, Point, Point, Point],
|
|
318
|
+
) -> bool:
|
|
319
|
+
"""Whether a given point is within the extents of a configured pipette on the specified robot."""
|
|
320
|
+
channels = engine_state.pipettes.get_channels(pipette_id)
|
|
321
|
+
robot_extents = engine_state.geometry.absolute_deck_extents
|
|
322
|
+
(
|
|
323
|
+
pip_back_left_bound,
|
|
324
|
+
pip_front_right_bound,
|
|
325
|
+
pip_back_right_bound,
|
|
326
|
+
pip_front_left_bound,
|
|
327
|
+
) = pipette_bounding_box_at_loc
|
|
328
|
+
|
|
329
|
+
# Given the padding values accounted for against the deck extents,
|
|
330
|
+
# a pipette is within extents when all of the following are true:
|
|
331
|
+
|
|
332
|
+
# Each corner slot full pickup case:
|
|
333
|
+
# A1: Front right nozzle is within the rear and left-side padding limits
|
|
334
|
+
# D1: Back right nozzle is within the front and left-side padding limits
|
|
335
|
+
# A3 Front left nozzle is within the rear and right-side padding limits
|
|
336
|
+
# D3: Back left nozzle is within the front and right-side padding limits
|
|
337
|
+
# Thermocycler Column A2: Front right nozzle is within padding limits
|
|
338
|
+
|
|
339
|
+
if channels == 96:
|
|
340
|
+
return (
|
|
341
|
+
pip_front_right_bound.y
|
|
342
|
+
<= robot_extents.deck_extents.y + robot_extents.padding_rear
|
|
343
|
+
and pip_front_right_bound.x >= robot_extents.padding_left_side
|
|
344
|
+
and pip_back_right_bound.y >= robot_extents.padding_front
|
|
345
|
+
and pip_back_right_bound.x >= robot_extents.padding_left_side
|
|
346
|
+
and pip_front_left_bound.y
|
|
347
|
+
<= robot_extents.deck_extents.y + robot_extents.padding_rear
|
|
348
|
+
and pip_front_left_bound.x
|
|
349
|
+
<= robot_extents.deck_extents.x + robot_extents.padding_right_side
|
|
350
|
+
and pip_back_left_bound.y >= robot_extents.padding_front
|
|
351
|
+
and pip_back_left_bound.x
|
|
352
|
+
<= robot_extents.deck_extents.x + robot_extents.padding_right_side
|
|
353
|
+
)
|
|
354
|
+
# For 8ch pipettes we only check the rear and front extents
|
|
355
|
+
return (
|
|
356
|
+
pip_front_right_bound.y
|
|
357
|
+
<= robot_extents.deck_extents.y + robot_extents.padding_rear
|
|
358
|
+
and pip_back_right_bound.y >= robot_extents.padding_front
|
|
359
|
+
and pip_front_left_bound.y
|
|
360
|
+
<= robot_extents.deck_extents.y + robot_extents.padding_rear
|
|
361
|
+
and pip_back_left_bound.y >= robot_extents.padding_front
|
|
362
|
+
)
|