opentrons 8.3.2a0__py2.py3-none-any.whl → 8.4.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.
Potentially problematic release.
This version of opentrons might be problematic. Click here for more details.
- opentrons/calibration_storage/ot2/mark_bad_calibration.py +2 -0
- opentrons/calibration_storage/ot2/tip_length.py +6 -6
- opentrons/config/advanced_settings.py +9 -11
- opentrons/config/feature_flags.py +0 -4
- opentrons/config/reset.py +7 -2
- opentrons/drivers/asyncio/communication/__init__.py +2 -0
- opentrons/drivers/asyncio/communication/async_serial.py +4 -0
- opentrons/drivers/asyncio/communication/errors.py +41 -8
- opentrons/drivers/asyncio/communication/serial_connection.py +36 -10
- opentrons/drivers/flex_stacker/__init__.py +9 -3
- opentrons/drivers/flex_stacker/abstract.py +140 -15
- opentrons/drivers/flex_stacker/driver.py +593 -47
- opentrons/drivers/flex_stacker/errors.py +64 -0
- opentrons/drivers/flex_stacker/simulator.py +222 -24
- opentrons/drivers/flex_stacker/types.py +211 -15
- opentrons/drivers/flex_stacker/utils.py +19 -0
- opentrons/execute.py +4 -2
- opentrons/hardware_control/api.py +5 -0
- opentrons/hardware_control/backends/flex_protocol.py +4 -0
- opentrons/hardware_control/backends/ot3controller.py +12 -1
- opentrons/hardware_control/backends/ot3simulator.py +3 -0
- opentrons/hardware_control/backends/subsystem_manager.py +8 -4
- opentrons/hardware_control/instruments/ot2/instrument_calibration.py +10 -6
- opentrons/hardware_control/instruments/ot3/pipette_handler.py +59 -6
- opentrons/hardware_control/modules/__init__.py +12 -1
- opentrons/hardware_control/modules/absorbance_reader.py +11 -9
- opentrons/hardware_control/modules/flex_stacker.py +498 -0
- opentrons/hardware_control/modules/heater_shaker.py +12 -10
- opentrons/hardware_control/modules/magdeck.py +5 -1
- opentrons/hardware_control/modules/tempdeck.py +5 -1
- opentrons/hardware_control/modules/thermocycler.py +15 -14
- opentrons/hardware_control/modules/types.py +191 -1
- opentrons/hardware_control/modules/utils.py +3 -0
- opentrons/hardware_control/motion_utilities.py +20 -0
- opentrons/hardware_control/ot3api.py +145 -15
- opentrons/hardware_control/protocols/liquid_handler.py +47 -1
- opentrons/hardware_control/types.py +6 -0
- opentrons/legacy_commands/commands.py +102 -5
- opentrons/legacy_commands/helpers.py +74 -1
- opentrons/legacy_commands/types.py +33 -2
- opentrons/protocol_api/__init__.py +2 -0
- opentrons/protocol_api/_liquid.py +39 -8
- opentrons/protocol_api/_liquid_properties.py +20 -19
- opentrons/protocol_api/_transfer_liquid_validation.py +91 -0
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/deck_conflict.py +11 -1
- opentrons/protocol_api/core/engine/instrument.py +1356 -107
- opentrons/protocol_api/core/engine/labware.py +8 -4
- opentrons/protocol_api/core/engine/load_labware_params.py +68 -10
- opentrons/protocol_api/core/engine/module_core.py +118 -2
- opentrons/protocol_api/core/engine/pipette_movement_conflict.py +6 -14
- opentrons/protocol_api/core/engine/protocol.py +253 -11
- opentrons/protocol_api/core/engine/stringify.py +19 -8
- opentrons/protocol_api/core/engine/transfer_components_executor.py +858 -0
- opentrons/protocol_api/core/engine/well.py +73 -5
- opentrons/protocol_api/core/instrument.py +71 -21
- opentrons/protocol_api/core/labware.py +6 -2
- opentrons/protocol_api/core/legacy/labware_offset_provider.py +7 -3
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +76 -49
- opentrons/protocol_api/core/legacy/legacy_labware_core.py +8 -4
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +36 -0
- opentrons/protocol_api/core/legacy/legacy_well_core.py +27 -2
- opentrons/protocol_api/core/legacy/load_info.py +4 -12
- opentrons/protocol_api/core/legacy/module_geometry.py +6 -1
- opentrons/protocol_api/core/legacy/well_geometry.py +3 -3
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +73 -23
- opentrons/protocol_api/core/module.py +43 -0
- opentrons/protocol_api/core/protocol.py +33 -0
- opentrons/protocol_api/core/well.py +23 -2
- opentrons/protocol_api/instrument_context.py +454 -150
- opentrons/protocol_api/labware.py +98 -50
- opentrons/protocol_api/module_contexts.py +140 -0
- opentrons/protocol_api/protocol_context.py +163 -19
- opentrons/protocol_api/validation.py +51 -41
- opentrons/protocol_engine/__init__.py +21 -2
- opentrons/protocol_engine/actions/actions.py +5 -5
- opentrons/protocol_engine/clients/sync_client.py +6 -0
- opentrons/protocol_engine/commands/__init__.py +66 -36
- opentrons/protocol_engine/commands/absorbance_reader/__init__.py +0 -1
- opentrons/protocol_engine/commands/air_gap_in_place.py +3 -2
- opentrons/protocol_engine/commands/aspirate.py +6 -2
- opentrons/protocol_engine/commands/aspirate_in_place.py +3 -1
- opentrons/protocol_engine/commands/aspirate_while_tracking.py +210 -0
- opentrons/protocol_engine/commands/blow_out.py +2 -0
- opentrons/protocol_engine/commands/blow_out_in_place.py +4 -1
- opentrons/protocol_engine/commands/command_unions.py +102 -33
- opentrons/protocol_engine/commands/configure_for_volume.py +3 -0
- opentrons/protocol_engine/commands/dispense.py +3 -1
- opentrons/protocol_engine/commands/dispense_in_place.py +3 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +204 -0
- opentrons/protocol_engine/commands/drop_tip.py +23 -1
- opentrons/protocol_engine/commands/flex_stacker/__init__.py +106 -0
- opentrons/protocol_engine/commands/flex_stacker/close_latch.py +72 -0
- opentrons/protocol_engine/commands/flex_stacker/common.py +15 -0
- opentrons/protocol_engine/commands/flex_stacker/empty.py +161 -0
- opentrons/protocol_engine/commands/flex_stacker/fill.py +164 -0
- opentrons/protocol_engine/commands/flex_stacker/open_latch.py +70 -0
- opentrons/protocol_engine/commands/flex_stacker/prepare_shuttle.py +112 -0
- opentrons/protocol_engine/commands/flex_stacker/retrieve.py +394 -0
- opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +190 -0
- opentrons/protocol_engine/commands/flex_stacker/store.py +291 -0
- opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
- opentrons/protocol_engine/commands/labware_handling_common.py +29 -0
- opentrons/protocol_engine/commands/liquid_probe.py +27 -13
- opentrons/protocol_engine/commands/load_labware.py +42 -39
- opentrons/protocol_engine/commands/load_lid.py +21 -13
- opentrons/protocol_engine/commands/load_lid_stack.py +130 -47
- opentrons/protocol_engine/commands/load_module.py +18 -17
- opentrons/protocol_engine/commands/load_pipette.py +3 -0
- opentrons/protocol_engine/commands/move_labware.py +139 -20
- opentrons/protocol_engine/commands/move_to_well.py +5 -11
- opentrons/protocol_engine/commands/pick_up_tip.py +5 -2
- opentrons/protocol_engine/commands/pipetting_common.py +159 -8
- opentrons/protocol_engine/commands/prepare_to_aspirate.py +15 -5
- opentrons/protocol_engine/commands/{evotip_dispense.py → pressure_dispense.py} +33 -34
- opentrons/protocol_engine/commands/reload_labware.py +6 -19
- opentrons/protocol_engine/commands/{evotip_seal_pipette.py → seal_pipette_to_tip.py} +97 -76
- opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +3 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +6 -1
- opentrons/protocol_engine/commands/{evotip_unseal_pipette.py → unseal_pipette_from_tip.py} +31 -40
- opentrons/protocol_engine/errors/__init__.py +10 -0
- opentrons/protocol_engine/errors/exceptions.py +62 -0
- opentrons/protocol_engine/execution/equipment.py +123 -106
- opentrons/protocol_engine/execution/labware_movement.py +8 -6
- opentrons/protocol_engine/execution/pipetting.py +235 -25
- opentrons/protocol_engine/execution/tip_handler.py +82 -32
- opentrons/protocol_engine/labware_offset_standardization.py +194 -0
- opentrons/protocol_engine/protocol_engine.py +22 -13
- opentrons/protocol_engine/resources/deck_configuration_provider.py +98 -2
- opentrons/protocol_engine/resources/deck_data_provider.py +1 -1
- opentrons/protocol_engine/resources/labware_data_provider.py +32 -12
- opentrons/protocol_engine/resources/labware_validation.py +7 -5
- opentrons/protocol_engine/slot_standardization.py +11 -23
- opentrons/protocol_engine/state/addressable_areas.py +84 -46
- opentrons/protocol_engine/state/frustum_helpers.py +36 -14
- opentrons/protocol_engine/state/geometry.py +892 -227
- opentrons/protocol_engine/state/labware.py +252 -55
- opentrons/protocol_engine/state/module_substates/__init__.py +4 -0
- opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +68 -0
- opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +22 -0
- opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +13 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +20 -0
- opentrons/protocol_engine/state/modules.py +210 -67
- opentrons/protocol_engine/state/pipettes.py +54 -0
- opentrons/protocol_engine/state/state.py +1 -1
- opentrons/protocol_engine/state/tips.py +14 -0
- opentrons/protocol_engine/state/update_types.py +180 -25
- opentrons/protocol_engine/state/wells.py +55 -9
- opentrons/protocol_engine/types/__init__.py +300 -0
- opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
- opentrons/protocol_engine/types/command_annotations.py +53 -0
- opentrons/protocol_engine/types/deck_configuration.py +72 -0
- opentrons/protocol_engine/types/execution.py +96 -0
- opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
- opentrons/protocol_engine/types/instrument.py +47 -0
- opentrons/protocol_engine/types/instrument_sensors.py +47 -0
- opentrons/protocol_engine/types/labware.py +111 -0
- opentrons/protocol_engine/types/labware_movement.py +22 -0
- opentrons/protocol_engine/types/labware_offset_location.py +111 -0
- opentrons/protocol_engine/types/labware_offset_vector.py +33 -0
- opentrons/protocol_engine/types/liquid.py +40 -0
- opentrons/protocol_engine/types/liquid_class.py +59 -0
- opentrons/protocol_engine/types/liquid_handling.py +13 -0
- opentrons/protocol_engine/types/liquid_level_detection.py +131 -0
- opentrons/protocol_engine/types/location.py +194 -0
- opentrons/protocol_engine/types/module.py +301 -0
- opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
- opentrons/protocol_engine/types/run_time_parameters.py +133 -0
- opentrons/protocol_engine/types/tip.py +18 -0
- opentrons/protocol_engine/types/util.py +21 -0
- opentrons/protocol_engine/types/well_position.py +124 -0
- opentrons/protocol_reader/extract_labware_definitions.py +7 -3
- opentrons/protocol_reader/file_format_validator.py +5 -3
- opentrons/protocol_runner/json_translator.py +4 -2
- opentrons/protocol_runner/legacy_command_mapper.py +6 -2
- opentrons/protocol_runner/run_orchestrator.py +4 -1
- opentrons/protocols/advanced_control/transfers/common.py +48 -1
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +204 -0
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/instrument.py +16 -3
- opentrons/protocols/labware.py +27 -23
- opentrons/protocols/models/__init__.py +0 -21
- opentrons/simulate.py +4 -2
- opentrons/types.py +20 -7
- opentrons/util/logging_config.py +94 -25
- opentrons/util/logging_queue_handler.py +61 -0
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/METADATA +4 -4
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/RECORD +192 -151
- opentrons/calibration_storage/ot2/models/defaults.py +0 -0
- opentrons/calibration_storage/ot3/models/defaults.py +0 -0
- opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
- opentrons/protocol_engine/types.py +0 -1311
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/LICENSE +0 -0
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/WHEEL +0 -0
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/entry_points.txt +0 -0
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/top_level.txt +0 -0
|
@@ -1,31 +1,49 @@
|
|
|
1
1
|
"""Geometry state getters."""
|
|
2
2
|
|
|
3
|
+
from logging import getLogger
|
|
3
4
|
import enum
|
|
4
5
|
from numpy import array, dot, double as npdouble
|
|
5
6
|
from numpy.typing import NDArray
|
|
6
|
-
from typing import Optional, List, Tuple, Union, cast, TypeVar, Dict
|
|
7
|
+
from typing import Optional, List, Tuple, Union, cast, TypeVar, Dict, Set
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from functools import cached_property
|
|
9
10
|
|
|
10
|
-
from opentrons.types import
|
|
11
|
+
from opentrons.types import (
|
|
12
|
+
Point,
|
|
13
|
+
DeckSlotName,
|
|
14
|
+
StagingSlotName,
|
|
15
|
+
MountType,
|
|
16
|
+
MeniscusTrackingTarget,
|
|
17
|
+
)
|
|
11
18
|
|
|
12
|
-
from opentrons_shared_data.errors.exceptions import
|
|
19
|
+
from opentrons_shared_data.errors.exceptions import (
|
|
20
|
+
InvalidStoredData,
|
|
21
|
+
PipetteLiquidNotFoundError,
|
|
22
|
+
)
|
|
13
23
|
from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN
|
|
24
|
+
from opentrons_shared_data.labware.labware_definition import LabwareDefinition
|
|
14
25
|
from opentrons_shared_data.deck.types import CutoutFixture
|
|
15
26
|
from opentrons_shared_data.pipette import PIPETTE_X_SPAN
|
|
16
|
-
from opentrons_shared_data.pipette.types import ChannelCount
|
|
17
|
-
from opentrons.protocols.models import LabwareDefinition
|
|
27
|
+
from opentrons_shared_data.pipette.types import ChannelCount, LabwareUri
|
|
18
28
|
|
|
19
29
|
from .. import errors
|
|
20
30
|
from ..errors import (
|
|
31
|
+
LabwareNotLoadedError,
|
|
21
32
|
LabwareNotLoadedOnLabwareError,
|
|
22
33
|
LabwareNotLoadedOnModuleError,
|
|
23
34
|
LabwareMovementNotAllowedError,
|
|
24
|
-
|
|
35
|
+
InvalidLabwarePositionError,
|
|
36
|
+
LabwareNotOnDeckError,
|
|
37
|
+
)
|
|
38
|
+
from ..errors.exceptions import InvalidLiquidHeightFound
|
|
39
|
+
from ..resources import (
|
|
40
|
+
fixture_validation,
|
|
41
|
+
labware_validation,
|
|
42
|
+
deck_configuration_provider,
|
|
25
43
|
)
|
|
26
|
-
from ..resources import fixture_validation, labware_validation
|
|
27
44
|
from ..types import (
|
|
28
45
|
OFF_DECK_LOCATION,
|
|
46
|
+
SYSTEM_LOCATION,
|
|
29
47
|
LoadedLabware,
|
|
30
48
|
LoadedModule,
|
|
31
49
|
WellLocation,
|
|
@@ -46,13 +64,30 @@ from ..types import (
|
|
|
46
64
|
CurrentPipetteLocation,
|
|
47
65
|
TipGeometry,
|
|
48
66
|
LabwareMovementOffsetData,
|
|
67
|
+
InStackerHopperLocation,
|
|
49
68
|
OnDeckLabwareLocation,
|
|
50
69
|
AddressableAreaLocation,
|
|
51
70
|
AddressableOffsetVector,
|
|
52
71
|
StagingSlotLocation,
|
|
53
|
-
|
|
72
|
+
LabwareOffsetLocationSequence,
|
|
73
|
+
OnModuleOffsetLocationSequenceComponent,
|
|
74
|
+
OnAddressableAreaOffsetLocationSequenceComponent,
|
|
75
|
+
OnLabwareOffsetLocationSequenceComponent,
|
|
76
|
+
OnLabwareLocationSequenceComponent,
|
|
54
77
|
ModuleModel,
|
|
78
|
+
PotentialCutoutFixture,
|
|
79
|
+
LabwareLocationSequence,
|
|
80
|
+
OnModuleLocationSequenceComponent,
|
|
81
|
+
OnAddressableAreaLocationSequenceComponent,
|
|
82
|
+
OnCutoutFixtureLocationSequenceComponent,
|
|
83
|
+
NotOnDeckLocationSequenceComponent,
|
|
84
|
+
AreaType,
|
|
85
|
+
labware_location_is_off_deck,
|
|
86
|
+
labware_location_is_system,
|
|
87
|
+
WellLocationType,
|
|
88
|
+
WellLocationFunction,
|
|
55
89
|
)
|
|
90
|
+
from ..types.liquid_level_detection import SimulatedProbeResult, LiquidTrackingType
|
|
56
91
|
from .config import Config
|
|
57
92
|
from .labware import LabwareView
|
|
58
93
|
from .wells import WellView
|
|
@@ -66,6 +101,7 @@ from .frustum_helpers import (
|
|
|
66
101
|
from ._well_math import wells_covered_by_pipette_configuration, nozzles_per_well
|
|
67
102
|
|
|
68
103
|
|
|
104
|
+
_LOG = getLogger(__name__)
|
|
69
105
|
SLOT_WIDTH = 128
|
|
70
106
|
_PIPETTE_HOMED_POSITION_Z = (
|
|
71
107
|
248.0 # Height of the bottom of the nozzle without the tip attached when homed
|
|
@@ -159,7 +195,8 @@ class GeometryView:
|
|
|
159
195
|
(
|
|
160
196
|
self._get_highest_z_from_labware_data(lw_data)
|
|
161
197
|
for lw_data in self._labware.get_all()
|
|
162
|
-
if lw_data.location
|
|
198
|
+
if lw_data.location not in [OFF_DECK_LOCATION, SYSTEM_LOCATION]
|
|
199
|
+
and not self._labware.get_labware_by_lid_id(lw_data.id)
|
|
163
200
|
),
|
|
164
201
|
default=0.0,
|
|
165
202
|
)
|
|
@@ -280,7 +317,6 @@ class GeometryView:
|
|
|
280
317
|
child_definition=self._labware.get_definition(labware_id),
|
|
281
318
|
parent=self._labware.get(labware_id).location,
|
|
282
319
|
)
|
|
283
|
-
|
|
284
320
|
return Point(
|
|
285
321
|
parent_pos.x + offset_from_parent.x,
|
|
286
322
|
parent_pos.y + offset_from_parent.y,
|
|
@@ -306,13 +342,66 @@ class GeometryView:
|
|
|
306
342
|
return LabwareOffsetVector(x=0, y=0, z=0)
|
|
307
343
|
elif isinstance(parent, ModuleLocation):
|
|
308
344
|
module_id = parent.moduleId
|
|
345
|
+
module_model = self._modules.get_connected_model(module_id)
|
|
346
|
+
stacking_overlap = self._labware.get_module_overlap_offsets(
|
|
347
|
+
child_definition, module_model
|
|
348
|
+
)
|
|
309
349
|
module_to_child = self._modules.get_nominal_offset_to_child(
|
|
310
350
|
module_id=module_id, addressable_areas=self._addressable_areas
|
|
311
351
|
)
|
|
352
|
+
return LabwareOffsetVector(
|
|
353
|
+
x=module_to_child.x - stacking_overlap.x,
|
|
354
|
+
y=module_to_child.y - stacking_overlap.y,
|
|
355
|
+
z=module_to_child.z - stacking_overlap.z,
|
|
356
|
+
)
|
|
357
|
+
elif isinstance(parent, OnLabwareLocation):
|
|
358
|
+
on_labware = self._labware.get(parent.labwareId)
|
|
359
|
+
on_labware_dimensions = self._labware.get_dimensions(
|
|
360
|
+
labware_id=on_labware.id
|
|
361
|
+
)
|
|
362
|
+
stacking_overlap = self._labware.get_labware_overlap_offsets(
|
|
363
|
+
definition=child_definition, below_labware_name=on_labware.loadName
|
|
364
|
+
)
|
|
365
|
+
labware_offset = LabwareOffsetVector(
|
|
366
|
+
x=stacking_overlap.x,
|
|
367
|
+
y=stacking_overlap.y,
|
|
368
|
+
z=on_labware_dimensions.z - stacking_overlap.z,
|
|
369
|
+
)
|
|
370
|
+
return labware_offset + self._get_offset_from_parent(
|
|
371
|
+
self._labware.get_definition(on_labware.id), on_labware.location
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
raise errors.LabwareNotOnDeckError(
|
|
375
|
+
"Cannot access labware since it is not on the deck. "
|
|
376
|
+
"Either it has been loaded off-deck or its been moved off-deck."
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def _get_offset_from_parent_addressable_area(
|
|
380
|
+
self, child_definition: LabwareDefinition, parent: LabwareLocation
|
|
381
|
+
) -> LabwareOffsetVector:
|
|
382
|
+
"""Gets the offset vector of a labware from its eventual parent addressable area.
|
|
383
|
+
|
|
384
|
+
This returns the sum of the offsets for any labware-on-labware pairs plus the
|
|
385
|
+
"base offset", which is (0, 0, 0) in all cases except for modules on the
|
|
386
|
+
OT-2. See
|
|
387
|
+
protocol_engine.state.modules.get_nominal_offset_to_child_from_addressable_area
|
|
388
|
+
for more.
|
|
389
|
+
|
|
390
|
+
This does not incorporate LPC offsets or module calibration offsets.
|
|
391
|
+
"""
|
|
392
|
+
if isinstance(parent, (AddressableAreaLocation, DeckSlotLocation)):
|
|
393
|
+
return LabwareOffsetVector(x=0, y=0, z=0)
|
|
394
|
+
elif isinstance(parent, ModuleLocation):
|
|
395
|
+
module_id = parent.moduleId
|
|
312
396
|
module_model = self._modules.get_connected_model(module_id)
|
|
313
397
|
stacking_overlap = self._labware.get_module_overlap_offsets(
|
|
314
398
|
child_definition, module_model
|
|
315
399
|
)
|
|
400
|
+
module_to_child = (
|
|
401
|
+
self._modules.get_nominal_offset_to_child_from_addressable_area(
|
|
402
|
+
module_id=module_id
|
|
403
|
+
)
|
|
404
|
+
)
|
|
316
405
|
return LabwareOffsetVector(
|
|
317
406
|
x=module_to_child.x - stacking_overlap.x,
|
|
318
407
|
y=module_to_child.y - stacking_overlap.y,
|
|
@@ -331,13 +420,13 @@ class GeometryView:
|
|
|
331
420
|
y=stacking_overlap.y,
|
|
332
421
|
z=on_labware_dimensions.z - stacking_overlap.z,
|
|
333
422
|
)
|
|
334
|
-
return labware_offset + self.
|
|
423
|
+
return labware_offset + self._get_offset_from_parent_addressable_area(
|
|
335
424
|
self._labware.get_definition(on_labware.id), on_labware.location
|
|
336
425
|
)
|
|
337
426
|
else:
|
|
338
427
|
raise errors.LabwareNotOnDeckError(
|
|
339
428
|
"Cannot access labware since it is not on the deck. "
|
|
340
|
-
"Either it has been loaded off-deck or
|
|
429
|
+
"Either it has been loaded off-deck or it has been moved off-deck."
|
|
341
430
|
)
|
|
342
431
|
|
|
343
432
|
def _normalize_module_calibration_offset(
|
|
@@ -388,7 +477,11 @@ class GeometryView:
|
|
|
388
477
|
elif isinstance(location, OnLabwareLocation):
|
|
389
478
|
labware_data = self._labware.get(location.labwareId)
|
|
390
479
|
return self._get_calibrated_module_offset(labware_data.location)
|
|
391
|
-
elif
|
|
480
|
+
elif (
|
|
481
|
+
location == OFF_DECK_LOCATION
|
|
482
|
+
or location == SYSTEM_LOCATION
|
|
483
|
+
or isinstance(location, InStackerHopperLocation)
|
|
484
|
+
):
|
|
392
485
|
raise errors.LabwareNotOnDeckError(
|
|
393
486
|
"Labware does not have a slot or module associated with it"
|
|
394
487
|
" since it is no longer on the deck."
|
|
@@ -421,56 +514,59 @@ class GeometryView:
|
|
|
421
514
|
"""Get the calibrated origin of the labware."""
|
|
422
515
|
origin_pos = self.get_labware_origin_position(labware_id)
|
|
423
516
|
cal_offset = self._labware.get_labware_offset_vector(labware_id)
|
|
424
|
-
|
|
425
517
|
return Point(
|
|
426
518
|
x=origin_pos.x + cal_offset.x,
|
|
427
519
|
y=origin_pos.y + cal_offset.y,
|
|
428
520
|
z=origin_pos.z + cal_offset.z,
|
|
429
521
|
)
|
|
430
522
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
523
|
+
def _validate_well_position(
|
|
524
|
+
self,
|
|
525
|
+
target_height: LiquidTrackingType, # height in mm inside a well relative to the bottom
|
|
526
|
+
well_max_height: float,
|
|
527
|
+
pipette_id: str,
|
|
528
|
+
) -> LiquidTrackingType:
|
|
529
|
+
"""If well offset would be outside the bounds of a well, silently bring it back to the boundary."""
|
|
530
|
+
if isinstance(target_height, SimulatedProbeResult):
|
|
531
|
+
return target_height
|
|
532
|
+
lld_min_height = self._pipettes.get_current_tip_lld_settings(
|
|
533
|
+
pipette_id=pipette_id
|
|
534
|
+
)
|
|
535
|
+
if target_height < lld_min_height:
|
|
536
|
+
target_height = lld_min_height
|
|
537
|
+
elif target_height > well_max_height:
|
|
538
|
+
target_height = well_max_height
|
|
539
|
+
return target_height
|
|
434
540
|
|
|
435
|
-
def
|
|
541
|
+
def validate_probed_height(
|
|
436
542
|
self,
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
pipette_id:
|
|
543
|
+
labware_id: str,
|
|
544
|
+
well_name: str,
|
|
545
|
+
pipette_id: str,
|
|
546
|
+
probed_height: LiquidTrackingType,
|
|
440
547
|
) -> None:
|
|
441
|
-
"""Raise
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
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"
|
|
458
|
-
)
|
|
459
|
-
elif z_offset < 0:
|
|
460
|
-
if isinstance(well_location, PickUpTipWellLocation):
|
|
461
|
-
raise OperationLocationNotInWellError(
|
|
462
|
-
f"Specifying {well_location.origin} with an offset of {well_location.offset} results in an operation location below the bottom of the well"
|
|
463
|
-
)
|
|
464
|
-
else:
|
|
465
|
-
raise OperationLocationNotInWellError(
|
|
466
|
-
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"
|
|
467
|
-
)
|
|
548
|
+
"""Raise an error if a probed liquid height is not within well bounds."""
|
|
549
|
+
if isinstance(probed_height, SimulatedProbeResult):
|
|
550
|
+
return
|
|
551
|
+
lld_min_height = self._pipettes.get_current_tip_lld_settings(
|
|
552
|
+
pipette_id=pipette_id
|
|
553
|
+
)
|
|
554
|
+
well_def = self._labware.get_well_definition(labware_id, well_name)
|
|
555
|
+
well_depth = well_def.depth
|
|
556
|
+
if probed_height < lld_min_height:
|
|
557
|
+
raise PipetteLiquidNotFoundError(
|
|
558
|
+
f"Liquid Height of {probed_height} mm is lower minumum allowed lld height {lld_min_height} mm."
|
|
559
|
+
)
|
|
560
|
+
if probed_height > well_depth:
|
|
561
|
+
raise PipetteLiquidNotFoundError(
|
|
562
|
+
f"Liquid Height of {probed_height} mm is greater than maximum well height {well_depth} mm."
|
|
563
|
+
)
|
|
468
564
|
|
|
469
565
|
def get_well_position(
|
|
470
566
|
self,
|
|
471
567
|
labware_id: str,
|
|
472
568
|
well_name: str,
|
|
473
|
-
well_location: Optional[
|
|
569
|
+
well_location: Optional[WellLocationType] = None,
|
|
474
570
|
operation_volume: Optional[float] = None,
|
|
475
571
|
pipette_id: Optional[str] = None,
|
|
476
572
|
) -> Point:
|
|
@@ -481,19 +577,17 @@ class GeometryView:
|
|
|
481
577
|
|
|
482
578
|
offset = WellOffset(x=0, y=0, z=well_depth)
|
|
483
579
|
if well_location is not None:
|
|
484
|
-
offset = well_location.offset
|
|
580
|
+
offset = well_location.offset # location of the bottom of the well
|
|
485
581
|
offset_adjustment = self.get_well_offset_adjustment(
|
|
486
582
|
labware_id=labware_id,
|
|
487
583
|
well_name=well_name,
|
|
488
584
|
well_location=well_location,
|
|
489
585
|
well_depth=well_depth,
|
|
490
586
|
operation_volume=operation_volume,
|
|
587
|
+
pipette_id=pipette_id,
|
|
491
588
|
)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
well_location=well_location, z_offset=offset.z, pipette_id=pipette_id
|
|
495
|
-
)
|
|
496
|
-
|
|
589
|
+
if not isinstance(offset_adjustment, SimulatedProbeResult):
|
|
590
|
+
offset = offset.model_copy(update={"z": offset.z + offset_adjustment})
|
|
497
591
|
return Point(
|
|
498
592
|
x=labware_pos.x + offset.x + well_def.x,
|
|
499
593
|
y=labware_pos.y + offset.y + well_def.y,
|
|
@@ -515,52 +609,65 @@ class GeometryView:
|
|
|
515
609
|
z=parent_pos.z + origin_offset.z + well_def.z + well_def.depth,
|
|
516
610
|
)
|
|
517
611
|
|
|
518
|
-
def
|
|
612
|
+
def _get_relative_liquid_handling_well_location(
|
|
519
613
|
self,
|
|
520
614
|
labware_id: str,
|
|
521
615
|
well_name: str,
|
|
522
616
|
absolute_point: Point,
|
|
523
|
-
|
|
617
|
+
delta: Point,
|
|
618
|
+
meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
|
|
619
|
+
) -> Tuple[WellLocationType, bool]:
|
|
524
620
|
"""Given absolute position, get relative location of a well in a labware."""
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
return WellLocation(offset=WellOffset(x=delta.x, y=delta.y, z=delta.z))
|
|
529
|
-
|
|
530
|
-
def get_relative_liquid_handling_well_location(
|
|
531
|
-
self,
|
|
532
|
-
labware_id: str,
|
|
533
|
-
well_name: str,
|
|
534
|
-
absolute_point: Point,
|
|
535
|
-
is_meniscus: Optional[bool] = None,
|
|
536
|
-
) -> LiquidHandlingWellLocation:
|
|
537
|
-
"""Given absolute position, get relative location of a well in a labware.
|
|
538
|
-
|
|
539
|
-
If is_meniscus is True, absolute_point will hold the z-offset in its z field.
|
|
540
|
-
"""
|
|
541
|
-
if is_meniscus:
|
|
542
|
-
return LiquidHandlingWellLocation(
|
|
621
|
+
dynamic_liquid_tracking = False
|
|
622
|
+
if meniscus_tracking:
|
|
623
|
+
location = LiquidHandlingWellLocation(
|
|
543
624
|
origin=WellOrigin.MENISCUS,
|
|
544
625
|
offset=WellOffset(x=0, y=0, z=absolute_point.z),
|
|
545
626
|
)
|
|
627
|
+
# TODO(cm): handle operationVolume being a float other than 0
|
|
628
|
+
if meniscus_tracking == MeniscusTrackingTarget.END:
|
|
629
|
+
location.volumeOffset = "operationVolume"
|
|
630
|
+
elif meniscus_tracking == MeniscusTrackingTarget.DYNAMIC:
|
|
631
|
+
dynamic_liquid_tracking = True
|
|
546
632
|
else:
|
|
547
|
-
|
|
548
|
-
delta = absolute_point - well_absolute_point
|
|
549
|
-
return LiquidHandlingWellLocation(
|
|
633
|
+
location = LiquidHandlingWellLocation(
|
|
550
634
|
offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)
|
|
551
635
|
)
|
|
636
|
+
return location, dynamic_liquid_tracking
|
|
552
637
|
|
|
553
|
-
def
|
|
638
|
+
def get_relative_well_location(
|
|
554
639
|
self,
|
|
555
640
|
labware_id: str,
|
|
556
641
|
well_name: str,
|
|
557
642
|
absolute_point: Point,
|
|
558
|
-
|
|
643
|
+
location_type: WellLocationFunction,
|
|
644
|
+
meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
|
|
645
|
+
) -> Tuple[WellLocationType, bool]:
|
|
559
646
|
"""Given absolute position, get relative location of a well in a labware."""
|
|
560
647
|
well_absolute_point = self.get_well_position(labware_id, well_name)
|
|
561
648
|
delta = absolute_point - well_absolute_point
|
|
562
|
-
|
|
563
|
-
|
|
649
|
+
match location_type:
|
|
650
|
+
case WellLocationFunction.BASE | WellLocationFunction.DROP_TIP:
|
|
651
|
+
return (
|
|
652
|
+
WellLocation(offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)),
|
|
653
|
+
False,
|
|
654
|
+
)
|
|
655
|
+
case WellLocationFunction.PICK_UP_TIP:
|
|
656
|
+
return (
|
|
657
|
+
PickUpTipWellLocation(
|
|
658
|
+
offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)
|
|
659
|
+
),
|
|
660
|
+
False,
|
|
661
|
+
)
|
|
662
|
+
case WellLocationFunction.LIQUID_HANDLING:
|
|
663
|
+
return self._get_relative_liquid_handling_well_location(
|
|
664
|
+
labware_id=labware_id,
|
|
665
|
+
well_name=well_name,
|
|
666
|
+
absolute_point=absolute_point,
|
|
667
|
+
delta=delta,
|
|
668
|
+
meniscus_tracking=meniscus_tracking,
|
|
669
|
+
)
|
|
670
|
+
return NotImplemented
|
|
564
671
|
|
|
565
672
|
def get_well_height(
|
|
566
673
|
self,
|
|
@@ -635,7 +742,7 @@ class GeometryView:
|
|
|
635
742
|
|
|
636
743
|
return TipGeometry(
|
|
637
744
|
length=effective_length,
|
|
638
|
-
diameter=well_def.diameter,
|
|
745
|
+
diameter=well_def.diameter,
|
|
639
746
|
# TODO(mc, 2020-11-12): WellDefinition type says totalLiquidVolume
|
|
640
747
|
# is a float, but hardware controller expects an int
|
|
641
748
|
volume=int(well_def.totalLiquidVolume),
|
|
@@ -647,6 +754,7 @@ class GeometryView:
|
|
|
647
754
|
labware_id: str,
|
|
648
755
|
well_location: DropTipWellLocation,
|
|
649
756
|
partially_configured: bool = False,
|
|
757
|
+
override_default_offset: float | None = None,
|
|
650
758
|
) -> WellLocation:
|
|
651
759
|
"""Get tip drop location given labware and hardware pipette.
|
|
652
760
|
|
|
@@ -665,8 +773,9 @@ class GeometryView:
|
|
|
665
773
|
origin=WellOrigin(well_location.origin.value),
|
|
666
774
|
offset=well_location.offset,
|
|
667
775
|
)
|
|
668
|
-
|
|
669
|
-
|
|
776
|
+
if override_default_offset is not None:
|
|
777
|
+
z_offset = override_default_offset
|
|
778
|
+
elif self._labware.get_definition(labware_id).parameters.isTiprack:
|
|
670
779
|
z_offset = self._labware.get_tip_drop_z_offset(
|
|
671
780
|
labware_id=labware_id,
|
|
672
781
|
length_scale=self._pipettes.get_return_tip_scale(pipette_id),
|
|
@@ -721,7 +830,6 @@ class GeometryView:
|
|
|
721
830
|
"""Get the slot name of the labware or the module that the labware is on."""
|
|
722
831
|
labware = self._labware.get(labware_id)
|
|
723
832
|
slot_name: Union[DeckSlotName, StagingSlotName]
|
|
724
|
-
|
|
725
833
|
if isinstance(labware.location, DeckSlotLocation):
|
|
726
834
|
slot_name = labware.location.slotName
|
|
727
835
|
elif isinstance(labware.location, ModuleLocation):
|
|
@@ -745,31 +853,188 @@ class GeometryView:
|
|
|
745
853
|
f"Labware {labware_id} does not have a slot associated with it"
|
|
746
854
|
f" since it is no longer on the deck."
|
|
747
855
|
)
|
|
856
|
+
else:
|
|
857
|
+
_LOG.error(
|
|
858
|
+
f"Unhandled location type in get_ancestor_slot_name: {labware.location}"
|
|
859
|
+
)
|
|
860
|
+
raise errors.InvalidLabwarePositionError(
|
|
861
|
+
f"Cannot get ancestor slot of {self._labware.get_display_name(labware_id)} with location {labware.location}"
|
|
862
|
+
)
|
|
748
863
|
|
|
749
864
|
return slot_name
|
|
750
865
|
|
|
866
|
+
def get_ancestor_addressable_area_name(self, labware_id: str) -> str:
|
|
867
|
+
"""Get the name of the addressable area the labware is eventually on."""
|
|
868
|
+
labware = self._labware.get(labware_id)
|
|
869
|
+
original_display_name = self._labware.get_display_name(labware_id)
|
|
870
|
+
seen: Set[str] = set((labware_id,))
|
|
871
|
+
while isinstance(labware.location, OnLabwareLocation):
|
|
872
|
+
labware = self._labware.get(labware.location.labwareId)
|
|
873
|
+
if labware.id in seen:
|
|
874
|
+
raise InvalidLabwarePositionError(
|
|
875
|
+
f"Cycle detected in labware positioning for {original_display_name}"
|
|
876
|
+
)
|
|
877
|
+
seen.add(labware.id)
|
|
878
|
+
if isinstance(labware.location, DeckSlotLocation):
|
|
879
|
+
return labware.location.slotName.id
|
|
880
|
+
elif isinstance(labware.location, AddressableAreaLocation):
|
|
881
|
+
return labware.location.addressableAreaName
|
|
882
|
+
elif isinstance(labware.location, ModuleLocation):
|
|
883
|
+
return self._modules.get_provided_addressable_area(
|
|
884
|
+
labware.location.moduleId
|
|
885
|
+
)
|
|
886
|
+
else:
|
|
887
|
+
raise LabwareNotOnDeckError(
|
|
888
|
+
f"Labware {original_display_name} is not loaded on deck",
|
|
889
|
+
details={"eventual-location": repr(labware.location)},
|
|
890
|
+
)
|
|
891
|
+
|
|
751
892
|
def ensure_location_not_occupied(
|
|
752
|
-
self,
|
|
893
|
+
self,
|
|
894
|
+
location: _LabwareLocation,
|
|
895
|
+
desired_addressable_area: Optional[str] = None,
|
|
753
896
|
) -> _LabwareLocation:
|
|
754
897
|
"""Ensure that the location does not already have either Labware or a Module in it."""
|
|
755
|
-
#
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
898
|
+
# Collect set of existing fixtures, if any
|
|
899
|
+
existing_fixtures = self._get_potential_fixtures_for_location_occupation(
|
|
900
|
+
location
|
|
901
|
+
)
|
|
902
|
+
potential_fixtures = (
|
|
903
|
+
self._get_potential_fixtures_for_location_occupation(
|
|
904
|
+
AddressableAreaLocation(addressableAreaName=desired_addressable_area)
|
|
905
|
+
)
|
|
906
|
+
if desired_addressable_area is not None
|
|
907
|
+
else None
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
# Handle the checking conflict on an incoming fixture
|
|
911
|
+
if potential_fixtures is not None and isinstance(location, DeckSlotLocation):
|
|
912
|
+
if (
|
|
913
|
+
existing_fixtures is not None
|
|
914
|
+
and not any(
|
|
915
|
+
location.slotName.id in fixture.provided_addressable_areas
|
|
916
|
+
for fixture in potential_fixtures[1].intersection(
|
|
917
|
+
existing_fixtures[1]
|
|
918
|
+
)
|
|
919
|
+
)
|
|
920
|
+
) or (
|
|
921
|
+
self._labware.get_by_slot(location.slotName) is not None
|
|
922
|
+
and not any(
|
|
923
|
+
location.slotName.id in fixture.provided_addressable_areas
|
|
924
|
+
for fixture in potential_fixtures[1]
|
|
925
|
+
)
|
|
926
|
+
):
|
|
927
|
+
self._labware.raise_if_labware_in_location(location)
|
|
928
|
+
|
|
929
|
+
else:
|
|
930
|
+
self._modules.raise_if_module_in_location(location)
|
|
931
|
+
|
|
932
|
+
# Otherwise handle standard conflict checking
|
|
933
|
+
else:
|
|
934
|
+
if isinstance(
|
|
935
|
+
location,
|
|
936
|
+
(
|
|
937
|
+
DeckSlotLocation,
|
|
938
|
+
ModuleLocation,
|
|
939
|
+
OnLabwareLocation,
|
|
940
|
+
AddressableAreaLocation,
|
|
941
|
+
),
|
|
942
|
+
):
|
|
943
|
+
self._labware.raise_if_labware_in_location(location)
|
|
944
|
+
|
|
945
|
+
area = (
|
|
946
|
+
location.slotName.id
|
|
947
|
+
if isinstance(location, DeckSlotLocation)
|
|
948
|
+
else (
|
|
949
|
+
location.addressableAreaName
|
|
950
|
+
if isinstance(location, AddressableAreaLocation)
|
|
951
|
+
else None
|
|
952
|
+
)
|
|
953
|
+
)
|
|
954
|
+
if area is not None and (
|
|
955
|
+
existing_fixtures is None
|
|
956
|
+
or not any(
|
|
957
|
+
area in fixture.provided_addressable_areas
|
|
958
|
+
for fixture in existing_fixtures[1]
|
|
959
|
+
)
|
|
960
|
+
):
|
|
961
|
+
if isinstance(location, DeckSlotLocation):
|
|
962
|
+
self._modules.raise_if_module_in_location(location)
|
|
963
|
+
elif isinstance(location, AddressableAreaLocation):
|
|
964
|
+
self._modules.raise_if_module_in_location(
|
|
965
|
+
DeckSlotLocation(
|
|
966
|
+
slotName=self._addressable_areas.get_addressable_area_base_slot(
|
|
967
|
+
location.addressableAreaName
|
|
968
|
+
)
|
|
969
|
+
)
|
|
970
|
+
)
|
|
971
|
+
|
|
771
972
|
return location
|
|
772
973
|
|
|
974
|
+
def _get_potential_fixtures_for_location_occupation(
|
|
975
|
+
self, location: _LabwareLocation
|
|
976
|
+
) -> Tuple[str, Set[PotentialCutoutFixture]] | None:
|
|
977
|
+
loc: DeckSlotLocation | AddressableAreaLocation | None = None
|
|
978
|
+
if isinstance(location, AddressableAreaLocation):
|
|
979
|
+
# Convert the addressable area into a staging slot if applicable
|
|
980
|
+
slots = StagingSlotName._value2member_map_
|
|
981
|
+
for slot in slots:
|
|
982
|
+
if location.addressableAreaName == slot:
|
|
983
|
+
loc = DeckSlotLocation(
|
|
984
|
+
slotName=DeckSlotName(location.addressableAreaName[0] + "3")
|
|
985
|
+
)
|
|
986
|
+
if loc is None:
|
|
987
|
+
loc = location
|
|
988
|
+
elif isinstance(location, DeckSlotLocation):
|
|
989
|
+
loc = location
|
|
990
|
+
|
|
991
|
+
if isinstance(loc, DeckSlotLocation):
|
|
992
|
+
module = self._modules.get_by_slot(loc.slotName)
|
|
993
|
+
if module is not None and self._config.robot_type != "OT-2 Standard":
|
|
994
|
+
fixtures = deck_configuration_provider.get_potential_cutout_fixtures(
|
|
995
|
+
addressable_area_name=self._modules.ensure_and_convert_module_fixture_location(
|
|
996
|
+
deck_slot=loc.slotName,
|
|
997
|
+
model=module.model,
|
|
998
|
+
),
|
|
999
|
+
deck_definition=self._addressable_areas.deck_definition,
|
|
1000
|
+
)
|
|
1001
|
+
else:
|
|
1002
|
+
fixtures = None
|
|
1003
|
+
elif isinstance(loc, AddressableAreaLocation):
|
|
1004
|
+
fixtures = deck_configuration_provider.get_potential_cutout_fixtures(
|
|
1005
|
+
addressable_area_name=loc.addressableAreaName,
|
|
1006
|
+
deck_definition=self._addressable_areas.deck_definition,
|
|
1007
|
+
)
|
|
1008
|
+
else:
|
|
1009
|
+
fixtures = None
|
|
1010
|
+
return fixtures
|
|
1011
|
+
|
|
1012
|
+
def _get_potential_disposal_location_cutout_fixtures(
|
|
1013
|
+
self, slot_name: DeckSlotName
|
|
1014
|
+
) -> CutoutFixture | None:
|
|
1015
|
+
for area in self._addressable_areas.get_all():
|
|
1016
|
+
if (
|
|
1017
|
+
self._addressable_areas.get_addressable_area(area).area_type
|
|
1018
|
+
== AreaType.WASTE_CHUTE
|
|
1019
|
+
or self._addressable_areas.get_addressable_area(area).area_type
|
|
1020
|
+
== AreaType.MOVABLE_TRASH
|
|
1021
|
+
) and slot_name == self._addressable_areas.get_addressable_area_base_slot(
|
|
1022
|
+
area
|
|
1023
|
+
):
|
|
1024
|
+
# Given we only have one Waste Chute fixture and one type of Trash bin fixture it's
|
|
1025
|
+
# fine to return the first result of our potential fixtures here. This will need to
|
|
1026
|
+
# change in the future if there multiple trash fixtures that share the same area type.
|
|
1027
|
+
potential_fixture = (
|
|
1028
|
+
deck_configuration_provider.get_potential_cutout_fixtures(
|
|
1029
|
+
area, self._addressable_areas.deck_definition
|
|
1030
|
+
)[1].pop()
|
|
1031
|
+
)
|
|
1032
|
+
return deck_configuration_provider.get_cutout_fixture(
|
|
1033
|
+
potential_fixture.cutout_fixture_id,
|
|
1034
|
+
self._addressable_areas.deck_definition,
|
|
1035
|
+
)
|
|
1036
|
+
return None
|
|
1037
|
+
|
|
773
1038
|
def get_labware_grip_point(
|
|
774
1039
|
self,
|
|
775
1040
|
labware_definition: LabwareDefinition,
|
|
@@ -791,43 +1056,24 @@ class GeometryView:
|
|
|
791
1056
|
self._labware.get_grip_height_from_labware_bottom(labware_definition)
|
|
792
1057
|
)
|
|
793
1058
|
location_name: str
|
|
794
|
-
|
|
1059
|
+
offset = self._get_offset_from_parent_addressable_area(
|
|
1060
|
+
child_definition=labware_definition, parent=location
|
|
1061
|
+
) + self._get_calibrated_module_offset(location)
|
|
795
1062
|
if isinstance(location, DeckSlotLocation):
|
|
796
1063
|
location_name = location.slotName.id
|
|
797
|
-
offset = LabwareOffsetVector(x=0, y=0, z=0)
|
|
798
1064
|
elif isinstance(location, AddressableAreaLocation):
|
|
799
1065
|
location_name = location.addressableAreaName
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
location_name
|
|
804
|
-
)
|
|
805
|
-
)
|
|
806
|
-
return drop_labware_location + Point(z=grip_height_from_labware_bottom)
|
|
807
|
-
# Location should have been pre-validated so this will be a deck/staging area slot
|
|
808
|
-
else:
|
|
809
|
-
offset = LabwareOffsetVector(x=0, y=0, z=0)
|
|
810
|
-
else:
|
|
811
|
-
if isinstance(location, ModuleLocation):
|
|
812
|
-
location_name = self._modules.get_location(
|
|
813
|
-
location.moduleId
|
|
814
|
-
).slotName.id
|
|
815
|
-
else: # OnLabwareLocation
|
|
816
|
-
location_name = self.get_ancestor_slot_name(location.labwareId).id
|
|
817
|
-
labware_offset = self._get_offset_from_parent(
|
|
818
|
-
child_definition=labware_definition, parent=location
|
|
819
|
-
)
|
|
820
|
-
# Get the calibrated offset if the on labware location is on top of a module, otherwise return empty one
|
|
821
|
-
cal_offset = self._get_calibrated_module_offset(location)
|
|
822
|
-
offset = LabwareOffsetVector(
|
|
823
|
-
x=labware_offset.x + cal_offset.x,
|
|
824
|
-
y=labware_offset.y + cal_offset.y,
|
|
825
|
-
z=labware_offset.z + cal_offset.z,
|
|
1066
|
+
elif isinstance(location, ModuleLocation):
|
|
1067
|
+
location_name = self._modules.get_provided_addressable_area(
|
|
1068
|
+
location.moduleId
|
|
826
1069
|
)
|
|
1070
|
+
else: # OnLabwareLocation
|
|
1071
|
+
location_name = self.get_ancestor_addressable_area_name(location.labwareId)
|
|
827
1072
|
|
|
828
1073
|
location_center = self._addressable_areas.get_addressable_area_center(
|
|
829
1074
|
location_name
|
|
830
1075
|
)
|
|
1076
|
+
|
|
831
1077
|
return Point(
|
|
832
1078
|
location_center.x + offset.x,
|
|
833
1079
|
location_center.y + offset.y,
|
|
@@ -877,6 +1123,7 @@ class GeometryView:
|
|
|
877
1123
|
maybe_fixture = self._addressable_areas.get_fixture_by_deck_slot_name(
|
|
878
1124
|
slot_name
|
|
879
1125
|
)
|
|
1126
|
+
|
|
880
1127
|
# Ignore generic single slot fixtures
|
|
881
1128
|
if maybe_fixture and maybe_fixture["id"] in {
|
|
882
1129
|
"singleLeftSlot",
|
|
@@ -888,6 +1135,13 @@ class GeometryView:
|
|
|
888
1135
|
maybe_module = self._modules.get_by_slot(
|
|
889
1136
|
slot_name=slot_name,
|
|
890
1137
|
) or self._modules.get_overflowed_module_in_slot(slot_name=slot_name)
|
|
1138
|
+
|
|
1139
|
+
# For situations in which the deck config is none
|
|
1140
|
+
if maybe_fixture is None and maybe_labware is None and maybe_module is None:
|
|
1141
|
+
# todo(chb 2025-03-19): This can go away once we solve the problem of no deck config in analysis
|
|
1142
|
+
maybe_fixture = self._get_potential_disposal_location_cutout_fixtures(
|
|
1143
|
+
slot_name
|
|
1144
|
+
)
|
|
891
1145
|
else:
|
|
892
1146
|
# Modules and fixtures can't be loaded on staging slots
|
|
893
1147
|
maybe_fixture = None
|
|
@@ -1257,10 +1511,6 @@ class GeometryView:
|
|
|
1257
1511
|
# * The "additional offset" or "user offset", e.g. the `pickUpOffset` and `dropOffset`
|
|
1258
1512
|
# params in the `moveLabware` command.
|
|
1259
1513
|
#
|
|
1260
|
-
# And this *does* take these extra offsets into account:
|
|
1261
|
-
#
|
|
1262
|
-
# * The labware's Labware Position Check offset
|
|
1263
|
-
#
|
|
1264
1514
|
# For robustness, we should combine this with `get_gripper_labware_movement_waypoints()`.
|
|
1265
1515
|
#
|
|
1266
1516
|
# We should also be more explicit about which offsets act to move the gripper paddles
|
|
@@ -1282,23 +1532,17 @@ class GeometryView:
|
|
|
1282
1532
|
return
|
|
1283
1533
|
|
|
1284
1534
|
tip = self._pipettes.get_attached_tip(pipette.id)
|
|
1285
|
-
if tip:
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1535
|
+
if not tip:
|
|
1536
|
+
continue
|
|
1537
|
+
labware_top_z_when_gripped = gripper_homed_position_z + (
|
|
1538
|
+
self._labware.get_dimensions(labware_definition=labware_definition).z
|
|
1539
|
+
- self._labware.get_grip_height_from_labware_bottom(labware_definition)
|
|
1540
|
+
)
|
|
1541
|
+
# 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)
|
|
1542
|
+
if (_PIPETTE_HOMED_POSITION_Z - tip.length) < labware_top_z_when_gripped:
|
|
1543
|
+
raise LabwareMovementNotAllowedError(
|
|
1544
|
+
f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
|
|
1294
1545
|
)
|
|
1295
|
-
# 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)
|
|
1296
|
-
if (
|
|
1297
|
-
_PIPETTE_HOMED_POSITION_Z - tip.length
|
|
1298
|
-
) < labware_top_z_when_gripped:
|
|
1299
|
-
raise LabwareMovementNotAllowedError(
|
|
1300
|
-
f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
|
|
1301
|
-
)
|
|
1302
1546
|
return
|
|
1303
1547
|
|
|
1304
1548
|
def _nominal_gripper_offsets_for_location(
|
|
@@ -1359,104 +1603,434 @@ class GeometryView:
|
|
|
1359
1603
|
labware_id=labware_id, slot_name=None
|
|
1360
1604
|
)
|
|
1361
1605
|
|
|
1362
|
-
def
|
|
1363
|
-
"""Provide the
|
|
1606
|
+
def get_location_sequence(self, labware_id: str) -> LabwareLocationSequence:
|
|
1607
|
+
"""Provide the LocationSequence specifying the current position of the labware.
|
|
1364
1608
|
|
|
1365
|
-
|
|
1609
|
+
Elements in this sequence contain instance IDs of things. The chain is valid only until the
|
|
1610
|
+
labware is moved.
|
|
1611
|
+
"""
|
|
1612
|
+
return self.get_predicted_location_sequence(
|
|
1613
|
+
self._labware.get_location(labware_id)
|
|
1614
|
+
)
|
|
1615
|
+
|
|
1616
|
+
def get_predicted_location_sequence(
|
|
1617
|
+
self,
|
|
1618
|
+
labware_location: LabwareLocation,
|
|
1619
|
+
labware_pending_load: dict[str, LoadedLabware] | None = None,
|
|
1620
|
+
) -> LabwareLocationSequence:
|
|
1621
|
+
"""Get the location sequence for this location. Useful for a labware that hasn't been loaded."""
|
|
1622
|
+
return self._recurse_labware_location(
|
|
1623
|
+
labware_location, [], labware_pending_load or {}
|
|
1624
|
+
)
|
|
1625
|
+
|
|
1626
|
+
def _cutout_fixture_location_sequence_from_addressable_area(
|
|
1627
|
+
self, addressable_area_name: str
|
|
1628
|
+
) -> OnCutoutFixtureLocationSequenceComponent:
|
|
1629
|
+
(
|
|
1630
|
+
cutout_id,
|
|
1631
|
+
potential_fixtures,
|
|
1632
|
+
) = self._addressable_areas.get_current_potential_cutout_fixtures_for_addressable_area(
|
|
1633
|
+
addressable_area_name
|
|
1634
|
+
)
|
|
1635
|
+
return OnCutoutFixtureLocationSequenceComponent(
|
|
1636
|
+
possibleCutoutFixtureIds=sorted(
|
|
1637
|
+
[fixture.cutout_fixture_id for fixture in potential_fixtures]
|
|
1638
|
+
),
|
|
1639
|
+
cutoutId=cutout_id,
|
|
1640
|
+
)
|
|
1641
|
+
|
|
1642
|
+
def _recurse_labware_location_from_aa_component(
|
|
1643
|
+
self,
|
|
1644
|
+
labware_location: AddressableAreaLocation,
|
|
1645
|
+
building: LabwareLocationSequence,
|
|
1646
|
+
) -> LabwareLocationSequence:
|
|
1647
|
+
cutout_location = self._cutout_fixture_location_sequence_from_addressable_area(
|
|
1648
|
+
labware_location.addressableAreaName
|
|
1649
|
+
)
|
|
1650
|
+
# If the labware is loaded on an AA that is a module, we want to respect the convention
|
|
1651
|
+
# of giving it an OnModuleLocation.
|
|
1652
|
+
possible_module = self._modules.get_by_addressable_area(
|
|
1653
|
+
labware_location.addressableAreaName
|
|
1654
|
+
)
|
|
1655
|
+
if possible_module is not None:
|
|
1656
|
+
return building + [
|
|
1657
|
+
OnAddressableAreaLocationSequenceComponent(
|
|
1658
|
+
addressableAreaName=labware_location.addressableAreaName
|
|
1659
|
+
),
|
|
1660
|
+
OnModuleLocationSequenceComponent(moduleId=possible_module.id),
|
|
1661
|
+
cutout_location,
|
|
1662
|
+
]
|
|
1663
|
+
else:
|
|
1664
|
+
return building + [
|
|
1665
|
+
OnAddressableAreaLocationSequenceComponent(
|
|
1666
|
+
addressableAreaName=labware_location.addressableAreaName,
|
|
1667
|
+
),
|
|
1668
|
+
cutout_location,
|
|
1669
|
+
]
|
|
1670
|
+
|
|
1671
|
+
def _recurse_labware_location_from_module_component(
|
|
1672
|
+
self, labware_location: ModuleLocation, building: LabwareLocationSequence
|
|
1673
|
+
) -> LabwareLocationSequence:
|
|
1674
|
+
module_id = labware_location.moduleId
|
|
1675
|
+
module_aa = self._modules.get_provided_addressable_area(module_id)
|
|
1676
|
+
base_location: (
|
|
1677
|
+
OnCutoutFixtureLocationSequenceComponent
|
|
1678
|
+
| NotOnDeckLocationSequenceComponent
|
|
1679
|
+
) = self._cutout_fixture_location_sequence_from_addressable_area(module_aa)
|
|
1680
|
+
|
|
1681
|
+
if self._modules.get_deck_supports_module_fixtures():
|
|
1682
|
+
# On a deck with modules as cutout fixtures, we want, in order,
|
|
1683
|
+
# - the addressable area of the module
|
|
1684
|
+
# - the module with its module id, which is what clients want
|
|
1685
|
+
# - the cutout
|
|
1686
|
+
loc = self._modules.get_location(module_id)
|
|
1687
|
+
model = self._modules.get_connected_model(module_id)
|
|
1688
|
+
module_aa = self._modules.ensure_and_convert_module_fixture_location(
|
|
1689
|
+
loc.slotName, model
|
|
1690
|
+
)
|
|
1691
|
+
return building + [
|
|
1692
|
+
OnAddressableAreaLocationSequenceComponent(
|
|
1693
|
+
addressableAreaName=module_aa
|
|
1694
|
+
),
|
|
1695
|
+
OnModuleLocationSequenceComponent(moduleId=module_id),
|
|
1696
|
+
base_location,
|
|
1697
|
+
]
|
|
1698
|
+
else:
|
|
1699
|
+
# If the module isn't a cutout fixture, then we want
|
|
1700
|
+
# - the module
|
|
1701
|
+
# - the addressable area the module is loaded on
|
|
1702
|
+
# - the cutout
|
|
1703
|
+
location = self._modules.get_location(module_id)
|
|
1704
|
+
return building + [
|
|
1705
|
+
OnModuleLocationSequenceComponent(moduleId=module_id),
|
|
1706
|
+
OnAddressableAreaLocationSequenceComponent(
|
|
1707
|
+
addressableAreaName=location.slotName.value
|
|
1708
|
+
),
|
|
1709
|
+
base_location,
|
|
1710
|
+
]
|
|
1711
|
+
|
|
1712
|
+
def _recurse_labware_location_from_stacker_hopper(
|
|
1713
|
+
self,
|
|
1714
|
+
labware_location: InStackerHopperLocation,
|
|
1715
|
+
building: LabwareLocationSequence,
|
|
1716
|
+
) -> LabwareLocationSequence:
|
|
1717
|
+
loc = self._modules.get_location(labware_location.moduleId)
|
|
1718
|
+
model = self._modules.get_connected_model(labware_location.moduleId)
|
|
1719
|
+
module_aa = self._modules.ensure_and_convert_module_fixture_location(
|
|
1720
|
+
loc.slotName, model
|
|
1721
|
+
)
|
|
1722
|
+
cutout_base = self._cutout_fixture_location_sequence_from_addressable_area(
|
|
1723
|
+
module_aa
|
|
1724
|
+
)
|
|
1725
|
+
return building + [labware_location, cutout_base]
|
|
1726
|
+
|
|
1727
|
+
def _recurse_labware_location(
|
|
1728
|
+
self,
|
|
1729
|
+
labware_location: LabwareLocation,
|
|
1730
|
+
building: LabwareLocationSequence,
|
|
1731
|
+
labware_pending_load: dict[str, LoadedLabware],
|
|
1732
|
+
) -> LabwareLocationSequence:
|
|
1733
|
+
if isinstance(labware_location, AddressableAreaLocation):
|
|
1734
|
+
return self._recurse_labware_location_from_aa_component(
|
|
1735
|
+
labware_location, building
|
|
1736
|
+
)
|
|
1737
|
+
elif labware_location_is_off_deck(
|
|
1738
|
+
labware_location
|
|
1739
|
+
) or labware_location_is_system(labware_location):
|
|
1740
|
+
return building + [
|
|
1741
|
+
NotOnDeckLocationSequenceComponent(logicalLocationName=labware_location)
|
|
1742
|
+
]
|
|
1743
|
+
|
|
1744
|
+
elif isinstance(labware_location, OnLabwareLocation):
|
|
1745
|
+
labware = self._get_or_default_labware(
|
|
1746
|
+
labware_location.labwareId, labware_pending_load
|
|
1747
|
+
)
|
|
1748
|
+
return self._recurse_labware_location(
|
|
1749
|
+
labware.location,
|
|
1750
|
+
building
|
|
1751
|
+
+ [
|
|
1752
|
+
OnLabwareLocationSequenceComponent(
|
|
1753
|
+
labwareId=labware_location.labwareId, lidId=labware.lid_id
|
|
1754
|
+
)
|
|
1755
|
+
],
|
|
1756
|
+
labware_pending_load,
|
|
1757
|
+
)
|
|
1758
|
+
elif isinstance(labware_location, ModuleLocation):
|
|
1759
|
+
return self._recurse_labware_location_from_module_component(
|
|
1760
|
+
labware_location, building
|
|
1761
|
+
)
|
|
1762
|
+
elif isinstance(labware_location, DeckSlotLocation):
|
|
1763
|
+
return building + [
|
|
1764
|
+
OnAddressableAreaLocationSequenceComponent(
|
|
1765
|
+
addressableAreaName=labware_location.slotName.value,
|
|
1766
|
+
),
|
|
1767
|
+
self._cutout_fixture_location_sequence_from_addressable_area(
|
|
1768
|
+
labware_location.slotName.value
|
|
1769
|
+
),
|
|
1770
|
+
]
|
|
1771
|
+
elif isinstance(labware_location, InStackerHopperLocation):
|
|
1772
|
+
return self._recurse_labware_location_from_stacker_hopper(
|
|
1773
|
+
labware_location, building
|
|
1774
|
+
)
|
|
1775
|
+
else:
|
|
1776
|
+
_LOG.warn(f"Unhandled labware location kind: {labware_location}")
|
|
1777
|
+
return building
|
|
1778
|
+
|
|
1779
|
+
def get_offset_location(
|
|
1780
|
+
self, labware_id: str
|
|
1781
|
+
) -> Optional[LabwareOffsetLocationSequence]:
|
|
1782
|
+
"""Provide the LegacyLabwareOffsetLocation specifying the current position of the labware.
|
|
1783
|
+
|
|
1784
|
+
If the labware is in a location that cannot be specified by a LabwareOffsetLocationSequence
|
|
1366
1785
|
(for instance, OFF_DECK) then return None.
|
|
1367
1786
|
"""
|
|
1368
1787
|
parent_location = self._labware.get_location(labware_id)
|
|
1788
|
+
return self.get_projected_offset_location(parent_location)
|
|
1369
1789
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
module_location = self._modules.get_location(
|
|
1397
|
-
non_labware_parent_location.moduleId
|
|
1790
|
+
def get_projected_offset_location(
|
|
1791
|
+
self,
|
|
1792
|
+
labware_location: LabwareLocation,
|
|
1793
|
+
labware_pending_load: dict[str, LoadedLabware] | None = None,
|
|
1794
|
+
) -> Optional[LabwareOffsetLocationSequence]:
|
|
1795
|
+
"""Get the offset location that a labware loaded into this location would match.
|
|
1796
|
+
|
|
1797
|
+
`None` indicates that the very concept of a labware offset would not make sense
|
|
1798
|
+
for the given location, such as if it's some kind of off-deck location. This
|
|
1799
|
+
is a difference from `get_predicted_location_sequence()`, where off-deck
|
|
1800
|
+
locations are still represented as lists, but with special final elements.
|
|
1801
|
+
"""
|
|
1802
|
+
return self._recurse_labware_offset_location(
|
|
1803
|
+
labware_location, [], labware_pending_load or {}
|
|
1804
|
+
)
|
|
1805
|
+
|
|
1806
|
+
def _recurse_labware_offset_location(
|
|
1807
|
+
self,
|
|
1808
|
+
labware_location: LabwareLocation,
|
|
1809
|
+
building: LabwareOffsetLocationSequence,
|
|
1810
|
+
labware_pending_load: dict[str, LoadedLabware],
|
|
1811
|
+
) -> LabwareOffsetLocationSequence | None:
|
|
1812
|
+
if isinstance(labware_location, DeckSlotLocation):
|
|
1813
|
+
return building + [
|
|
1814
|
+
OnAddressableAreaOffsetLocationSequenceComponent(
|
|
1815
|
+
addressableAreaName=labware_location.slotName.value
|
|
1398
1816
|
)
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1817
|
+
]
|
|
1818
|
+
|
|
1819
|
+
elif isinstance(labware_location, ModuleLocation):
|
|
1820
|
+
module_id = labware_location.moduleId
|
|
1821
|
+
# Allow ModuleNotLoadedError to propagate.
|
|
1822
|
+
# Note also that we match based on the module's requested model, not its
|
|
1823
|
+
# actual model, to implement robot-server's documented HTTP API semantics.
|
|
1824
|
+
module_model = self._modules.get_requested_model(module_id=module_id)
|
|
1825
|
+
|
|
1826
|
+
# If `module_model is None`, it probably means that this module was added by
|
|
1827
|
+
# `ProtocolEngine.use_attached_modules()`, instead of an explicit
|
|
1828
|
+
# `loadModule` command.
|
|
1829
|
+
#
|
|
1830
|
+
# This assert should never raise in practice because:
|
|
1831
|
+
# 1. `ProtocolEngine.use_attached_modules()` is only used by
|
|
1832
|
+
# robot-server's "stateless command" endpoints, under `/commands`.
|
|
1833
|
+
# 2. Those endpoints don't support loading labware, so this code will
|
|
1834
|
+
# never run.
|
|
1835
|
+
#
|
|
1836
|
+
# Nevertheless, if it does happen somehow, we do NOT want to pass the
|
|
1837
|
+
# `None` value along to `LabwareView.find_applicable_labware_offset()`.
|
|
1838
|
+
# `None` means something different there, which will cause us to return
|
|
1839
|
+
# wrong results.
|
|
1840
|
+
assert module_model is not None, (
|
|
1841
|
+
"Can't find offsets for labware"
|
|
1842
|
+
" that are loaded on modules"
|
|
1843
|
+
" that were loaded with ProtocolEngine.use_attached_modules()."
|
|
1844
|
+
)
|
|
1845
|
+
|
|
1846
|
+
module_location = self._modules.get_location(module_id=module_id)
|
|
1847
|
+
if self._modules.get_deck_supports_module_fixtures():
|
|
1848
|
+
module_aa = self._modules.ensure_and_convert_module_fixture_location(
|
|
1849
|
+
module_location.slotName, module_model
|
|
1403
1850
|
)
|
|
1851
|
+
else:
|
|
1852
|
+
module_aa = module_location.slotName.value
|
|
1853
|
+
return building + [
|
|
1854
|
+
OnModuleOffsetLocationSequenceComponent(moduleModel=module_model),
|
|
1855
|
+
OnAddressableAreaOffsetLocationSequenceComponent(
|
|
1856
|
+
addressableAreaName=module_aa
|
|
1857
|
+
),
|
|
1858
|
+
]
|
|
1404
1859
|
|
|
1405
|
-
|
|
1860
|
+
elif isinstance(labware_location, OnLabwareLocation):
|
|
1861
|
+
parent_labware_id = labware_location.labwareId
|
|
1862
|
+
parent_labware = self._get_or_default_labware(
|
|
1863
|
+
parent_labware_id, labware_pending_load
|
|
1864
|
+
)
|
|
1865
|
+
parent_labware_uri = LabwareUri(parent_labware.definitionUri)
|
|
1866
|
+
base_location = parent_labware.location
|
|
1867
|
+
return self._recurse_labware_offset_location(
|
|
1868
|
+
base_location,
|
|
1869
|
+
building
|
|
1870
|
+
+ [
|
|
1871
|
+
OnLabwareOffsetLocationSequenceComponent(
|
|
1872
|
+
labwareUri=parent_labware_uri
|
|
1873
|
+
)
|
|
1874
|
+
],
|
|
1875
|
+
labware_pending_load,
|
|
1876
|
+
)
|
|
1877
|
+
|
|
1878
|
+
else: # Off deck
|
|
1879
|
+
return None
|
|
1880
|
+
|
|
1881
|
+
def get_liquid_handling_z_change(
|
|
1882
|
+
self,
|
|
1883
|
+
labware_id: str,
|
|
1884
|
+
well_name: str,
|
|
1885
|
+
pipette_id: str,
|
|
1886
|
+
operation_volume: float,
|
|
1887
|
+
) -> float:
|
|
1888
|
+
"""Get the change in height from a liquid handling operation."""
|
|
1889
|
+
initial_handling_height = self.get_meniscus_height(
|
|
1890
|
+
labware_id=labware_id, well_name=well_name
|
|
1891
|
+
)
|
|
1892
|
+
final_height = self.get_well_height_after_liquid_handling(
|
|
1893
|
+
labware_id=labware_id,
|
|
1894
|
+
well_name=well_name,
|
|
1895
|
+
pipette_id=pipette_id,
|
|
1896
|
+
initial_height=initial_handling_height,
|
|
1897
|
+
volume=operation_volume,
|
|
1898
|
+
)
|
|
1899
|
+
# this function is only called by
|
|
1900
|
+
# HardwarePipetteHandler::aspirate/dispense while_tracking, and shouldn't
|
|
1901
|
+
# be reached in the case of a simulated liquid_probe
|
|
1902
|
+
assert not isinstance(
|
|
1903
|
+
initial_handling_height, SimulatedProbeResult
|
|
1904
|
+
), "Initial handling height got SimulatedProbeResult"
|
|
1905
|
+
assert not isinstance(
|
|
1906
|
+
final_height, SimulatedProbeResult
|
|
1907
|
+
), "final height is SimulatedProbeResult"
|
|
1908
|
+
return final_height - initial_handling_height
|
|
1406
1909
|
|
|
1407
1910
|
def get_well_offset_adjustment(
|
|
1408
1911
|
self,
|
|
1409
1912
|
labware_id: str,
|
|
1410
1913
|
well_name: str,
|
|
1411
|
-
well_location:
|
|
1914
|
+
well_location: WellLocationType,
|
|
1412
1915
|
well_depth: float,
|
|
1916
|
+
pipette_id: Optional[str] = None,
|
|
1413
1917
|
operation_volume: Optional[float] = None,
|
|
1414
|
-
) ->
|
|
1918
|
+
) -> LiquidTrackingType:
|
|
1415
1919
|
"""Return a z-axis distance that accounts for well handling height and operation volume.
|
|
1416
1920
|
|
|
1417
1921
|
Distance is with reference to the well bottom.
|
|
1418
1922
|
"""
|
|
1419
1923
|
# TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
|
|
1924
|
+
|
|
1420
1925
|
initial_handling_height = self.get_well_handling_height(
|
|
1421
1926
|
labware_id=labware_id,
|
|
1422
1927
|
well_name=well_name,
|
|
1423
1928
|
well_location=well_location,
|
|
1424
1929
|
well_depth=well_depth,
|
|
1425
1930
|
)
|
|
1931
|
+
# if we're tracking a MENISCUS origin, and targeting either the beginning
|
|
1932
|
+
# position of the liquid or doing dynamic tracking, return the initial height
|
|
1933
|
+
if (
|
|
1934
|
+
well_location.origin == WellOrigin.MENISCUS
|
|
1935
|
+
and not well_location.volumeOffset
|
|
1936
|
+
):
|
|
1937
|
+
return initial_handling_height
|
|
1938
|
+
volume: Optional[float] = None
|
|
1426
1939
|
if isinstance(well_location, PickUpTipWellLocation):
|
|
1427
1940
|
volume = 0.0
|
|
1428
|
-
elif isinstance(well_location
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1941
|
+
elif isinstance(well_location, LiquidHandlingWellLocation):
|
|
1942
|
+
if well_location.volumeOffset == "operationVolume":
|
|
1943
|
+
volume = operation_volume or 0.0
|
|
1944
|
+
else:
|
|
1945
|
+
if not isinstance(well_location.volumeOffset, float):
|
|
1946
|
+
raise ValueError("Invalid volume offset.")
|
|
1947
|
+
volume = well_location.volumeOffset
|
|
1432
1948
|
|
|
1433
1949
|
if volume:
|
|
1434
|
-
|
|
1950
|
+
if pipette_id is None:
|
|
1951
|
+
raise ValueError(
|
|
1952
|
+
"cannot get liquid handling offset without pipette id."
|
|
1953
|
+
)
|
|
1954
|
+
liquid_height_after = self.get_well_height_after_liquid_handling(
|
|
1435
1955
|
labware_id=labware_id,
|
|
1436
1956
|
well_name=well_name,
|
|
1957
|
+
pipette_id=pipette_id,
|
|
1437
1958
|
initial_height=initial_handling_height,
|
|
1438
1959
|
volume=volume,
|
|
1439
1960
|
)
|
|
1961
|
+
return liquid_height_after
|
|
1440
1962
|
else:
|
|
1441
1963
|
return initial_handling_height
|
|
1442
1964
|
|
|
1965
|
+
def get_current_well_volume(
|
|
1966
|
+
self,
|
|
1967
|
+
labware_id: str,
|
|
1968
|
+
well_name: str,
|
|
1969
|
+
) -> LiquidTrackingType:
|
|
1970
|
+
"""Returns most recently updated volume in specified well."""
|
|
1971
|
+
last_updated = self._wells.get_last_liquid_update(labware_id, well_name)
|
|
1972
|
+
if last_updated is None:
|
|
1973
|
+
raise errors.LiquidHeightUnknownError(
|
|
1974
|
+
"Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS."
|
|
1975
|
+
)
|
|
1976
|
+
|
|
1977
|
+
well_liquid = self._wells.get_well_liquid_info(
|
|
1978
|
+
labware_id=labware_id, well_name=well_name
|
|
1979
|
+
)
|
|
1980
|
+
if (
|
|
1981
|
+
well_liquid.probed_height is not None
|
|
1982
|
+
and well_liquid.probed_height.height is not None
|
|
1983
|
+
and well_liquid.probed_height.last_probed == last_updated
|
|
1984
|
+
):
|
|
1985
|
+
volume = self.get_well_volume_at_height(
|
|
1986
|
+
labware_id=labware_id,
|
|
1987
|
+
well_name=well_name,
|
|
1988
|
+
height=well_liquid.probed_height.height,
|
|
1989
|
+
)
|
|
1990
|
+
return volume
|
|
1991
|
+
elif (
|
|
1992
|
+
well_liquid.loaded_volume is not None
|
|
1993
|
+
and well_liquid.loaded_volume.volume is not None
|
|
1994
|
+
and well_liquid.loaded_volume.last_loaded == last_updated
|
|
1995
|
+
):
|
|
1996
|
+
return well_liquid.loaded_volume.volume
|
|
1997
|
+
elif (
|
|
1998
|
+
well_liquid.probed_volume is not None
|
|
1999
|
+
and well_liquid.probed_volume.volume is not None
|
|
2000
|
+
and well_liquid.probed_volume.last_probed == last_updated
|
|
2001
|
+
):
|
|
2002
|
+
return well_liquid.probed_volume.volume
|
|
2003
|
+
else:
|
|
2004
|
+
# This should not happen if there was an update but who knows
|
|
2005
|
+
raise errors.LiquidVolumeUnknownError(
|
|
2006
|
+
f"Unable to find liquid volume despite an update at {last_updated}."
|
|
2007
|
+
)
|
|
2008
|
+
|
|
1443
2009
|
def get_meniscus_height(
|
|
1444
2010
|
self,
|
|
1445
2011
|
labware_id: str,
|
|
1446
2012
|
well_name: str,
|
|
1447
|
-
) ->
|
|
2013
|
+
) -> LiquidTrackingType:
|
|
1448
2014
|
"""Returns stored meniscus height in specified well."""
|
|
2015
|
+
last_updated = self._wells.get_last_liquid_update(labware_id, well_name)
|
|
2016
|
+
if last_updated is None:
|
|
2017
|
+
raise errors.LiquidHeightUnknownError(
|
|
2018
|
+
"Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS."
|
|
2019
|
+
)
|
|
2020
|
+
|
|
1449
2021
|
well_liquid = self._wells.get_well_liquid_info(
|
|
1450
2022
|
labware_id=labware_id, well_name=well_name
|
|
1451
2023
|
)
|
|
1452
2024
|
if (
|
|
1453
2025
|
well_liquid.probed_height is not None
|
|
1454
2026
|
and well_liquid.probed_height.height is not None
|
|
2027
|
+
and well_liquid.probed_height.last_probed == last_updated
|
|
1455
2028
|
):
|
|
1456
2029
|
return well_liquid.probed_height.height
|
|
1457
2030
|
elif (
|
|
1458
2031
|
well_liquid.loaded_volume is not None
|
|
1459
2032
|
and well_liquid.loaded_volume.volume is not None
|
|
2033
|
+
and well_liquid.loaded_volume.last_loaded == last_updated
|
|
1460
2034
|
):
|
|
1461
2035
|
return self.get_well_height_at_volume(
|
|
1462
2036
|
labware_id=labware_id,
|
|
@@ -1466,6 +2040,7 @@ class GeometryView:
|
|
|
1466
2040
|
elif (
|
|
1467
2041
|
well_liquid.probed_volume is not None
|
|
1468
2042
|
and well_liquid.probed_volume.volume is not None
|
|
2043
|
+
and well_liquid.probed_volume.last_probed == last_updated
|
|
1469
2044
|
):
|
|
1470
2045
|
return self.get_well_height_at_volume(
|
|
1471
2046
|
labware_id=labware_id,
|
|
@@ -1473,84 +2048,137 @@ class GeometryView:
|
|
|
1473
2048
|
volume=well_liquid.probed_volume.volume,
|
|
1474
2049
|
)
|
|
1475
2050
|
else:
|
|
2051
|
+
# This should not happen if there was an update but who knows
|
|
1476
2052
|
raise errors.LiquidHeightUnknownError(
|
|
1477
|
-
"
|
|
2053
|
+
f"Unable to find liquid height despite an update at {last_updated}."
|
|
1478
2054
|
)
|
|
1479
2055
|
|
|
1480
2056
|
def get_well_handling_height(
|
|
1481
2057
|
self,
|
|
1482
2058
|
labware_id: str,
|
|
1483
2059
|
well_name: str,
|
|
1484
|
-
well_location:
|
|
2060
|
+
well_location: WellLocationType,
|
|
1485
2061
|
well_depth: float,
|
|
1486
|
-
) ->
|
|
2062
|
+
) -> LiquidTrackingType:
|
|
1487
2063
|
"""Return the handling height for a labware well (with reference to the well bottom)."""
|
|
1488
|
-
handling_height = 0.0
|
|
2064
|
+
handling_height: LiquidTrackingType = 0.0
|
|
1489
2065
|
if well_location.origin == WellOrigin.TOP:
|
|
1490
|
-
handling_height = well_depth
|
|
2066
|
+
handling_height = float(well_depth)
|
|
1491
2067
|
elif well_location.origin == WellOrigin.CENTER:
|
|
1492
|
-
handling_height = well_depth / 2.0
|
|
2068
|
+
handling_height = float(well_depth / 2.0)
|
|
1493
2069
|
elif well_location.origin == WellOrigin.MENISCUS:
|
|
1494
2070
|
handling_height = self.get_meniscus_height(
|
|
1495
2071
|
labware_id=labware_id, well_name=well_name
|
|
1496
2072
|
)
|
|
1497
|
-
return
|
|
2073
|
+
return handling_height
|
|
1498
2074
|
|
|
1499
|
-
def
|
|
1500
|
-
self,
|
|
1501
|
-
|
|
2075
|
+
def get_well_height_after_liquid_handling(
|
|
2076
|
+
self,
|
|
2077
|
+
labware_id: str,
|
|
2078
|
+
well_name: str,
|
|
2079
|
+
pipette_id: str,
|
|
2080
|
+
initial_height: LiquidTrackingType,
|
|
2081
|
+
volume: float,
|
|
2082
|
+
) -> LiquidTrackingType:
|
|
1502
2083
|
"""Return the height of liquid in a labware well after a given volume has been handled.
|
|
1503
2084
|
|
|
1504
2085
|
This is given an initial handling height, with reference to the well bottom.
|
|
1505
2086
|
"""
|
|
2087
|
+
well_def = self._labware.get_well_definition(labware_id, well_name)
|
|
2088
|
+
well_depth = well_def.depth
|
|
1506
2089
|
well_geometry = self._labware.get_well_geometry(
|
|
1507
2090
|
labware_id=labware_id, well_name=well_name
|
|
1508
2091
|
)
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
2092
|
+
try:
|
|
2093
|
+
initial_volume = find_volume_at_well_height(
|
|
2094
|
+
target_height=initial_height, well_geometry=well_geometry
|
|
2095
|
+
)
|
|
2096
|
+
final_volume = initial_volume + (
|
|
2097
|
+
volume
|
|
2098
|
+
* self.get_nozzles_per_well(
|
|
2099
|
+
labware_id=labware_id,
|
|
2100
|
+
target_well_name=well_name,
|
|
2101
|
+
pipette_id=pipette_id,
|
|
2102
|
+
)
|
|
2103
|
+
)
|
|
2104
|
+
# NOTE(cm): if final_volume is outside the bounds of the well, it will get
|
|
2105
|
+
# adjusted inside find_height_at_well_volume to accomodate well the height
|
|
2106
|
+
# calculation.
|
|
2107
|
+
height_inside_well = find_height_at_well_volume(
|
|
2108
|
+
target_volume=final_volume, well_geometry=well_geometry
|
|
2109
|
+
)
|
|
2110
|
+
return self._validate_well_position(
|
|
2111
|
+
target_height=height_inside_well,
|
|
2112
|
+
well_max_height=well_depth,
|
|
2113
|
+
pipette_id=pipette_id,
|
|
2114
|
+
)
|
|
2115
|
+
except InvalidLiquidHeightFound as _exception:
|
|
2116
|
+
raise InvalidLiquidHeightFound(
|
|
2117
|
+
message=_exception.message
|
|
2118
|
+
+ f"for well {well_name} of {self._labware.get_display_name(labware_id)} on slot {self.get_ancestor_slot_name(labware_id)}"
|
|
2119
|
+
)
|
|
1516
2120
|
|
|
1517
2121
|
def get_well_height_at_volume(
|
|
1518
|
-
self, labware_id: str, well_name: str, volume:
|
|
1519
|
-
) ->
|
|
2122
|
+
self, labware_id: str, well_name: str, volume: LiquidTrackingType
|
|
2123
|
+
) -> LiquidTrackingType:
|
|
1520
2124
|
"""Convert well volume to height."""
|
|
1521
2125
|
well_geometry = self._labware.get_well_geometry(labware_id, well_name)
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
2126
|
+
try:
|
|
2127
|
+
return find_height_at_well_volume(
|
|
2128
|
+
target_volume=volume, well_geometry=well_geometry
|
|
2129
|
+
)
|
|
2130
|
+
except InvalidLiquidHeightFound as _exception:
|
|
2131
|
+
raise InvalidLiquidHeightFound(
|
|
2132
|
+
message=_exception.message
|
|
2133
|
+
+ f"for well {well_name} of {self._labware.get_display_name(labware_id)} on slot {self.get_ancestor_slot_name(labware_id)}"
|
|
2134
|
+
)
|
|
1525
2135
|
|
|
1526
2136
|
def get_well_volume_at_height(
|
|
1527
|
-
self,
|
|
1528
|
-
|
|
2137
|
+
self,
|
|
2138
|
+
labware_id: str,
|
|
2139
|
+
well_name: str,
|
|
2140
|
+
height: LiquidTrackingType,
|
|
2141
|
+
) -> LiquidTrackingType:
|
|
1529
2142
|
"""Convert well height to volume."""
|
|
1530
2143
|
well_geometry = self._labware.get_well_geometry(labware_id, well_name)
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
2144
|
+
try:
|
|
2145
|
+
return find_volume_at_well_height(
|
|
2146
|
+
target_height=height, well_geometry=well_geometry
|
|
2147
|
+
)
|
|
2148
|
+
except InvalidLiquidHeightFound as _exception:
|
|
2149
|
+
raise InvalidLiquidHeightFound(
|
|
2150
|
+
message=_exception.message
|
|
2151
|
+
+ f"for well {well_name} of {self._labware.get_display_name(labware_id)} on slot {self.get_ancestor_slot_name(labware_id)}"
|
|
2152
|
+
)
|
|
1534
2153
|
|
|
1535
2154
|
def validate_dispense_volume_into_well(
|
|
1536
2155
|
self,
|
|
1537
2156
|
labware_id: str,
|
|
1538
2157
|
well_name: str,
|
|
1539
|
-
well_location:
|
|
2158
|
+
well_location: WellLocationType,
|
|
1540
2159
|
volume: float,
|
|
1541
2160
|
) -> None:
|
|
1542
2161
|
"""Raise InvalidDispenseVolumeError if planned dispense volume will overflow well."""
|
|
1543
2162
|
well_def = self._labware.get_well_definition(labware_id, well_name)
|
|
1544
|
-
well_volumetric_capacity = well_def.totalLiquidVolume
|
|
2163
|
+
well_volumetric_capacity = float(well_def.totalLiquidVolume)
|
|
1545
2164
|
if well_location.origin == WellOrigin.MENISCUS:
|
|
1546
2165
|
# TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
|
|
1547
2166
|
well_geometry = self._labware.get_well_geometry(labware_id, well_name)
|
|
1548
2167
|
meniscus_height = self.get_meniscus_height(
|
|
1549
2168
|
labware_id=labware_id, well_name=well_name
|
|
1550
2169
|
)
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
2170
|
+
try:
|
|
2171
|
+
meniscus_volume = find_volume_at_well_height(
|
|
2172
|
+
target_height=meniscus_height, well_geometry=well_geometry
|
|
2173
|
+
)
|
|
2174
|
+
except InvalidLiquidHeightFound as _exception:
|
|
2175
|
+
raise InvalidLiquidHeightFound(
|
|
2176
|
+
message=_exception.message
|
|
2177
|
+
+ f"for well {well_name} of {self._labware.get_display_name(labware_id)} on slot {self.get_ancestor_slot_name(labware_id)}"
|
|
2178
|
+
)
|
|
2179
|
+
# if meniscus volume is a simulated value, comparisons aren't meaningful
|
|
2180
|
+
if isinstance(meniscus_volume, SimulatedProbeResult):
|
|
2181
|
+
return
|
|
1554
2182
|
remaining_volume = well_volumetric_capacity - meniscus_volume
|
|
1555
2183
|
if volume > remaining_volume:
|
|
1556
2184
|
raise errors.InvalidDispenseVolumeError(
|
|
@@ -1605,3 +2233,40 @@ class GeometryView:
|
|
|
1605
2233
|
target_well_name,
|
|
1606
2234
|
self._labware.get_definition(labware_id).ordering,
|
|
1607
2235
|
)
|
|
2236
|
+
|
|
2237
|
+
def get_height_of_labware_stack(
|
|
2238
|
+
self, definitions: list[LabwareDefinition]
|
|
2239
|
+
) -> float:
|
|
2240
|
+
"""Get the overall height of a stack of labware listed by definition in top-first order."""
|
|
2241
|
+
if len(definitions) == 0:
|
|
2242
|
+
return 0
|
|
2243
|
+
if len(definitions) == 1:
|
|
2244
|
+
return definitions[0].dimensions.zDimension
|
|
2245
|
+
total_height = 0.0
|
|
2246
|
+
upper_def: LabwareDefinition = definitions[0]
|
|
2247
|
+
for lower_def in definitions[1:]:
|
|
2248
|
+
overlap = self._labware.get_labware_overlap_offsets(
|
|
2249
|
+
upper_def, lower_def.parameters.loadName
|
|
2250
|
+
).z
|
|
2251
|
+
total_height += upper_def.dimensions.zDimension - overlap
|
|
2252
|
+
upper_def = lower_def
|
|
2253
|
+
return total_height + upper_def.dimensions.zDimension
|
|
2254
|
+
|
|
2255
|
+
def get_height_of_stacker_labware_pool(self, module_id: str) -> float:
|
|
2256
|
+
"""Get the overall height of a stack of labware in a Stacker module."""
|
|
2257
|
+
stacker = self._modules.get_flex_stacker_substate(module_id)
|
|
2258
|
+
pool_list = stacker.get_pool_definition_ordered_list()
|
|
2259
|
+
if not pool_list:
|
|
2260
|
+
return 0.0
|
|
2261
|
+
return self.get_height_of_labware_stack(pool_list)
|
|
2262
|
+
|
|
2263
|
+
def _get_or_default_labware(
|
|
2264
|
+
self, labware_id: str, pending_labware: dict[str, LoadedLabware]
|
|
2265
|
+
) -> LoadedLabware:
|
|
2266
|
+
try:
|
|
2267
|
+
return self._labware.get(labware_id)
|
|
2268
|
+
except LabwareNotLoadedError as lnle:
|
|
2269
|
+
try:
|
|
2270
|
+
return pending_labware[labware_id]
|
|
2271
|
+
except KeyError as ke:
|
|
2272
|
+
raise lnle from ke
|