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
|
@@ -12,20 +12,24 @@ from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN
|
|
|
12
12
|
from opentrons_shared_data.deck.types import CutoutFixture
|
|
13
13
|
from opentrons_shared_data.pipette import PIPETTE_X_SPAN
|
|
14
14
|
from opentrons_shared_data.pipette.types import ChannelCount
|
|
15
|
+
from opentrons.protocols.models import LabwareDefinition
|
|
15
16
|
|
|
16
17
|
from .. import errors
|
|
17
18
|
from ..errors import (
|
|
18
19
|
LabwareNotLoadedOnLabwareError,
|
|
19
20
|
LabwareNotLoadedOnModuleError,
|
|
20
21
|
LabwareMovementNotAllowedError,
|
|
22
|
+
OperationLocationNotInWellError,
|
|
21
23
|
)
|
|
22
|
-
from ..resources import fixture_validation
|
|
24
|
+
from ..resources import fixture_validation, labware_validation
|
|
23
25
|
from ..types import (
|
|
24
26
|
OFF_DECK_LOCATION,
|
|
25
27
|
LoadedLabware,
|
|
26
28
|
LoadedModule,
|
|
27
29
|
WellLocation,
|
|
30
|
+
LiquidHandlingWellLocation,
|
|
28
31
|
DropTipWellLocation,
|
|
32
|
+
PickUpTipWellLocation,
|
|
29
33
|
WellOrigin,
|
|
30
34
|
DropTipWellOrigin,
|
|
31
35
|
WellOffset,
|
|
@@ -45,12 +49,18 @@ from ..types import (
|
|
|
45
49
|
AddressableOffsetVector,
|
|
46
50
|
StagingSlotLocation,
|
|
47
51
|
LabwareOffsetLocation,
|
|
52
|
+
ModuleModel,
|
|
48
53
|
)
|
|
49
54
|
from .config import Config
|
|
50
55
|
from .labware import LabwareView
|
|
56
|
+
from .wells import WellView
|
|
51
57
|
from .modules import ModuleView
|
|
52
58
|
from .pipettes import PipetteView
|
|
53
59
|
from .addressable_areas import AddressableAreaView
|
|
60
|
+
from .frustum_helpers import (
|
|
61
|
+
find_volume_at_well_height,
|
|
62
|
+
find_height_at_well_volume,
|
|
63
|
+
)
|
|
54
64
|
|
|
55
65
|
|
|
56
66
|
SLOT_WIDTH = 128
|
|
@@ -96,6 +106,7 @@ class GeometryView:
|
|
|
96
106
|
self,
|
|
97
107
|
config: Config,
|
|
98
108
|
labware_view: LabwareView,
|
|
109
|
+
well_view: WellView,
|
|
99
110
|
module_view: ModuleView,
|
|
100
111
|
pipette_view: PipetteView,
|
|
101
112
|
addressable_area_view: AddressableAreaView,
|
|
@@ -103,6 +114,7 @@ class GeometryView:
|
|
|
103
114
|
"""Initialize a GeometryView instance."""
|
|
104
115
|
self._config = config
|
|
105
116
|
self._labware = labware_view
|
|
117
|
+
self._wells = well_view
|
|
106
118
|
self._modules = module_view
|
|
107
119
|
self._pipettes = pipette_view
|
|
108
120
|
self._addressable_areas = addressable_area_view
|
|
@@ -250,27 +262,33 @@ class GeometryView:
|
|
|
250
262
|
return min_travel_z
|
|
251
263
|
|
|
252
264
|
def get_labware_parent_nominal_position(self, labware_id: str) -> Point:
|
|
253
|
-
"""Get the position of the labware's uncalibrated parent
|
|
265
|
+
"""Get the position of the labware's uncalibrated parent (deck slot, module, or another labware)."""
|
|
254
266
|
try:
|
|
255
|
-
|
|
267
|
+
addressable_area_name = self.get_ancestor_slot_name(labware_id).id
|
|
256
268
|
except errors.LocationIsStagingSlotError:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
269
|
+
addressable_area_name = self._get_staging_slot_name(labware_id)
|
|
270
|
+
except errors.LocationIsLidDockSlotError:
|
|
271
|
+
addressable_area_name = self._get_lid_dock_slot_name(labware_id)
|
|
272
|
+
parent_pos = self._addressable_areas.get_addressable_area_position(
|
|
273
|
+
addressable_area_name
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
offset_from_parent = self._get_offset_from_parent(
|
|
277
|
+
child_definition=self._labware.get_definition(labware_id),
|
|
278
|
+
parent=self._labware.get(labware_id).location,
|
|
279
|
+
)
|
|
261
280
|
|
|
262
281
|
return Point(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
282
|
+
parent_pos.x + offset_from_parent.x,
|
|
283
|
+
parent_pos.y + offset_from_parent.y,
|
|
284
|
+
parent_pos.z + offset_from_parent.z,
|
|
266
285
|
)
|
|
267
286
|
|
|
268
|
-
def
|
|
269
|
-
self,
|
|
287
|
+
def _get_offset_from_parent(
|
|
288
|
+
self, child_definition: LabwareDefinition, parent: LabwareLocation
|
|
270
289
|
) -> LabwareOffsetVector:
|
|
271
|
-
"""Gets the offset vector of a labware on the given location.
|
|
290
|
+
"""Gets the offset vector of a labware placed on the given location.
|
|
272
291
|
|
|
273
|
-
NOTE: Not to be confused with LPC offset.
|
|
274
292
|
- For labware on Deck Slot: returns an offset of (0, 0, 0)
|
|
275
293
|
- For labware on a Module: returns the nominal offset for the labware's position
|
|
276
294
|
when placed on the specified module (using slot-transformed labwareOffset
|
|
@@ -281,40 +299,42 @@ class GeometryView:
|
|
|
281
299
|
on modules as well as stacking overlaps.
|
|
282
300
|
Does not include module calibration offset or LPC offset.
|
|
283
301
|
"""
|
|
284
|
-
if isinstance(
|
|
302
|
+
if isinstance(parent, (AddressableAreaLocation, DeckSlotLocation)):
|
|
285
303
|
return LabwareOffsetVector(x=0, y=0, z=0)
|
|
286
|
-
elif isinstance(
|
|
287
|
-
module_id =
|
|
288
|
-
|
|
304
|
+
elif isinstance(parent, ModuleLocation):
|
|
305
|
+
module_id = parent.moduleId
|
|
306
|
+
module_to_child = self._modules.get_nominal_offset_to_child(
|
|
289
307
|
module_id=module_id, addressable_areas=self._addressable_areas
|
|
290
308
|
)
|
|
291
309
|
module_model = self._modules.get_connected_model(module_id)
|
|
292
310
|
stacking_overlap = self._labware.get_module_overlap_offsets(
|
|
293
|
-
|
|
311
|
+
child_definition, module_model
|
|
294
312
|
)
|
|
295
313
|
return LabwareOffsetVector(
|
|
296
|
-
x=
|
|
297
|
-
y=
|
|
298
|
-
z=
|
|
314
|
+
x=module_to_child.x - stacking_overlap.x,
|
|
315
|
+
y=module_to_child.y - stacking_overlap.y,
|
|
316
|
+
z=module_to_child.z - stacking_overlap.z,
|
|
317
|
+
)
|
|
318
|
+
elif isinstance(parent, OnLabwareLocation):
|
|
319
|
+
on_labware = self._labware.get(parent.labwareId)
|
|
320
|
+
on_labware_dimensions = self._labware.get_dimensions(
|
|
321
|
+
labware_id=on_labware.id
|
|
299
322
|
)
|
|
300
|
-
elif isinstance(labware_location, OnLabwareLocation):
|
|
301
|
-
on_labware = self._labware.get(labware_location.labwareId)
|
|
302
|
-
on_labware_dimensions = self._labware.get_dimensions(on_labware.id)
|
|
303
323
|
stacking_overlap = self._labware.get_labware_overlap_offsets(
|
|
304
|
-
|
|
324
|
+
definition=child_definition, below_labware_name=on_labware.loadName
|
|
305
325
|
)
|
|
306
326
|
labware_offset = LabwareOffsetVector(
|
|
307
327
|
x=stacking_overlap.x,
|
|
308
328
|
y=stacking_overlap.y,
|
|
309
329
|
z=on_labware_dimensions.z - stacking_overlap.z,
|
|
310
330
|
)
|
|
311
|
-
return labware_offset + self.
|
|
312
|
-
on_labware.id, on_labware.location
|
|
331
|
+
return labware_offset + self._get_offset_from_parent(
|
|
332
|
+
self._labware.get_definition(on_labware.id), on_labware.location
|
|
313
333
|
)
|
|
314
334
|
else:
|
|
315
335
|
raise errors.LabwareNotOnDeckError(
|
|
316
|
-
|
|
317
|
-
|
|
336
|
+
"Cannot access labware since it is not on the deck. "
|
|
337
|
+
"Either it has been loaded off-deck or its been moved off-deck."
|
|
318
338
|
)
|
|
319
339
|
|
|
320
340
|
def _normalize_module_calibration_offset(
|
|
@@ -405,11 +425,51 @@ class GeometryView:
|
|
|
405
425
|
z=origin_pos.z + cal_offset.z,
|
|
406
426
|
)
|
|
407
427
|
|
|
428
|
+
WellLocations = Union[
|
|
429
|
+
WellLocation, LiquidHandlingWellLocation, PickUpTipWellLocation
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
def validate_well_position(
|
|
433
|
+
self,
|
|
434
|
+
well_location: WellLocations,
|
|
435
|
+
z_offset: float,
|
|
436
|
+
pipette_id: Optional[str] = None,
|
|
437
|
+
) -> None:
|
|
438
|
+
"""Raise exception if operation location is not within well.
|
|
439
|
+
|
|
440
|
+
Primarily this checks if there is not enough liquid in a well to do meniscus-relative static aspiration.
|
|
441
|
+
"""
|
|
442
|
+
if well_location.origin == WellOrigin.MENISCUS:
|
|
443
|
+
assert pipette_id is not None
|
|
444
|
+
lld_min_height = self._pipettes.get_current_tip_lld_settings(
|
|
445
|
+
pipette_id=pipette_id
|
|
446
|
+
)
|
|
447
|
+
if z_offset < lld_min_height:
|
|
448
|
+
if isinstance(well_location, PickUpTipWellLocation):
|
|
449
|
+
raise OperationLocationNotInWellError(
|
|
450
|
+
f"Specifying {well_location.origin} with an offset of {well_location.offset} results in an operation location that could be below the bottom of the well"
|
|
451
|
+
)
|
|
452
|
+
else:
|
|
453
|
+
raise OperationLocationNotInWellError(
|
|
454
|
+
f"Specifying {well_location.origin} with an offset of {well_location.offset} and a volume offset of {well_location.volumeOffset} results in an operation location that could be below the bottom of the well"
|
|
455
|
+
)
|
|
456
|
+
elif z_offset < 0:
|
|
457
|
+
if isinstance(well_location, PickUpTipWellLocation):
|
|
458
|
+
raise OperationLocationNotInWellError(
|
|
459
|
+
f"Specifying {well_location.origin} with an offset of {well_location.offset} results in an operation location below the bottom of the well"
|
|
460
|
+
)
|
|
461
|
+
else:
|
|
462
|
+
raise OperationLocationNotInWellError(
|
|
463
|
+
f"Specifying {well_location.origin} with an offset of {well_location.offset} and a volume offset of {well_location.volumeOffset} results in an operation location below the bottom of the well"
|
|
464
|
+
)
|
|
465
|
+
|
|
408
466
|
def get_well_position(
|
|
409
467
|
self,
|
|
410
468
|
labware_id: str,
|
|
411
469
|
well_name: str,
|
|
412
|
-
well_location: Optional[
|
|
470
|
+
well_location: Optional[WellLocations] = None,
|
|
471
|
+
operation_volume: Optional[float] = None,
|
|
472
|
+
pipette_id: Optional[str] = None,
|
|
413
473
|
) -> Point:
|
|
414
474
|
"""Given relative well location in a labware, get absolute position."""
|
|
415
475
|
labware_pos = self.get_labware_position(labware_id)
|
|
@@ -419,10 +479,17 @@ class GeometryView:
|
|
|
419
479
|
offset = WellOffset(x=0, y=0, z=well_depth)
|
|
420
480
|
if well_location is not None:
|
|
421
481
|
offset = well_location.offset
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
482
|
+
offset_adjustment = self.get_well_offset_adjustment(
|
|
483
|
+
labware_id=labware_id,
|
|
484
|
+
well_name=well_name,
|
|
485
|
+
well_location=well_location,
|
|
486
|
+
well_depth=well_depth,
|
|
487
|
+
operation_volume=operation_volume,
|
|
488
|
+
)
|
|
489
|
+
offset = offset.copy(update={"z": offset.z + offset_adjustment})
|
|
490
|
+
self.validate_well_position(
|
|
491
|
+
well_location=well_location, z_offset=offset.z, pipette_id=pipette_id
|
|
492
|
+
)
|
|
426
493
|
|
|
427
494
|
return Point(
|
|
428
495
|
x=labware_pos.x + offset.x + well_def.x,
|
|
@@ -457,6 +524,41 @@ class GeometryView:
|
|
|
457
524
|
|
|
458
525
|
return WellLocation(offset=WellOffset(x=delta.x, y=delta.y, z=delta.z))
|
|
459
526
|
|
|
527
|
+
def get_relative_liquid_handling_well_location(
|
|
528
|
+
self,
|
|
529
|
+
labware_id: str,
|
|
530
|
+
well_name: str,
|
|
531
|
+
absolute_point: Point,
|
|
532
|
+
is_meniscus: Optional[bool] = None,
|
|
533
|
+
) -> LiquidHandlingWellLocation:
|
|
534
|
+
"""Given absolute position, get relative location of a well in a labware.
|
|
535
|
+
|
|
536
|
+
If is_meniscus is True, absolute_point will hold the z-offset in its z field.
|
|
537
|
+
"""
|
|
538
|
+
if is_meniscus:
|
|
539
|
+
return LiquidHandlingWellLocation(
|
|
540
|
+
origin=WellOrigin.MENISCUS,
|
|
541
|
+
offset=WellOffset(x=0, y=0, z=absolute_point.z),
|
|
542
|
+
)
|
|
543
|
+
else:
|
|
544
|
+
well_absolute_point = self.get_well_position(labware_id, well_name)
|
|
545
|
+
delta = absolute_point - well_absolute_point
|
|
546
|
+
return LiquidHandlingWellLocation(
|
|
547
|
+
offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
def get_relative_pick_up_tip_well_location(
|
|
551
|
+
self,
|
|
552
|
+
labware_id: str,
|
|
553
|
+
well_name: str,
|
|
554
|
+
absolute_point: Point,
|
|
555
|
+
) -> PickUpTipWellLocation:
|
|
556
|
+
"""Given absolute position, get relative location of a well in a labware."""
|
|
557
|
+
well_absolute_point = self.get_well_position(labware_id, well_name)
|
|
558
|
+
delta = absolute_point - well_absolute_point
|
|
559
|
+
|
|
560
|
+
return PickUpTipWellLocation(offset=WellOffset(x=delta.x, y=delta.y, z=delta.z))
|
|
561
|
+
|
|
460
562
|
def get_well_height(
|
|
461
563
|
self,
|
|
462
564
|
labware_id: str,
|
|
@@ -580,6 +682,14 @@ class GeometryView:
|
|
|
580
682
|
),
|
|
581
683
|
)
|
|
582
684
|
|
|
685
|
+
def convert_pick_up_tip_well_location(
|
|
686
|
+
self, well_location: PickUpTipWellLocation
|
|
687
|
+
) -> WellLocation:
|
|
688
|
+
"""Convert PickUpTipWellLocation to WellLocation."""
|
|
689
|
+
return WellLocation(
|
|
690
|
+
origin=WellOrigin(well_location.origin.value), offset=well_location.offset
|
|
691
|
+
)
|
|
692
|
+
|
|
583
693
|
# TODO(jbl 11-30-2023) fold this function into get_ancestor_slot_name see RSS-411
|
|
584
694
|
def _get_staging_slot_name(self, labware_id: str) -> str:
|
|
585
695
|
"""Get the staging slot name that the labware is on."""
|
|
@@ -596,10 +706,18 @@ class GeometryView:
|
|
|
596
706
|
"Cannot get staging slot name for labware not on staging slot."
|
|
597
707
|
)
|
|
598
708
|
|
|
599
|
-
def
|
|
709
|
+
def _get_lid_dock_slot_name(self, labware_id: str) -> str:
|
|
710
|
+
"""Get the staging slot name that the labware is on."""
|
|
711
|
+
labware_location = self._labware.get(labware_id).location
|
|
712
|
+
assert isinstance(labware_location, AddressableAreaLocation)
|
|
713
|
+
return labware_location.addressableAreaName
|
|
714
|
+
|
|
715
|
+
def get_ancestor_slot_name(
|
|
716
|
+
self, labware_id: str
|
|
717
|
+
) -> Union[DeckSlotName, StagingSlotName]:
|
|
600
718
|
"""Get the slot name of the labware or the module that the labware is on."""
|
|
601
719
|
labware = self._labware.get(labware_id)
|
|
602
|
-
slot_name: DeckSlotName
|
|
720
|
+
slot_name: Union[DeckSlotName, StagingSlotName]
|
|
603
721
|
|
|
604
722
|
if isinstance(labware.location, DeckSlotLocation):
|
|
605
723
|
slot_name = labware.location.slotName
|
|
@@ -611,13 +729,14 @@ class GeometryView:
|
|
|
611
729
|
slot_name = self.get_ancestor_slot_name(below_labware_id)
|
|
612
730
|
elif isinstance(labware.location, AddressableAreaLocation):
|
|
613
731
|
area_name = labware.location.addressableAreaName
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
raise errors.LocationIsStagingSlotError(
|
|
618
|
-
"Cannot get ancestor slot name for labware on staging slot."
|
|
732
|
+
if self._labware.is_absorbance_reader_lid(labware_id):
|
|
733
|
+
raise errors.LocationIsLidDockSlotError(
|
|
734
|
+
"Cannot get ancestor slot name for labware on lid dock slot."
|
|
619
735
|
)
|
|
620
|
-
|
|
736
|
+
elif fixture_validation.is_staging_slot(area_name):
|
|
737
|
+
slot_name = StagingSlotName.from_primitive(area_name)
|
|
738
|
+
else:
|
|
739
|
+
slot_name = DeckSlotName.from_primitive(area_name)
|
|
621
740
|
elif labware.location == OFF_DECK_LOCATION:
|
|
622
741
|
raise errors.LabwareNotOnDeckError(
|
|
623
742
|
f"Labware {labware_id} does not have a slot associated with it"
|
|
@@ -650,7 +769,7 @@ class GeometryView:
|
|
|
650
769
|
|
|
651
770
|
def get_labware_grip_point(
|
|
652
771
|
self,
|
|
653
|
-
|
|
772
|
+
labware_definition: LabwareDefinition,
|
|
654
773
|
location: Union[
|
|
655
774
|
DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
|
|
656
775
|
],
|
|
@@ -666,7 +785,7 @@ class GeometryView:
|
|
|
666
785
|
z-position of labware bottom + grip height from labware bottom.
|
|
667
786
|
"""
|
|
668
787
|
grip_height_from_labware_bottom = (
|
|
669
|
-
self._labware.get_grip_height_from_labware_bottom(
|
|
788
|
+
self._labware.get_grip_height_from_labware_bottom(labware_definition)
|
|
670
789
|
)
|
|
671
790
|
location_name: str
|
|
672
791
|
|
|
@@ -692,7 +811,9 @@ class GeometryView:
|
|
|
692
811
|
).slotName.id
|
|
693
812
|
else: # OnLabwareLocation
|
|
694
813
|
location_name = self.get_ancestor_slot_name(location.labwareId).id
|
|
695
|
-
labware_offset = self.
|
|
814
|
+
labware_offset = self._get_offset_from_parent(
|
|
815
|
+
child_definition=labware_definition, parent=location
|
|
816
|
+
)
|
|
696
817
|
# Get the calibrated offset if the on labware location is on top of a module, otherwise return empty one
|
|
697
818
|
cal_offset = self._get_calibrated_module_offset(location)
|
|
698
819
|
offset = LabwareOffsetVector(
|
|
@@ -711,7 +832,9 @@ class GeometryView:
|
|
|
711
832
|
)
|
|
712
833
|
|
|
713
834
|
def get_extra_waypoints(
|
|
714
|
-
self,
|
|
835
|
+
self,
|
|
836
|
+
location: Optional[CurrentPipetteLocation],
|
|
837
|
+
to_slot: Union[DeckSlotName, StagingSlotName],
|
|
715
838
|
) -> List[Tuple[float, float]]:
|
|
716
839
|
"""Get extra waypoints for movement if thermocycler needs to be dodged."""
|
|
717
840
|
if location is not None:
|
|
@@ -770,8 +893,10 @@ class GeometryView:
|
|
|
770
893
|
return maybe_labware or maybe_module or maybe_fixture or None
|
|
771
894
|
|
|
772
895
|
@staticmethod
|
|
773
|
-
def get_slot_column(slot_name: DeckSlotName) -> int:
|
|
896
|
+
def get_slot_column(slot_name: Union[DeckSlotName, StagingSlotName]) -> int:
|
|
774
897
|
"""Get the column number for the specified slot."""
|
|
898
|
+
if isinstance(slot_name, StagingSlotName):
|
|
899
|
+
return 4
|
|
775
900
|
row_col_name = slot_name.to_ot3_equivalent()
|
|
776
901
|
slot_name_match = WELL_NAME_PATTERN.match(row_col_name.value)
|
|
777
902
|
assert (
|
|
@@ -962,17 +1087,22 @@ class GeometryView:
|
|
|
962
1087
|
from_location: OnDeckLabwareLocation,
|
|
963
1088
|
to_location: OnDeckLabwareLocation,
|
|
964
1089
|
additional_offset_vector: LabwareMovementOffsetData,
|
|
1090
|
+
current_labware: LabwareDefinition,
|
|
965
1091
|
) -> LabwareMovementOffsetData:
|
|
966
1092
|
"""Calculate the final labware offset vector to use in labware movement."""
|
|
967
1093
|
pick_up_offset = (
|
|
968
1094
|
self.get_total_nominal_gripper_offset_for_move_type(
|
|
969
|
-
location=from_location,
|
|
1095
|
+
location=from_location,
|
|
1096
|
+
move_type=_GripperMoveType.PICK_UP_LABWARE,
|
|
1097
|
+
current_labware=current_labware,
|
|
970
1098
|
)
|
|
971
1099
|
+ additional_offset_vector.pickUpOffset
|
|
972
1100
|
)
|
|
973
1101
|
drop_offset = (
|
|
974
1102
|
self.get_total_nominal_gripper_offset_for_move_type(
|
|
975
|
-
location=to_location,
|
|
1103
|
+
location=to_location,
|
|
1104
|
+
move_type=_GripperMoveType.DROP_LABWARE,
|
|
1105
|
+
current_labware=current_labware,
|
|
976
1106
|
)
|
|
977
1107
|
+ additional_offset_vector.dropOffset
|
|
978
1108
|
)
|
|
@@ -1003,7 +1133,10 @@ class GeometryView:
|
|
|
1003
1133
|
return location
|
|
1004
1134
|
|
|
1005
1135
|
def get_total_nominal_gripper_offset_for_move_type(
|
|
1006
|
-
self,
|
|
1136
|
+
self,
|
|
1137
|
+
location: OnDeckLabwareLocation,
|
|
1138
|
+
move_type: _GripperMoveType,
|
|
1139
|
+
current_labware: LabwareDefinition,
|
|
1007
1140
|
) -> LabwareOffsetVector:
|
|
1008
1141
|
"""Get the total of the offsets to be used to pick up labware in its current location."""
|
|
1009
1142
|
if move_type == _GripperMoveType.PICK_UP_LABWARE:
|
|
@@ -1019,14 +1152,45 @@ class GeometryView:
|
|
|
1019
1152
|
location
|
|
1020
1153
|
)
|
|
1021
1154
|
ancestor = self._labware.get_parent_location(location.labwareId)
|
|
1155
|
+
extra_offset = LabwareOffsetVector(x=0, y=0, z=0)
|
|
1156
|
+
if (
|
|
1157
|
+
isinstance(ancestor, ModuleLocation)
|
|
1158
|
+
and self._modules._state.requested_model_by_id[ancestor.moduleId]
|
|
1159
|
+
== ModuleModel.THERMOCYCLER_MODULE_V2
|
|
1160
|
+
and labware_validation.validate_definition_is_lid(current_labware)
|
|
1161
|
+
):
|
|
1162
|
+
if "lidOffsets" in current_labware.gripperOffsets.keys():
|
|
1163
|
+
extra_offset = LabwareOffsetVector(
|
|
1164
|
+
x=current_labware.gripperOffsets[
|
|
1165
|
+
"lidOffsets"
|
|
1166
|
+
].pickUpOffset.x,
|
|
1167
|
+
y=current_labware.gripperOffsets[
|
|
1168
|
+
"lidOffsets"
|
|
1169
|
+
].pickUpOffset.y,
|
|
1170
|
+
z=current_labware.gripperOffsets[
|
|
1171
|
+
"lidOffsets"
|
|
1172
|
+
].pickUpOffset.z,
|
|
1173
|
+
)
|
|
1174
|
+
else:
|
|
1175
|
+
raise errors.LabwareOffsetDoesNotExistError(
|
|
1176
|
+
f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1022
1179
|
assert isinstance(
|
|
1023
|
-
ancestor,
|
|
1180
|
+
ancestor,
|
|
1181
|
+
(
|
|
1182
|
+
DeckSlotLocation,
|
|
1183
|
+
ModuleLocation,
|
|
1184
|
+
OnLabwareLocation,
|
|
1185
|
+
AddressableAreaLocation,
|
|
1186
|
+
),
|
|
1024
1187
|
), "No gripper offsets for off-deck labware"
|
|
1025
1188
|
return (
|
|
1026
1189
|
direct_parent_offset.pickUpOffset
|
|
1027
1190
|
+ self._nominal_gripper_offsets_for_location(
|
|
1028
1191
|
location=ancestor
|
|
1029
1192
|
).pickUpOffset
|
|
1193
|
+
+ extra_offset
|
|
1030
1194
|
)
|
|
1031
1195
|
else:
|
|
1032
1196
|
if isinstance(
|
|
@@ -1041,16 +1205,65 @@ class GeometryView:
|
|
|
1041
1205
|
location
|
|
1042
1206
|
)
|
|
1043
1207
|
ancestor = self._labware.get_parent_location(location.labwareId)
|
|
1208
|
+
extra_offset = LabwareOffsetVector(x=0, y=0, z=0)
|
|
1209
|
+
if (
|
|
1210
|
+
isinstance(ancestor, ModuleLocation)
|
|
1211
|
+
# todo(mm, 2024-11-06): Do not access private module state; only use public ModuleView methods.
|
|
1212
|
+
and self._modules._state.requested_model_by_id[ancestor.moduleId]
|
|
1213
|
+
== ModuleModel.THERMOCYCLER_MODULE_V2
|
|
1214
|
+
and labware_validation.validate_definition_is_lid(current_labware)
|
|
1215
|
+
):
|
|
1216
|
+
if "lidOffsets" in current_labware.gripperOffsets.keys():
|
|
1217
|
+
extra_offset = LabwareOffsetVector(
|
|
1218
|
+
x=current_labware.gripperOffsets[
|
|
1219
|
+
"lidOffsets"
|
|
1220
|
+
].pickUpOffset.x,
|
|
1221
|
+
y=current_labware.gripperOffsets[
|
|
1222
|
+
"lidOffsets"
|
|
1223
|
+
].pickUpOffset.y,
|
|
1224
|
+
z=current_labware.gripperOffsets[
|
|
1225
|
+
"lidOffsets"
|
|
1226
|
+
].pickUpOffset.z,
|
|
1227
|
+
)
|
|
1228
|
+
else:
|
|
1229
|
+
raise errors.LabwareOffsetDoesNotExistError(
|
|
1230
|
+
f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
|
|
1231
|
+
)
|
|
1232
|
+
|
|
1044
1233
|
assert isinstance(
|
|
1045
|
-
ancestor,
|
|
1234
|
+
ancestor,
|
|
1235
|
+
(
|
|
1236
|
+
DeckSlotLocation,
|
|
1237
|
+
ModuleLocation,
|
|
1238
|
+
OnLabwareLocation,
|
|
1239
|
+
AddressableAreaLocation,
|
|
1240
|
+
),
|
|
1046
1241
|
), "No gripper offsets for off-deck labware"
|
|
1047
1242
|
return (
|
|
1048
1243
|
direct_parent_offset.dropOffset
|
|
1049
1244
|
+ self._nominal_gripper_offsets_for_location(
|
|
1050
1245
|
location=ancestor
|
|
1051
1246
|
).dropOffset
|
|
1247
|
+
+ extra_offset
|
|
1052
1248
|
)
|
|
1053
1249
|
|
|
1250
|
+
# todo(mm, 2024-11-05): This may be incorrect because it does not take the following
|
|
1251
|
+
# offsets into account, which *are* taken into account for the actual gripper movement:
|
|
1252
|
+
#
|
|
1253
|
+
# * The pickup offset in the definition of the parent of the gripped labware.
|
|
1254
|
+
# * The "additional offset" or "user offset", e.g. the `pickUpOffset` and `dropOffset`
|
|
1255
|
+
# params in the `moveLabware` command.
|
|
1256
|
+
#
|
|
1257
|
+
# And this *does* take these extra offsets into account:
|
|
1258
|
+
#
|
|
1259
|
+
# * The labware's Labware Position Check offset
|
|
1260
|
+
#
|
|
1261
|
+
# For robustness, we should combine this with `get_gripper_labware_movement_waypoints()`.
|
|
1262
|
+
#
|
|
1263
|
+
# We should also be more explicit about which offsets act to move the gripper paddles
|
|
1264
|
+
# relative to the gripped labware, and which offsets act to change how the gripped
|
|
1265
|
+
# labware sits atop its parent. Those have different effects on how far the gripped
|
|
1266
|
+
# labware juts beyond the paddles while it's in transit.
|
|
1054
1267
|
def check_gripper_labware_tip_collision(
|
|
1055
1268
|
self,
|
|
1056
1269
|
gripper_homed_position_z: float,
|
|
@@ -1058,18 +1271,22 @@ class GeometryView:
|
|
|
1058
1271
|
current_location: OnDeckLabwareLocation,
|
|
1059
1272
|
) -> None:
|
|
1060
1273
|
"""Check for potential collision of tips against labware to be lifted."""
|
|
1061
|
-
|
|
1274
|
+
labware_definition = self._labware.get_definition(labware_id)
|
|
1062
1275
|
pipettes = self._pipettes.get_all()
|
|
1063
1276
|
for pipette in pipettes:
|
|
1277
|
+
# TODO(cb, 2024-01-22): Remove the 1 and 8 channel special case once we are doing X axis validation
|
|
1064
1278
|
if self._pipettes.get_channels(pipette.id) in [1, 8]:
|
|
1065
1279
|
return
|
|
1066
1280
|
|
|
1067
1281
|
tip = self._pipettes.get_attached_tip(pipette.id)
|
|
1068
1282
|
if tip:
|
|
1283
|
+
# NOTE: This call to get_labware_highest_z() uses the labware's LPC offset,
|
|
1284
|
+
# which is an inconsistency between this and the actual gripper movement.
|
|
1285
|
+
# See the todo comment above this function.
|
|
1069
1286
|
labware_top_z_when_gripped = gripper_homed_position_z + (
|
|
1070
1287
|
self.get_labware_highest_z(labware_id=labware_id)
|
|
1071
1288
|
- self.get_labware_grip_point(
|
|
1072
|
-
|
|
1289
|
+
labware_definition=labware_definition, location=current_location
|
|
1073
1290
|
).z
|
|
1074
1291
|
)
|
|
1075
1292
|
# TODO(cb, 2024-01-18): Utilizing the nozzle map and labware X coordinates verify if collisions will occur on the X axis (analysis will use hard coded data to measure from the gripper critical point to the pipette mount)
|
|
@@ -1077,7 +1294,7 @@ class GeometryView:
|
|
|
1077
1294
|
_PIPETTE_HOMED_POSITION_Z - tip.length
|
|
1078
1295
|
) < labware_top_z_when_gripped:
|
|
1079
1296
|
raise LabwareMovementNotAllowedError(
|
|
1080
|
-
f"Cannot move labware '{
|
|
1297
|
+
f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
|
|
1081
1298
|
)
|
|
1082
1299
|
return
|
|
1083
1300
|
|
|
@@ -1112,20 +1329,30 @@ class GeometryView:
|
|
|
1112
1329
|
"""
|
|
1113
1330
|
parent_location = self._labware.get_parent_location(labware_id)
|
|
1114
1331
|
assert isinstance(
|
|
1115
|
-
parent_location,
|
|
1332
|
+
parent_location,
|
|
1333
|
+
(
|
|
1334
|
+
DeckSlotLocation,
|
|
1335
|
+
ModuleLocation,
|
|
1336
|
+
AddressableAreaLocation,
|
|
1337
|
+
OnLabwareLocation,
|
|
1338
|
+
),
|
|
1116
1339
|
), "No gripper offsets for off-deck labware"
|
|
1117
1340
|
|
|
1118
1341
|
if isinstance(parent_location, DeckSlotLocation):
|
|
1119
1342
|
slot_name = parent_location.slotName
|
|
1343
|
+
elif isinstance(parent_location, AddressableAreaLocation):
|
|
1344
|
+
slot_name = self._addressable_areas.get_addressable_area_base_slot(
|
|
1345
|
+
parent_location.addressableAreaName
|
|
1346
|
+
)
|
|
1120
1347
|
else:
|
|
1121
1348
|
module_loc = self._modules.get_location(parent_location.moduleId)
|
|
1122
1349
|
slot_name = module_loc.slotName
|
|
1123
1350
|
|
|
1124
|
-
slot_based_offset = self._labware.
|
|
1351
|
+
slot_based_offset = self._labware.get_child_gripper_offsets(
|
|
1125
1352
|
labware_id=labware_id, slot_name=slot_name.to_ot3_equivalent()
|
|
1126
1353
|
)
|
|
1127
1354
|
|
|
1128
|
-
return slot_based_offset or self._labware.
|
|
1355
|
+
return slot_based_offset or self._labware.get_child_gripper_offsets(
|
|
1129
1356
|
labware_id=labware_id, slot_name=None
|
|
1130
1357
|
)
|
|
1131
1358
|
|
|
@@ -1173,3 +1400,162 @@ class GeometryView:
|
|
|
1173
1400
|
)
|
|
1174
1401
|
|
|
1175
1402
|
return None
|
|
1403
|
+
|
|
1404
|
+
def get_well_offset_adjustment(
|
|
1405
|
+
self,
|
|
1406
|
+
labware_id: str,
|
|
1407
|
+
well_name: str,
|
|
1408
|
+
well_location: WellLocations,
|
|
1409
|
+
well_depth: float,
|
|
1410
|
+
operation_volume: Optional[float] = None,
|
|
1411
|
+
) -> float:
|
|
1412
|
+
"""Return a z-axis distance that accounts for well handling height and operation volume.
|
|
1413
|
+
|
|
1414
|
+
Distance is with reference to the well bottom.
|
|
1415
|
+
"""
|
|
1416
|
+
# TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
|
|
1417
|
+
initial_handling_height = self.get_well_handling_height(
|
|
1418
|
+
labware_id=labware_id,
|
|
1419
|
+
well_name=well_name,
|
|
1420
|
+
well_location=well_location,
|
|
1421
|
+
well_depth=well_depth,
|
|
1422
|
+
)
|
|
1423
|
+
if isinstance(well_location, PickUpTipWellLocation):
|
|
1424
|
+
volume = 0.0
|
|
1425
|
+
elif isinstance(well_location.volumeOffset, float):
|
|
1426
|
+
volume = well_location.volumeOffset
|
|
1427
|
+
elif well_location.volumeOffset == "operationVolume":
|
|
1428
|
+
volume = operation_volume or 0.0
|
|
1429
|
+
|
|
1430
|
+
if volume:
|
|
1431
|
+
return self.get_well_height_after_volume(
|
|
1432
|
+
labware_id=labware_id,
|
|
1433
|
+
well_name=well_name,
|
|
1434
|
+
initial_height=initial_handling_height,
|
|
1435
|
+
volume=volume,
|
|
1436
|
+
)
|
|
1437
|
+
else:
|
|
1438
|
+
return initial_handling_height
|
|
1439
|
+
|
|
1440
|
+
def get_meniscus_height(
|
|
1441
|
+
self,
|
|
1442
|
+
labware_id: str,
|
|
1443
|
+
well_name: str,
|
|
1444
|
+
) -> float:
|
|
1445
|
+
"""Returns stored meniscus height in specified well."""
|
|
1446
|
+
well_liquid = self._wells.get_well_liquid_info(
|
|
1447
|
+
labware_id=labware_id, well_name=well_name
|
|
1448
|
+
)
|
|
1449
|
+
if (
|
|
1450
|
+
well_liquid.probed_height is not None
|
|
1451
|
+
and well_liquid.probed_height.height is not None
|
|
1452
|
+
):
|
|
1453
|
+
return well_liquid.probed_height.height
|
|
1454
|
+
elif (
|
|
1455
|
+
well_liquid.loaded_volume is not None
|
|
1456
|
+
and well_liquid.loaded_volume.volume is not None
|
|
1457
|
+
):
|
|
1458
|
+
return self.get_well_height_at_volume(
|
|
1459
|
+
labware_id=labware_id,
|
|
1460
|
+
well_name=well_name,
|
|
1461
|
+
volume=well_liquid.loaded_volume.volume,
|
|
1462
|
+
)
|
|
1463
|
+
elif (
|
|
1464
|
+
well_liquid.probed_volume is not None
|
|
1465
|
+
and well_liquid.probed_volume.volume is not None
|
|
1466
|
+
):
|
|
1467
|
+
return self.get_well_height_at_volume(
|
|
1468
|
+
labware_id=labware_id,
|
|
1469
|
+
well_name=well_name,
|
|
1470
|
+
volume=well_liquid.probed_volume.volume,
|
|
1471
|
+
)
|
|
1472
|
+
else:
|
|
1473
|
+
raise errors.LiquidHeightUnknownError(
|
|
1474
|
+
"Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS."
|
|
1475
|
+
)
|
|
1476
|
+
|
|
1477
|
+
def get_well_handling_height(
|
|
1478
|
+
self,
|
|
1479
|
+
labware_id: str,
|
|
1480
|
+
well_name: str,
|
|
1481
|
+
well_location: WellLocations,
|
|
1482
|
+
well_depth: float,
|
|
1483
|
+
) -> float:
|
|
1484
|
+
"""Return the handling height for a labware well (with reference to the well bottom)."""
|
|
1485
|
+
handling_height = 0.0
|
|
1486
|
+
if well_location.origin == WellOrigin.TOP:
|
|
1487
|
+
handling_height = well_depth
|
|
1488
|
+
elif well_location.origin == WellOrigin.CENTER:
|
|
1489
|
+
handling_height = well_depth / 2.0
|
|
1490
|
+
elif well_location.origin == WellOrigin.MENISCUS:
|
|
1491
|
+
handling_height = self.get_meniscus_height(
|
|
1492
|
+
labware_id=labware_id, well_name=well_name
|
|
1493
|
+
)
|
|
1494
|
+
return float(handling_height)
|
|
1495
|
+
|
|
1496
|
+
def get_well_height_after_volume(
|
|
1497
|
+
self, labware_id: str, well_name: str, initial_height: float, volume: float
|
|
1498
|
+
) -> float:
|
|
1499
|
+
"""Return the height of liquid in a labware well after a given volume has been handled.
|
|
1500
|
+
|
|
1501
|
+
This is given an initial handling height, with reference to the well bottom.
|
|
1502
|
+
"""
|
|
1503
|
+
well_geometry = self._labware.get_well_geometry(
|
|
1504
|
+
labware_id=labware_id, well_name=well_name
|
|
1505
|
+
)
|
|
1506
|
+
initial_volume = find_volume_at_well_height(
|
|
1507
|
+
target_height=initial_height, well_geometry=well_geometry
|
|
1508
|
+
)
|
|
1509
|
+
final_volume = initial_volume + volume
|
|
1510
|
+
return find_height_at_well_volume(
|
|
1511
|
+
target_volume=final_volume, well_geometry=well_geometry
|
|
1512
|
+
)
|
|
1513
|
+
|
|
1514
|
+
def get_well_height_at_volume(
|
|
1515
|
+
self, labware_id: str, well_name: str, volume: float
|
|
1516
|
+
) -> float:
|
|
1517
|
+
"""Convert well volume to height."""
|
|
1518
|
+
well_geometry = self._labware.get_well_geometry(labware_id, well_name)
|
|
1519
|
+
return find_height_at_well_volume(
|
|
1520
|
+
target_volume=volume, well_geometry=well_geometry
|
|
1521
|
+
)
|
|
1522
|
+
|
|
1523
|
+
def get_well_volume_at_height(
|
|
1524
|
+
self, labware_id: str, well_name: str, height: float
|
|
1525
|
+
) -> float:
|
|
1526
|
+
"""Convert well height to volume."""
|
|
1527
|
+
well_geometry = self._labware.get_well_geometry(labware_id, well_name)
|
|
1528
|
+
return find_volume_at_well_height(
|
|
1529
|
+
target_height=height, well_geometry=well_geometry
|
|
1530
|
+
)
|
|
1531
|
+
|
|
1532
|
+
def validate_dispense_volume_into_well(
|
|
1533
|
+
self,
|
|
1534
|
+
labware_id: str,
|
|
1535
|
+
well_name: str,
|
|
1536
|
+
well_location: WellLocations,
|
|
1537
|
+
volume: float,
|
|
1538
|
+
) -> None:
|
|
1539
|
+
"""Raise InvalidDispenseVolumeError if planned dispense volume will overflow well."""
|
|
1540
|
+
well_def = self._labware.get_well_definition(labware_id, well_name)
|
|
1541
|
+
well_volumetric_capacity = well_def.totalLiquidVolume
|
|
1542
|
+
if well_location.origin == WellOrigin.MENISCUS:
|
|
1543
|
+
# TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
|
|
1544
|
+
well_geometry = self._labware.get_well_geometry(labware_id, well_name)
|
|
1545
|
+
meniscus_height = self.get_meniscus_height(
|
|
1546
|
+
labware_id=labware_id, well_name=well_name
|
|
1547
|
+
)
|
|
1548
|
+
meniscus_volume = find_volume_at_well_height(
|
|
1549
|
+
target_height=meniscus_height, well_geometry=well_geometry
|
|
1550
|
+
)
|
|
1551
|
+
remaining_volume = well_volumetric_capacity - meniscus_volume
|
|
1552
|
+
if volume > remaining_volume:
|
|
1553
|
+
raise errors.InvalidDispenseVolumeError(
|
|
1554
|
+
f"Attempting to dispense {volume}µL of liquid into a well that can currently only hold {remaining_volume}µL (well {well_name} in labware_id: {labware_id})"
|
|
1555
|
+
)
|
|
1556
|
+
else:
|
|
1557
|
+
# TODO(pbm, 10-08-24): factor in well (LabwareStore) state volume
|
|
1558
|
+
if volume > well_volumetric_capacity:
|
|
1559
|
+
raise errors.InvalidDispenseVolumeError(
|
|
1560
|
+
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
|
+
)
|