opentrons 8.3.1a1__py2.py3-none-any.whl → 8.4.0a1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opentrons/calibration_storage/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 +19 -3
- opentrons/legacy_commands/helpers.py +15 -0
- opentrons/legacy_commands/types.py +3 -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 +1233 -65
- 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/protocol.py +253 -11
- opentrons/protocol_api/core/engine/stringify.py +19 -8
- opentrons/protocol_api/core/engine/transfer_components_executor.py +853 -0
- opentrons/protocol_api/core/engine/well.py +60 -5
- opentrons/protocol_api/core/instrument.py +65 -19
- 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 +69 -21
- 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 +25 -1
- 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 +67 -21
- opentrons/protocol_api/core/module.py +43 -0
- opentrons/protocol_api/core/protocol.py +33 -0
- opentrons/protocol_api/core/well.py +21 -1
- opentrons/protocol_api/instrument_context.py +246 -123
- opentrons/protocol_api/labware.py +75 -11
- opentrons/protocol_api/module_contexts.py +140 -0
- opentrons/protocol_api/protocol_context.py +156 -16
- 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 +30 -0
- 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 +237 -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 +69 -0
- 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 +240 -0
- opentrons/protocol_engine/commands/drop_tip.py +23 -1
- opentrons/protocol_engine/commands/evotip_dispense.py +6 -7
- opentrons/protocol_engine/commands/evotip_seal_pipette.py +24 -29
- opentrons/protocol_engine/commands/evotip_unseal_pipette.py +1 -7
- 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 +288 -0
- opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
- opentrons/protocol_engine/commands/labware_handling_common.py +24 -0
- opentrons/protocol_engine/commands/liquid_probe.py +21 -12
- 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/pick_up_tip.py +5 -2
- opentrons/protocol_engine/commands/pipetting_common.py +154 -7
- opentrons/protocol_engine/commands/prepare_to_aspirate.py +17 -2
- opentrons/protocol_engine/commands/reload_labware.py +6 -19
- 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/errors/__init__.py +8 -0
- opentrons/protocol_engine/errors/exceptions.py +50 -0
- opentrons/protocol_engine/execution/equipment.py +123 -106
- opentrons/protocol_engine/execution/labware_movement.py +8 -6
- opentrons/protocol_engine/execution/pipetting.py +233 -26
- opentrons/protocol_engine/execution/tip_handler.py +14 -5
- opentrons/protocol_engine/labware_offset_standardization.py +173 -0
- opentrons/protocol_engine/protocol_engine.py +22 -13
- opentrons/protocol_engine/resources/deck_configuration_provider.py +94 -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 +26 -10
- opentrons/protocol_engine/state/geometry.py +683 -100
- 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 +178 -52
- 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 +54 -8
- opentrons/protocol_engine/types/__init__.py +292 -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 +110 -0
- opentrons/protocol_engine/types/labware_movement.py +22 -0
- opentrons/protocol_engine/types/labware_offset_location.py +108 -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 +137 -0
- opentrons/protocol_engine/types/location.py +193 -0
- opentrons/protocol_engine/types/module.py +269 -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 +107 -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 +5 -6
- opentrons/protocols/models/__init__.py +0 -21
- opentrons/simulate.py +4 -2
- opentrons/types.py +15 -6
- {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/METADATA +4 -4
- {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/RECORD +188 -148
- 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.1a1.dist-info → opentrons-8.4.0a1.dist-info}/LICENSE +0 -0
- {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/WHEEL +0 -0
- {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/entry_points.txt +0 -0
- {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/top_level.txt +0 -0
|
@@ -1,31 +1,44 @@
|
|
|
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
19
|
from opentrons_shared_data.errors.exceptions import InvalidStoredData
|
|
13
20
|
from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN
|
|
21
|
+
from opentrons_shared_data.labware.labware_definition import LabwareDefinition
|
|
14
22
|
from opentrons_shared_data.deck.types import CutoutFixture
|
|
15
23
|
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
|
|
24
|
+
from opentrons_shared_data.pipette.types import ChannelCount, LabwareUri
|
|
18
25
|
|
|
19
26
|
from .. import errors
|
|
20
27
|
from ..errors import (
|
|
28
|
+
LabwareNotLoadedError,
|
|
21
29
|
LabwareNotLoadedOnLabwareError,
|
|
22
30
|
LabwareNotLoadedOnModuleError,
|
|
23
31
|
LabwareMovementNotAllowedError,
|
|
24
32
|
OperationLocationNotInWellError,
|
|
25
33
|
)
|
|
26
|
-
from ..resources import
|
|
34
|
+
from ..resources import (
|
|
35
|
+
fixture_validation,
|
|
36
|
+
labware_validation,
|
|
37
|
+
deck_configuration_provider,
|
|
38
|
+
)
|
|
27
39
|
from ..types import (
|
|
28
40
|
OFF_DECK_LOCATION,
|
|
41
|
+
SYSTEM_LOCATION,
|
|
29
42
|
LoadedLabware,
|
|
30
43
|
LoadedModule,
|
|
31
44
|
WellLocation,
|
|
@@ -46,13 +59,28 @@ from ..types import (
|
|
|
46
59
|
CurrentPipetteLocation,
|
|
47
60
|
TipGeometry,
|
|
48
61
|
LabwareMovementOffsetData,
|
|
62
|
+
InStackerHopperLocation,
|
|
49
63
|
OnDeckLabwareLocation,
|
|
50
64
|
AddressableAreaLocation,
|
|
51
65
|
AddressableOffsetVector,
|
|
52
66
|
StagingSlotLocation,
|
|
53
|
-
|
|
67
|
+
LabwareOffsetLocationSequence,
|
|
68
|
+
OnModuleOffsetLocationSequenceComponent,
|
|
69
|
+
OnAddressableAreaOffsetLocationSequenceComponent,
|
|
70
|
+
OnLabwareOffsetLocationSequenceComponent,
|
|
71
|
+
OnLabwareLocationSequenceComponent,
|
|
54
72
|
ModuleModel,
|
|
73
|
+
PotentialCutoutFixture,
|
|
74
|
+
LabwareLocationSequence,
|
|
75
|
+
OnModuleLocationSequenceComponent,
|
|
76
|
+
OnAddressableAreaLocationSequenceComponent,
|
|
77
|
+
OnCutoutFixtureLocationSequenceComponent,
|
|
78
|
+
NotOnDeckLocationSequenceComponent,
|
|
79
|
+
AreaType,
|
|
80
|
+
labware_location_is_off_deck,
|
|
81
|
+
labware_location_is_system,
|
|
55
82
|
)
|
|
83
|
+
from ..types.liquid_level_detection import SimulatedProbeResult, LiquidTrackingType
|
|
56
84
|
from .config import Config
|
|
57
85
|
from .labware import LabwareView
|
|
58
86
|
from .wells import WellView
|
|
@@ -66,6 +94,7 @@ from .frustum_helpers import (
|
|
|
66
94
|
from ._well_math import wells_covered_by_pipette_configuration, nozzles_per_well
|
|
67
95
|
|
|
68
96
|
|
|
97
|
+
_LOG = getLogger(__name__)
|
|
69
98
|
SLOT_WIDTH = 128
|
|
70
99
|
_PIPETTE_HOMED_POSITION_Z = (
|
|
71
100
|
248.0 # Height of the bottom of the nozzle without the tip attached when homed
|
|
@@ -160,6 +189,7 @@ class GeometryView:
|
|
|
160
189
|
self._get_highest_z_from_labware_data(lw_data)
|
|
161
190
|
for lw_data in self._labware.get_all()
|
|
162
191
|
if lw_data.location != OFF_DECK_LOCATION
|
|
192
|
+
and not self._labware.get_labware_by_lid_id(lw_data.id)
|
|
163
193
|
),
|
|
164
194
|
default=0.0,
|
|
165
195
|
)
|
|
@@ -306,13 +336,13 @@ class GeometryView:
|
|
|
306
336
|
return LabwareOffsetVector(x=0, y=0, z=0)
|
|
307
337
|
elif isinstance(parent, ModuleLocation):
|
|
308
338
|
module_id = parent.moduleId
|
|
309
|
-
module_to_child = self._modules.get_nominal_offset_to_child(
|
|
310
|
-
module_id=module_id, addressable_areas=self._addressable_areas
|
|
311
|
-
)
|
|
312
339
|
module_model = self._modules.get_connected_model(module_id)
|
|
313
340
|
stacking_overlap = self._labware.get_module_overlap_offsets(
|
|
314
341
|
child_definition, module_model
|
|
315
342
|
)
|
|
343
|
+
module_to_child = self._modules.get_nominal_offset_to_child(
|
|
344
|
+
module_id=module_id, addressable_areas=self._addressable_areas
|
|
345
|
+
)
|
|
316
346
|
return LabwareOffsetVector(
|
|
317
347
|
x=module_to_child.x - stacking_overlap.x,
|
|
318
348
|
y=module_to_child.y - stacking_overlap.y,
|
|
@@ -388,7 +418,11 @@ class GeometryView:
|
|
|
388
418
|
elif isinstance(location, OnLabwareLocation):
|
|
389
419
|
labware_data = self._labware.get(location.labwareId)
|
|
390
420
|
return self._get_calibrated_module_offset(labware_data.location)
|
|
391
|
-
elif
|
|
421
|
+
elif (
|
|
422
|
+
location == OFF_DECK_LOCATION
|
|
423
|
+
or location == SYSTEM_LOCATION
|
|
424
|
+
or isinstance(location, InStackerHopperLocation)
|
|
425
|
+
):
|
|
392
426
|
raise errors.LabwareNotOnDeckError(
|
|
393
427
|
"Labware does not have a slot or module associated with it"
|
|
394
428
|
" since it is no longer on the deck."
|
|
@@ -489,10 +523,13 @@ class GeometryView:
|
|
|
489
523
|
well_depth=well_depth,
|
|
490
524
|
operation_volume=operation_volume,
|
|
491
525
|
)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
526
|
+
if not isinstance(offset_adjustment, SimulatedProbeResult):
|
|
527
|
+
offset = offset.model_copy(update={"z": offset.z + offset_adjustment})
|
|
528
|
+
self.validate_well_position(
|
|
529
|
+
well_location=well_location,
|
|
530
|
+
z_offset=offset.z,
|
|
531
|
+
pipette_id=pipette_id,
|
|
532
|
+
)
|
|
496
533
|
|
|
497
534
|
return Point(
|
|
498
535
|
x=labware_pos.x + offset.x + well_def.x,
|
|
@@ -532,23 +569,26 @@ class GeometryView:
|
|
|
532
569
|
labware_id: str,
|
|
533
570
|
well_name: str,
|
|
534
571
|
absolute_point: Point,
|
|
535
|
-
|
|
536
|
-
) -> LiquidHandlingWellLocation:
|
|
537
|
-
"""Given absolute position, get relative location of a well in a labware.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if is_meniscus:
|
|
542
|
-
return LiquidHandlingWellLocation(
|
|
572
|
+
meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
|
|
573
|
+
) -> Tuple[LiquidHandlingWellLocation, bool]:
|
|
574
|
+
"""Given absolute position, get relative location of a well in a labware."""
|
|
575
|
+
dynamic_liquid_tracking = False
|
|
576
|
+
if meniscus_tracking:
|
|
577
|
+
location = LiquidHandlingWellLocation(
|
|
543
578
|
origin=WellOrigin.MENISCUS,
|
|
544
579
|
offset=WellOffset(x=0, y=0, z=absolute_point.z),
|
|
545
580
|
)
|
|
581
|
+
if meniscus_tracking == MeniscusTrackingTarget.END:
|
|
582
|
+
location.volumeOffset = "operationVolume"
|
|
583
|
+
elif meniscus_tracking == MeniscusTrackingTarget.DYNAMIC:
|
|
584
|
+
dynamic_liquid_tracking = True
|
|
546
585
|
else:
|
|
547
586
|
well_absolute_point = self.get_well_position(labware_id, well_name)
|
|
548
587
|
delta = absolute_point - well_absolute_point
|
|
549
|
-
|
|
588
|
+
location = LiquidHandlingWellLocation(
|
|
550
589
|
offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)
|
|
551
590
|
)
|
|
591
|
+
return location, dynamic_liquid_tracking
|
|
552
592
|
|
|
553
593
|
def get_relative_pick_up_tip_well_location(
|
|
554
594
|
self,
|
|
@@ -635,7 +675,7 @@ class GeometryView:
|
|
|
635
675
|
|
|
636
676
|
return TipGeometry(
|
|
637
677
|
length=effective_length,
|
|
638
|
-
diameter=well_def.diameter,
|
|
678
|
+
diameter=well_def.diameter,
|
|
639
679
|
# TODO(mc, 2020-11-12): WellDefinition type says totalLiquidVolume
|
|
640
680
|
# is a float, but hardware controller expects an int
|
|
641
681
|
volume=int(well_def.totalLiquidVolume),
|
|
@@ -749,27 +789,151 @@ class GeometryView:
|
|
|
749
789
|
return slot_name
|
|
750
790
|
|
|
751
791
|
def ensure_location_not_occupied(
|
|
752
|
-
self,
|
|
792
|
+
self,
|
|
793
|
+
location: _LabwareLocation,
|
|
794
|
+
desired_addressable_area: Optional[str] = None,
|
|
753
795
|
) -> _LabwareLocation:
|
|
754
796
|
"""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
|
-
|
|
797
|
+
# Collect set of existing fixtures, if any
|
|
798
|
+
existing_fixtures = self._get_potential_fixtures_for_location_occupation(
|
|
799
|
+
location
|
|
800
|
+
)
|
|
801
|
+
potential_fixtures = (
|
|
802
|
+
self._get_potential_fixtures_for_location_occupation(
|
|
803
|
+
AddressableAreaLocation(addressableAreaName=desired_addressable_area)
|
|
804
|
+
)
|
|
805
|
+
if desired_addressable_area is not None
|
|
806
|
+
else None
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
# Handle the checking conflict on an incoming fixture
|
|
810
|
+
if potential_fixtures is not None and isinstance(location, DeckSlotLocation):
|
|
811
|
+
if (
|
|
812
|
+
existing_fixtures is not None
|
|
813
|
+
and not any(
|
|
814
|
+
location.slotName.id in fixture.provided_addressable_areas
|
|
815
|
+
for fixture in potential_fixtures[1].intersection(
|
|
816
|
+
existing_fixtures[1]
|
|
817
|
+
)
|
|
818
|
+
)
|
|
819
|
+
) or (
|
|
820
|
+
self._labware.get_by_slot(location.slotName) is not None
|
|
821
|
+
and not any(
|
|
822
|
+
location.slotName.id in fixture.provided_addressable_areas
|
|
823
|
+
for fixture in potential_fixtures[1]
|
|
824
|
+
)
|
|
825
|
+
):
|
|
826
|
+
self._labware.raise_if_labware_in_location(location)
|
|
827
|
+
|
|
828
|
+
else:
|
|
829
|
+
self._modules.raise_if_module_in_location(location)
|
|
830
|
+
|
|
831
|
+
# Otherwise handle standard conflict checking
|
|
832
|
+
else:
|
|
833
|
+
if isinstance(
|
|
834
|
+
location,
|
|
835
|
+
(
|
|
836
|
+
DeckSlotLocation,
|
|
837
|
+
ModuleLocation,
|
|
838
|
+
OnLabwareLocation,
|
|
839
|
+
AddressableAreaLocation,
|
|
840
|
+
),
|
|
841
|
+
):
|
|
842
|
+
self._labware.raise_if_labware_in_location(location)
|
|
843
|
+
|
|
844
|
+
area = (
|
|
845
|
+
location.slotName.id
|
|
846
|
+
if isinstance(location, DeckSlotLocation)
|
|
847
|
+
else (
|
|
848
|
+
location.addressableAreaName
|
|
849
|
+
if isinstance(location, AddressableAreaLocation)
|
|
850
|
+
else None
|
|
851
|
+
)
|
|
852
|
+
)
|
|
853
|
+
if area is not None and (
|
|
854
|
+
existing_fixtures is None
|
|
855
|
+
or not any(
|
|
856
|
+
area in fixture.provided_addressable_areas
|
|
857
|
+
for fixture in existing_fixtures[1]
|
|
858
|
+
)
|
|
859
|
+
):
|
|
860
|
+
if isinstance(location, DeckSlotLocation):
|
|
861
|
+
self._modules.raise_if_module_in_location(location)
|
|
862
|
+
elif isinstance(location, AddressableAreaLocation):
|
|
863
|
+
self._modules.raise_if_module_in_location(
|
|
864
|
+
DeckSlotLocation(
|
|
865
|
+
slotName=self._addressable_areas.get_addressable_area_base_slot(
|
|
866
|
+
location.addressableAreaName
|
|
867
|
+
)
|
|
868
|
+
)
|
|
869
|
+
)
|
|
870
|
+
|
|
771
871
|
return location
|
|
772
872
|
|
|
873
|
+
def _get_potential_fixtures_for_location_occupation(
|
|
874
|
+
self, location: _LabwareLocation
|
|
875
|
+
) -> Tuple[str, Set[PotentialCutoutFixture]] | None:
|
|
876
|
+
loc: DeckSlotLocation | AddressableAreaLocation | None = None
|
|
877
|
+
if isinstance(location, AddressableAreaLocation):
|
|
878
|
+
# Convert the addressable area into a staging slot if applicable
|
|
879
|
+
slots = StagingSlotName._value2member_map_
|
|
880
|
+
for slot in slots:
|
|
881
|
+
if location.addressableAreaName == slot:
|
|
882
|
+
loc = DeckSlotLocation(
|
|
883
|
+
slotName=DeckSlotName(location.addressableAreaName[0] + "3")
|
|
884
|
+
)
|
|
885
|
+
if loc is None:
|
|
886
|
+
loc = location
|
|
887
|
+
elif isinstance(location, DeckSlotLocation):
|
|
888
|
+
loc = location
|
|
889
|
+
|
|
890
|
+
if isinstance(loc, DeckSlotLocation):
|
|
891
|
+
module = self._modules.get_by_slot(loc.slotName)
|
|
892
|
+
if module is not None and self._config.robot_type != "OT-2 Standard":
|
|
893
|
+
fixtures = deck_configuration_provider.get_potential_cutout_fixtures(
|
|
894
|
+
addressable_area_name=self._modules.ensure_and_convert_module_fixture_location(
|
|
895
|
+
deck_slot=loc.slotName,
|
|
896
|
+
model=module.model,
|
|
897
|
+
),
|
|
898
|
+
deck_definition=self._addressable_areas.deck_definition,
|
|
899
|
+
)
|
|
900
|
+
else:
|
|
901
|
+
fixtures = None
|
|
902
|
+
elif isinstance(loc, AddressableAreaLocation):
|
|
903
|
+
fixtures = deck_configuration_provider.get_potential_cutout_fixtures(
|
|
904
|
+
addressable_area_name=loc.addressableAreaName,
|
|
905
|
+
deck_definition=self._addressable_areas.deck_definition,
|
|
906
|
+
)
|
|
907
|
+
else:
|
|
908
|
+
fixtures = None
|
|
909
|
+
return fixtures
|
|
910
|
+
|
|
911
|
+
def _get_potential_disposal_location_cutout_fixtures(
|
|
912
|
+
self, slot_name: DeckSlotName
|
|
913
|
+
) -> CutoutFixture | None:
|
|
914
|
+
for area in self._addressable_areas.get_all():
|
|
915
|
+
if (
|
|
916
|
+
self._addressable_areas.get_addressable_area(area).area_type
|
|
917
|
+
== AreaType.WASTE_CHUTE
|
|
918
|
+
or self._addressable_areas.get_addressable_area(area).area_type
|
|
919
|
+
== AreaType.MOVABLE_TRASH
|
|
920
|
+
) and slot_name == self._addressable_areas.get_addressable_area_base_slot(
|
|
921
|
+
area
|
|
922
|
+
):
|
|
923
|
+
# Given we only have one Waste Chute fixture and one type of Trash bin fixture it's
|
|
924
|
+
# fine to return the first result of our potential fixtures here. This will need to
|
|
925
|
+
# change in the future if there multiple trash fixtures that share the same area type.
|
|
926
|
+
potential_fixture = (
|
|
927
|
+
deck_configuration_provider.get_potential_cutout_fixtures(
|
|
928
|
+
area, self._addressable_areas.deck_definition
|
|
929
|
+
)[1].pop()
|
|
930
|
+
)
|
|
931
|
+
return deck_configuration_provider.get_cutout_fixture(
|
|
932
|
+
potential_fixture.cutout_fixture_id,
|
|
933
|
+
self._addressable_areas.deck_definition,
|
|
934
|
+
)
|
|
935
|
+
return None
|
|
936
|
+
|
|
773
937
|
def get_labware_grip_point(
|
|
774
938
|
self,
|
|
775
939
|
labware_definition: LabwareDefinition,
|
|
@@ -791,6 +955,7 @@ class GeometryView:
|
|
|
791
955
|
self._labware.get_grip_height_from_labware_bottom(labware_definition)
|
|
792
956
|
)
|
|
793
957
|
location_name: str
|
|
958
|
+
module_location: ModuleLocation | None = None
|
|
794
959
|
|
|
795
960
|
if isinstance(location, DeckSlotLocation):
|
|
796
961
|
location_name = location.slotName.id
|
|
@@ -809,11 +974,19 @@ class GeometryView:
|
|
|
809
974
|
offset = LabwareOffsetVector(x=0, y=0, z=0)
|
|
810
975
|
else:
|
|
811
976
|
if isinstance(location, ModuleLocation):
|
|
812
|
-
location_name = self._modules.
|
|
977
|
+
location_name = self._modules.get_provided_addressable_area(
|
|
813
978
|
location.moduleId
|
|
814
|
-
)
|
|
979
|
+
)
|
|
980
|
+
module_location = location
|
|
815
981
|
else: # OnLabwareLocation
|
|
816
|
-
|
|
982
|
+
labware_loc = self._labware.get(location.labwareId).location
|
|
983
|
+
if isinstance(labware_loc, ModuleLocation):
|
|
984
|
+
location_name = self._modules.get_provided_addressable_area(
|
|
985
|
+
labware_loc.moduleId
|
|
986
|
+
)
|
|
987
|
+
module_location = labware_loc
|
|
988
|
+
else:
|
|
989
|
+
location_name = self.get_ancestor_slot_name(location.labwareId).id
|
|
817
990
|
labware_offset = self._get_offset_from_parent(
|
|
818
991
|
child_definition=labware_definition, parent=location
|
|
819
992
|
)
|
|
@@ -825,9 +998,28 @@ class GeometryView:
|
|
|
825
998
|
z=labware_offset.z + cal_offset.z,
|
|
826
999
|
)
|
|
827
1000
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1001
|
+
if module_location is not None:
|
|
1002
|
+
# Location center must be determined from the cutout the Module is loaded in
|
|
1003
|
+
position = deck_configuration_provider.get_cutout_position(
|
|
1004
|
+
cutout_id=self._addressable_areas.get_cutout_id_by_deck_slot_name(
|
|
1005
|
+
self._modules.get_location(module_location.moduleId).slotName
|
|
1006
|
+
),
|
|
1007
|
+
deck_definition=self._addressable_areas.deck_definition,
|
|
1008
|
+
)
|
|
1009
|
+
bounding_box = self._addressable_areas.get_addressable_area(
|
|
1010
|
+
location_name
|
|
1011
|
+
).bounding_box
|
|
1012
|
+
location_center = Point(
|
|
1013
|
+
position.x + bounding_box.x / 2,
|
|
1014
|
+
position.y + bounding_box.y / 2,
|
|
1015
|
+
position.z,
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
else:
|
|
1019
|
+
location_center = self._addressable_areas.get_addressable_area_center(
|
|
1020
|
+
location_name
|
|
1021
|
+
)
|
|
1022
|
+
|
|
831
1023
|
return Point(
|
|
832
1024
|
location_center.x + offset.x,
|
|
833
1025
|
location_center.y + offset.y,
|
|
@@ -877,6 +1069,7 @@ class GeometryView:
|
|
|
877
1069
|
maybe_fixture = self._addressable_areas.get_fixture_by_deck_slot_name(
|
|
878
1070
|
slot_name
|
|
879
1071
|
)
|
|
1072
|
+
|
|
880
1073
|
# Ignore generic single slot fixtures
|
|
881
1074
|
if maybe_fixture and maybe_fixture["id"] in {
|
|
882
1075
|
"singleLeftSlot",
|
|
@@ -888,6 +1081,13 @@ class GeometryView:
|
|
|
888
1081
|
maybe_module = self._modules.get_by_slot(
|
|
889
1082
|
slot_name=slot_name,
|
|
890
1083
|
) or self._modules.get_overflowed_module_in_slot(slot_name=slot_name)
|
|
1084
|
+
|
|
1085
|
+
# For situations in which the deck config is none
|
|
1086
|
+
if maybe_fixture is None and maybe_labware is None and maybe_module is None:
|
|
1087
|
+
# todo(chb 2025-03-19): This can go away once we solve the problem of no deck config in analysis
|
|
1088
|
+
maybe_fixture = self._get_potential_disposal_location_cutout_fixtures(
|
|
1089
|
+
slot_name
|
|
1090
|
+
)
|
|
891
1091
|
else:
|
|
892
1092
|
# Modules and fixtures can't be loaded on staging slots
|
|
893
1093
|
maybe_fixture = None
|
|
@@ -1359,50 +1559,297 @@ class GeometryView:
|
|
|
1359
1559
|
labware_id=labware_id, slot_name=None
|
|
1360
1560
|
)
|
|
1361
1561
|
|
|
1362
|
-
def
|
|
1363
|
-
"""Provide the
|
|
1562
|
+
def get_location_sequence(self, labware_id: str) -> LabwareLocationSequence:
|
|
1563
|
+
"""Provide the LocationSequence specifying the current position of the labware.
|
|
1364
1564
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1565
|
+
Elements in this sequence contain instance IDs of things. The chain is valid only until the
|
|
1566
|
+
labware is moved.
|
|
1367
1567
|
"""
|
|
1368
|
-
|
|
1568
|
+
return self.get_predicted_location_sequence(
|
|
1569
|
+
self._labware.get_location(labware_id)
|
|
1570
|
+
)
|
|
1369
1571
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1572
|
+
def get_predicted_location_sequence(
|
|
1573
|
+
self,
|
|
1574
|
+
labware_location: LabwareLocation,
|
|
1575
|
+
labware_pending_load: dict[str, LoadedLabware] | None = None,
|
|
1576
|
+
) -> LabwareLocationSequence:
|
|
1577
|
+
"""Get the location sequence for this location. Useful for a labware that hasn't been loaded."""
|
|
1578
|
+
return self._recurse_labware_location(
|
|
1579
|
+
labware_location, [], labware_pending_load or {}
|
|
1580
|
+
)
|
|
1581
|
+
|
|
1582
|
+
def _cutout_fixture_location_sequence_from_addressable_area(
|
|
1583
|
+
self, addressable_area_name: str
|
|
1584
|
+
) -> OnCutoutFixtureLocationSequenceComponent:
|
|
1585
|
+
(
|
|
1586
|
+
cutout_id,
|
|
1587
|
+
potential_fixtures,
|
|
1588
|
+
) = self._addressable_areas.get_current_potential_cutout_fixtures_for_addressable_area(
|
|
1589
|
+
addressable_area_name
|
|
1590
|
+
)
|
|
1591
|
+
return OnCutoutFixtureLocationSequenceComponent(
|
|
1592
|
+
possibleCutoutFixtureIds=sorted(
|
|
1593
|
+
[fixture.cutout_fixture_id for fixture in potential_fixtures]
|
|
1594
|
+
),
|
|
1595
|
+
cutoutId=cutout_id,
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
def _recurse_labware_location_from_aa_component(
|
|
1599
|
+
self,
|
|
1600
|
+
labware_location: AddressableAreaLocation,
|
|
1601
|
+
building: LabwareLocationSequence,
|
|
1602
|
+
) -> LabwareLocationSequence:
|
|
1603
|
+
cutout_location = self._cutout_fixture_location_sequence_from_addressable_area(
|
|
1604
|
+
labware_location.addressableAreaName
|
|
1605
|
+
)
|
|
1606
|
+
# If the labware is loaded on an AA that is a module, we want to respect the convention
|
|
1607
|
+
# of giving it an OnModuleLocation.
|
|
1608
|
+
possible_module = self._modules.get_by_addressable_area(
|
|
1609
|
+
labware_location.addressableAreaName
|
|
1610
|
+
)
|
|
1611
|
+
if possible_module is not None:
|
|
1612
|
+
return building + [
|
|
1613
|
+
OnAddressableAreaLocationSequenceComponent(
|
|
1614
|
+
addressableAreaName=labware_location.addressableAreaName
|
|
1615
|
+
),
|
|
1616
|
+
OnModuleLocationSequenceComponent(moduleId=possible_module.id),
|
|
1617
|
+
cutout_location,
|
|
1618
|
+
]
|
|
1619
|
+
else:
|
|
1620
|
+
return building + [
|
|
1621
|
+
OnAddressableAreaLocationSequenceComponent(
|
|
1622
|
+
addressableAreaName=labware_location.addressableAreaName,
|
|
1623
|
+
),
|
|
1624
|
+
cutout_location,
|
|
1625
|
+
]
|
|
1626
|
+
|
|
1627
|
+
def _recurse_labware_location_from_module_component(
|
|
1628
|
+
self, labware_location: ModuleLocation, building: LabwareLocationSequence
|
|
1629
|
+
) -> LabwareLocationSequence:
|
|
1630
|
+
module_id = labware_location.moduleId
|
|
1631
|
+
module_aa = self._modules.get_provided_addressable_area(module_id)
|
|
1632
|
+
base_location: (
|
|
1633
|
+
OnCutoutFixtureLocationSequenceComponent
|
|
1634
|
+
| NotOnDeckLocationSequenceComponent
|
|
1635
|
+
) = self._cutout_fixture_location_sequence_from_addressable_area(module_aa)
|
|
1636
|
+
|
|
1637
|
+
if self._modules.get_deck_supports_module_fixtures():
|
|
1638
|
+
# On a deck with modules as cutout fixtures, we want, in order,
|
|
1639
|
+
# - the addressable area of the module
|
|
1640
|
+
# - the module with its module id, which is what clients want
|
|
1641
|
+
# - the cutout
|
|
1642
|
+
loc = self._modules.get_location(module_id)
|
|
1643
|
+
model = self._modules.get_connected_model(module_id)
|
|
1644
|
+
module_aa = self._modules.ensure_and_convert_module_fixture_location(
|
|
1645
|
+
loc.slotName, model
|
|
1373
1646
|
)
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1647
|
+
return building + [
|
|
1648
|
+
OnAddressableAreaLocationSequenceComponent(
|
|
1649
|
+
addressableAreaName=module_aa
|
|
1650
|
+
),
|
|
1651
|
+
OnModuleLocationSequenceComponent(moduleId=module_id),
|
|
1652
|
+
base_location,
|
|
1653
|
+
]
|
|
1654
|
+
else:
|
|
1655
|
+
# If the module isn't a cutout fixture, then we want
|
|
1656
|
+
# - the module
|
|
1657
|
+
# - the addressable area the module is loaded on
|
|
1658
|
+
# - the cutout
|
|
1659
|
+
location = self._modules.get_location(module_id)
|
|
1660
|
+
return building + [
|
|
1661
|
+
OnModuleLocationSequenceComponent(moduleId=module_id),
|
|
1662
|
+
OnAddressableAreaLocationSequenceComponent(
|
|
1663
|
+
addressableAreaName=location.slotName.value
|
|
1664
|
+
),
|
|
1665
|
+
base_location,
|
|
1666
|
+
]
|
|
1667
|
+
|
|
1668
|
+
def _recurse_labware_location_from_stacker_hopper(
|
|
1669
|
+
self,
|
|
1670
|
+
labware_location: InStackerHopperLocation,
|
|
1671
|
+
building: LabwareLocationSequence,
|
|
1672
|
+
) -> LabwareLocationSequence:
|
|
1673
|
+
loc = self._modules.get_location(labware_location.moduleId)
|
|
1674
|
+
model = self._modules.get_connected_model(labware_location.moduleId)
|
|
1675
|
+
module_aa = self._modules.ensure_and_convert_module_fixture_location(
|
|
1676
|
+
loc.slotName, model
|
|
1677
|
+
)
|
|
1678
|
+
cutout_base = self._cutout_fixture_location_sequence_from_addressable_area(
|
|
1679
|
+
module_aa
|
|
1680
|
+
)
|
|
1681
|
+
return building + [labware_location, cutout_base]
|
|
1682
|
+
|
|
1683
|
+
def _recurse_labware_location(
|
|
1684
|
+
self,
|
|
1685
|
+
labware_location: LabwareLocation,
|
|
1686
|
+
building: LabwareLocationSequence,
|
|
1687
|
+
labware_pending_load: dict[str, LoadedLabware],
|
|
1688
|
+
) -> LabwareLocationSequence:
|
|
1689
|
+
if isinstance(labware_location, AddressableAreaLocation):
|
|
1690
|
+
return self._recurse_labware_location_from_aa_component(
|
|
1691
|
+
labware_location, building
|
|
1381
1692
|
)
|
|
1382
|
-
elif
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1693
|
+
elif labware_location_is_off_deck(
|
|
1694
|
+
labware_location
|
|
1695
|
+
) or labware_location_is_system(labware_location):
|
|
1696
|
+
return building + [
|
|
1697
|
+
NotOnDeckLocationSequenceComponent(logicalLocationName=labware_location)
|
|
1698
|
+
]
|
|
1699
|
+
|
|
1700
|
+
elif isinstance(labware_location, OnLabwareLocation):
|
|
1701
|
+
labware = self._get_or_default_labware(
|
|
1702
|
+
labware_location.labwareId, labware_pending_load
|
|
1703
|
+
)
|
|
1704
|
+
return self._recurse_labware_location(
|
|
1705
|
+
labware.location,
|
|
1706
|
+
building
|
|
1707
|
+
+ [
|
|
1708
|
+
OnLabwareLocationSequenceComponent(
|
|
1709
|
+
labwareId=labware_location.labwareId, lidId=labware.lid_id
|
|
1710
|
+
)
|
|
1711
|
+
],
|
|
1712
|
+
labware_pending_load,
|
|
1713
|
+
)
|
|
1714
|
+
elif isinstance(labware_location, ModuleLocation):
|
|
1715
|
+
return self._recurse_labware_location_from_module_component(
|
|
1716
|
+
labware_location, building
|
|
1717
|
+
)
|
|
1718
|
+
elif isinstance(labware_location, DeckSlotLocation):
|
|
1719
|
+
return building + [
|
|
1720
|
+
OnAddressableAreaLocationSequenceComponent(
|
|
1721
|
+
addressableAreaName=labware_location.slotName.value,
|
|
1722
|
+
),
|
|
1723
|
+
self._cutout_fixture_location_sequence_from_addressable_area(
|
|
1724
|
+
labware_location.slotName.value
|
|
1725
|
+
),
|
|
1726
|
+
]
|
|
1727
|
+
elif isinstance(labware_location, InStackerHopperLocation):
|
|
1728
|
+
return self._recurse_labware_location_from_stacker_hopper(
|
|
1729
|
+
labware_location, building
|
|
1730
|
+
)
|
|
1731
|
+
else:
|
|
1732
|
+
_LOG.warn(f"Unhandled labware location kind: {labware_location}")
|
|
1733
|
+
return building
|
|
1734
|
+
|
|
1735
|
+
def get_offset_location(
|
|
1736
|
+
self, labware_id: str
|
|
1737
|
+
) -> Optional[LabwareOffsetLocationSequence]:
|
|
1738
|
+
"""Provide the LegacyLabwareOffsetLocation specifying the current position of the labware.
|
|
1739
|
+
|
|
1740
|
+
If the labware is in a location that cannot be specified by a LabwareOffsetLocationSequence
|
|
1741
|
+
(for instance, OFF_DECK) then return None.
|
|
1742
|
+
"""
|
|
1743
|
+
parent_location = self._labware.get_location(labware_id)
|
|
1744
|
+
return self.get_projected_offset_location(parent_location)
|
|
1745
|
+
|
|
1746
|
+
def get_projected_offset_location(
|
|
1747
|
+
self,
|
|
1748
|
+
labware_location: LabwareLocation,
|
|
1749
|
+
labware_pending_load: dict[str, LoadedLabware] | None = None,
|
|
1750
|
+
) -> Optional[LabwareOffsetLocationSequence]:
|
|
1751
|
+
"""Get the offset location that a labware loaded into this location would match."""
|
|
1752
|
+
return self._recurse_labware_offset_location(
|
|
1753
|
+
labware_location, [], labware_pending_load or {}
|
|
1754
|
+
)
|
|
1755
|
+
|
|
1756
|
+
def _recurse_labware_offset_location(
|
|
1757
|
+
self,
|
|
1758
|
+
labware_location: LabwareLocation,
|
|
1759
|
+
building: LabwareOffsetLocationSequence,
|
|
1760
|
+
labware_pending_load: dict[str, LoadedLabware],
|
|
1761
|
+
) -> LabwareOffsetLocationSequence | None:
|
|
1762
|
+
if isinstance(labware_location, DeckSlotLocation):
|
|
1763
|
+
return building + [
|
|
1764
|
+
OnAddressableAreaOffsetLocationSequenceComponent(
|
|
1765
|
+
addressableAreaName=labware_location.slotName.value
|
|
1398
1766
|
)
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1767
|
+
]
|
|
1768
|
+
|
|
1769
|
+
elif isinstance(labware_location, ModuleLocation):
|
|
1770
|
+
module_id = labware_location.moduleId
|
|
1771
|
+
# Allow ModuleNotLoadedError to propagate.
|
|
1772
|
+
# Note also that we match based on the module's requested model, not its
|
|
1773
|
+
# actual model, to implement robot-server's documented HTTP API semantics.
|
|
1774
|
+
module_model = self._modules.get_requested_model(module_id=module_id)
|
|
1775
|
+
|
|
1776
|
+
# If `module_model is None`, it probably means that this module was added by
|
|
1777
|
+
# `ProtocolEngine.use_attached_modules()`, instead of an explicit
|
|
1778
|
+
# `loadModule` command.
|
|
1779
|
+
#
|
|
1780
|
+
# This assert should never raise in practice because:
|
|
1781
|
+
# 1. `ProtocolEngine.use_attached_modules()` is only used by
|
|
1782
|
+
# robot-server's "stateless command" endpoints, under `/commands`.
|
|
1783
|
+
# 2. Those endpoints don't support loading labware, so this code will
|
|
1784
|
+
# never run.
|
|
1785
|
+
#
|
|
1786
|
+
# Nevertheless, if it does happen somehow, we do NOT want to pass the
|
|
1787
|
+
# `None` value along to `LabwareView.find_applicable_labware_offset()`.
|
|
1788
|
+
# `None` means something different there, which will cause us to return
|
|
1789
|
+
# wrong results.
|
|
1790
|
+
assert module_model is not None, (
|
|
1791
|
+
"Can't find offsets for labware"
|
|
1792
|
+
" that are loaded on modules"
|
|
1793
|
+
" that were loaded with ProtocolEngine.use_attached_modules()."
|
|
1794
|
+
)
|
|
1795
|
+
|
|
1796
|
+
module_location = self._modules.get_location(module_id=module_id)
|
|
1797
|
+
if self._modules.get_deck_supports_module_fixtures():
|
|
1798
|
+
module_aa = self._modules.ensure_and_convert_module_fixture_location(
|
|
1799
|
+
module_location.slotName, module_model
|
|
1403
1800
|
)
|
|
1801
|
+
else:
|
|
1802
|
+
module_aa = module_location.slotName.value
|
|
1803
|
+
return building + [
|
|
1804
|
+
OnModuleOffsetLocationSequenceComponent(moduleModel=module_model),
|
|
1805
|
+
OnAddressableAreaOffsetLocationSequenceComponent(
|
|
1806
|
+
addressableAreaName=module_aa
|
|
1807
|
+
),
|
|
1808
|
+
]
|
|
1404
1809
|
|
|
1405
|
-
|
|
1810
|
+
elif isinstance(labware_location, OnLabwareLocation):
|
|
1811
|
+
parent_labware_id = labware_location.labwareId
|
|
1812
|
+
parent_labware = self._get_or_default_labware(
|
|
1813
|
+
parent_labware_id, labware_pending_load
|
|
1814
|
+
)
|
|
1815
|
+
parent_labware_uri = LabwareUri(parent_labware.definitionUri)
|
|
1816
|
+
base_location = parent_labware.location
|
|
1817
|
+
return self._recurse_labware_offset_location(
|
|
1818
|
+
base_location,
|
|
1819
|
+
building
|
|
1820
|
+
+ [
|
|
1821
|
+
OnLabwareOffsetLocationSequenceComponent(
|
|
1822
|
+
labwareUri=parent_labware_uri
|
|
1823
|
+
)
|
|
1824
|
+
],
|
|
1825
|
+
labware_pending_load,
|
|
1826
|
+
)
|
|
1827
|
+
|
|
1828
|
+
else: # Off deck
|
|
1829
|
+
return None
|
|
1830
|
+
|
|
1831
|
+
def get_liquid_handling_z_change(
|
|
1832
|
+
self,
|
|
1833
|
+
labware_id: str,
|
|
1834
|
+
well_name: str,
|
|
1835
|
+
operation_volume: float,
|
|
1836
|
+
) -> float:
|
|
1837
|
+
"""Get the change in height from a liquid handling operation."""
|
|
1838
|
+
initial_handling_height = self.get_meniscus_height(
|
|
1839
|
+
labware_id=labware_id, well_name=well_name
|
|
1840
|
+
)
|
|
1841
|
+
final_height = self.get_well_height_after_liquid_handling(
|
|
1842
|
+
labware_id=labware_id,
|
|
1843
|
+
well_name=well_name,
|
|
1844
|
+
initial_height=initial_handling_height,
|
|
1845
|
+
volume=operation_volume,
|
|
1846
|
+
)
|
|
1847
|
+
# this function is only called by
|
|
1848
|
+
# HardwarePipetteHandler::aspirate/dispense while_tracking, and shouldn't
|
|
1849
|
+
# be reached in the case of a simulated liquid_probe
|
|
1850
|
+
assert not isinstance(initial_handling_height, SimulatedProbeResult)
|
|
1851
|
+
assert not isinstance(final_height, SimulatedProbeResult)
|
|
1852
|
+
return final_height - initial_handling_height
|
|
1406
1853
|
|
|
1407
1854
|
def get_well_offset_adjustment(
|
|
1408
1855
|
self,
|
|
@@ -1411,18 +1858,26 @@ class GeometryView:
|
|
|
1411
1858
|
well_location: WellLocations,
|
|
1412
1859
|
well_depth: float,
|
|
1413
1860
|
operation_volume: Optional[float] = None,
|
|
1414
|
-
) ->
|
|
1861
|
+
) -> LiquidTrackingType:
|
|
1415
1862
|
"""Return a z-axis distance that accounts for well handling height and operation volume.
|
|
1416
1863
|
|
|
1417
1864
|
Distance is with reference to the well bottom.
|
|
1418
1865
|
"""
|
|
1419
1866
|
# TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
|
|
1867
|
+
|
|
1420
1868
|
initial_handling_height = self.get_well_handling_height(
|
|
1421
1869
|
labware_id=labware_id,
|
|
1422
1870
|
well_name=well_name,
|
|
1423
1871
|
well_location=well_location,
|
|
1424
1872
|
well_depth=well_depth,
|
|
1425
1873
|
)
|
|
1874
|
+
# if we're tracking a MENISCUS origin, and targeting either the beginning
|
|
1875
|
+
# position of the liquid or doing dynamic tracking, return the initial height
|
|
1876
|
+
if (
|
|
1877
|
+
well_location.origin == WellOrigin.MENISCUS
|
|
1878
|
+
and not well_location.volumeOffset
|
|
1879
|
+
):
|
|
1880
|
+
return initial_handling_height
|
|
1426
1881
|
if isinstance(well_location, PickUpTipWellLocation):
|
|
1427
1882
|
volume = 0.0
|
|
1428
1883
|
elif isinstance(well_location.volumeOffset, float):
|
|
@@ -1431,32 +1886,85 @@ class GeometryView:
|
|
|
1431
1886
|
volume = operation_volume or 0.0
|
|
1432
1887
|
|
|
1433
1888
|
if volume:
|
|
1434
|
-
|
|
1889
|
+
liquid_height_after = self.get_well_height_after_liquid_handling(
|
|
1435
1890
|
labware_id=labware_id,
|
|
1436
1891
|
well_name=well_name,
|
|
1437
1892
|
initial_height=initial_handling_height,
|
|
1438
1893
|
volume=volume,
|
|
1439
1894
|
)
|
|
1895
|
+
return liquid_height_after
|
|
1440
1896
|
else:
|
|
1441
1897
|
return initial_handling_height
|
|
1442
1898
|
|
|
1899
|
+
def get_current_well_volume(
|
|
1900
|
+
self,
|
|
1901
|
+
labware_id: str,
|
|
1902
|
+
well_name: str,
|
|
1903
|
+
) -> LiquidTrackingType:
|
|
1904
|
+
"""Returns most recently updated volume in specified well."""
|
|
1905
|
+
last_updated = self._wells.get_last_liquid_update(labware_id, well_name)
|
|
1906
|
+
if last_updated is None:
|
|
1907
|
+
raise errors.LiquidHeightUnknownError(
|
|
1908
|
+
"Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS."
|
|
1909
|
+
)
|
|
1910
|
+
|
|
1911
|
+
well_liquid = self._wells.get_well_liquid_info(
|
|
1912
|
+
labware_id=labware_id, well_name=well_name
|
|
1913
|
+
)
|
|
1914
|
+
if (
|
|
1915
|
+
well_liquid.probed_height is not None
|
|
1916
|
+
and well_liquid.probed_height.height is not None
|
|
1917
|
+
and well_liquid.probed_height.last_probed == last_updated
|
|
1918
|
+
):
|
|
1919
|
+
volume = self.get_well_volume_at_height(
|
|
1920
|
+
labware_id=labware_id,
|
|
1921
|
+
well_name=well_name,
|
|
1922
|
+
height=well_liquid.probed_height.height,
|
|
1923
|
+
)
|
|
1924
|
+
return volume
|
|
1925
|
+
elif (
|
|
1926
|
+
well_liquid.loaded_volume is not None
|
|
1927
|
+
and well_liquid.loaded_volume.volume is not None
|
|
1928
|
+
and well_liquid.loaded_volume.last_loaded == last_updated
|
|
1929
|
+
):
|
|
1930
|
+
return well_liquid.loaded_volume.volume
|
|
1931
|
+
elif (
|
|
1932
|
+
well_liquid.probed_volume is not None
|
|
1933
|
+
and well_liquid.probed_volume.volume is not None
|
|
1934
|
+
and well_liquid.probed_volume.last_probed == last_updated
|
|
1935
|
+
):
|
|
1936
|
+
return well_liquid.probed_volume.volume
|
|
1937
|
+
else:
|
|
1938
|
+
# This should not happen if there was an update but who knows
|
|
1939
|
+
raise errors.LiquidVolumeUnknownError(
|
|
1940
|
+
f"Unable to find liquid volume despite an update at {last_updated}."
|
|
1941
|
+
)
|
|
1942
|
+
|
|
1443
1943
|
def get_meniscus_height(
|
|
1444
1944
|
self,
|
|
1445
1945
|
labware_id: str,
|
|
1446
1946
|
well_name: str,
|
|
1447
|
-
) ->
|
|
1947
|
+
) -> LiquidTrackingType:
|
|
1448
1948
|
"""Returns stored meniscus height in specified well."""
|
|
1949
|
+
last_updated = self._wells.get_last_liquid_update(labware_id, well_name)
|
|
1950
|
+
if last_updated is None:
|
|
1951
|
+
raise errors.LiquidHeightUnknownError(
|
|
1952
|
+
"Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS."
|
|
1953
|
+
)
|
|
1954
|
+
|
|
1449
1955
|
well_liquid = self._wells.get_well_liquid_info(
|
|
1450
1956
|
labware_id=labware_id, well_name=well_name
|
|
1451
1957
|
)
|
|
1452
1958
|
if (
|
|
1453
1959
|
well_liquid.probed_height is not None
|
|
1454
1960
|
and well_liquid.probed_height.height is not None
|
|
1961
|
+
and well_liquid.probed_height.last_probed == last_updated
|
|
1455
1962
|
):
|
|
1456
1963
|
return well_liquid.probed_height.height
|
|
1457
1964
|
elif (
|
|
1458
1965
|
well_liquid.loaded_volume is not None
|
|
1459
1966
|
and well_liquid.loaded_volume.volume is not None
|
|
1967
|
+
and well_liquid.loaded_volume.last_loaded == last_updated
|
|
1460
1968
|
):
|
|
1461
1969
|
return self.get_well_height_at_volume(
|
|
1462
1970
|
labware_id=labware_id,
|
|
@@ -1466,6 +1974,7 @@ class GeometryView:
|
|
|
1466
1974
|
elif (
|
|
1467
1975
|
well_liquid.probed_volume is not None
|
|
1468
1976
|
and well_liquid.probed_volume.volume is not None
|
|
1977
|
+
and well_liquid.probed_volume.last_probed == last_updated
|
|
1469
1978
|
):
|
|
1470
1979
|
return self.get_well_height_at_volume(
|
|
1471
1980
|
labware_id=labware_id,
|
|
@@ -1473,8 +1982,9 @@ class GeometryView:
|
|
|
1473
1982
|
volume=well_liquid.probed_volume.volume,
|
|
1474
1983
|
)
|
|
1475
1984
|
else:
|
|
1985
|
+
# This should not happen if there was an update but who knows
|
|
1476
1986
|
raise errors.LiquidHeightUnknownError(
|
|
1477
|
-
"
|
|
1987
|
+
f"Unable to find liquid height despite an update at {last_updated}."
|
|
1478
1988
|
)
|
|
1479
1989
|
|
|
1480
1990
|
def get_well_handling_height(
|
|
@@ -1483,22 +1993,26 @@ class GeometryView:
|
|
|
1483
1993
|
well_name: str,
|
|
1484
1994
|
well_location: WellLocations,
|
|
1485
1995
|
well_depth: float,
|
|
1486
|
-
) ->
|
|
1996
|
+
) -> LiquidTrackingType:
|
|
1487
1997
|
"""Return the handling height for a labware well (with reference to the well bottom)."""
|
|
1488
|
-
handling_height = 0.0
|
|
1998
|
+
handling_height: LiquidTrackingType = 0.0
|
|
1489
1999
|
if well_location.origin == WellOrigin.TOP:
|
|
1490
|
-
handling_height = well_depth
|
|
2000
|
+
handling_height = float(well_depth)
|
|
1491
2001
|
elif well_location.origin == WellOrigin.CENTER:
|
|
1492
|
-
handling_height = well_depth / 2.0
|
|
2002
|
+
handling_height = float(well_depth / 2.0)
|
|
1493
2003
|
elif well_location.origin == WellOrigin.MENISCUS:
|
|
1494
2004
|
handling_height = self.get_meniscus_height(
|
|
1495
2005
|
labware_id=labware_id, well_name=well_name
|
|
1496
2006
|
)
|
|
1497
|
-
return
|
|
2007
|
+
return handling_height
|
|
1498
2008
|
|
|
1499
|
-
def
|
|
1500
|
-
self,
|
|
1501
|
-
|
|
2009
|
+
def get_well_height_after_liquid_handling(
|
|
2010
|
+
self,
|
|
2011
|
+
labware_id: str,
|
|
2012
|
+
well_name: str,
|
|
2013
|
+
initial_height: LiquidTrackingType,
|
|
2014
|
+
volume: float,
|
|
2015
|
+
) -> LiquidTrackingType:
|
|
1502
2016
|
"""Return the height of liquid in a labware well after a given volume has been handled.
|
|
1503
2017
|
|
|
1504
2018
|
This is given an initial handling height, with reference to the well bottom.
|
|
@@ -1514,9 +2028,35 @@ class GeometryView:
|
|
|
1514
2028
|
target_volume=final_volume, well_geometry=well_geometry
|
|
1515
2029
|
)
|
|
1516
2030
|
|
|
2031
|
+
def get_well_height_after_liquid_handling_no_error(
|
|
2032
|
+
self,
|
|
2033
|
+
labware_id: str,
|
|
2034
|
+
well_name: str,
|
|
2035
|
+
initial_height: LiquidTrackingType,
|
|
2036
|
+
volume: float,
|
|
2037
|
+
) -> LiquidTrackingType:
|
|
2038
|
+
"""Return what the height of liquid in a labware well after liquid handling will be.
|
|
2039
|
+
|
|
2040
|
+
This raises no error if the value returned is an invalid physical location, so it should never be
|
|
2041
|
+
used for navigation, only for a pre-emptive estimate.
|
|
2042
|
+
"""
|
|
2043
|
+
well_geometry = self._labware.get_well_geometry(
|
|
2044
|
+
labware_id=labware_id, well_name=well_name
|
|
2045
|
+
)
|
|
2046
|
+
initial_volume = find_volume_at_well_height(
|
|
2047
|
+
target_height=initial_height, well_geometry=well_geometry
|
|
2048
|
+
)
|
|
2049
|
+
final_volume = initial_volume + volume
|
|
2050
|
+
well_volume = find_height_at_well_volume(
|
|
2051
|
+
target_volume=final_volume,
|
|
2052
|
+
well_geometry=well_geometry,
|
|
2053
|
+
raise_error_if_result_invalid=False,
|
|
2054
|
+
)
|
|
2055
|
+
return well_volume
|
|
2056
|
+
|
|
1517
2057
|
def get_well_height_at_volume(
|
|
1518
|
-
self, labware_id: str, well_name: str, volume:
|
|
1519
|
-
) ->
|
|
2058
|
+
self, labware_id: str, well_name: str, volume: LiquidTrackingType
|
|
2059
|
+
) -> LiquidTrackingType:
|
|
1520
2060
|
"""Convert well volume to height."""
|
|
1521
2061
|
well_geometry = self._labware.get_well_geometry(labware_id, well_name)
|
|
1522
2062
|
return find_height_at_well_volume(
|
|
@@ -1524,8 +2064,11 @@ class GeometryView:
|
|
|
1524
2064
|
)
|
|
1525
2065
|
|
|
1526
2066
|
def get_well_volume_at_height(
|
|
1527
|
-
self,
|
|
1528
|
-
|
|
2067
|
+
self,
|
|
2068
|
+
labware_id: str,
|
|
2069
|
+
well_name: str,
|
|
2070
|
+
height: LiquidTrackingType,
|
|
2071
|
+
) -> LiquidTrackingType:
|
|
1529
2072
|
"""Convert well height to volume."""
|
|
1530
2073
|
well_geometry = self._labware.get_well_geometry(labware_id, well_name)
|
|
1531
2074
|
return find_volume_at_well_height(
|
|
@@ -1541,7 +2084,7 @@ class GeometryView:
|
|
|
1541
2084
|
) -> None:
|
|
1542
2085
|
"""Raise InvalidDispenseVolumeError if planned dispense volume will overflow well."""
|
|
1543
2086
|
well_def = self._labware.get_well_definition(labware_id, well_name)
|
|
1544
|
-
well_volumetric_capacity = well_def.totalLiquidVolume
|
|
2087
|
+
well_volumetric_capacity = float(well_def.totalLiquidVolume)
|
|
1545
2088
|
if well_location.origin == WellOrigin.MENISCUS:
|
|
1546
2089
|
# TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
|
|
1547
2090
|
well_geometry = self._labware.get_well_geometry(labware_id, well_name)
|
|
@@ -1551,6 +2094,9 @@ class GeometryView:
|
|
|
1551
2094
|
meniscus_volume = find_volume_at_well_height(
|
|
1552
2095
|
target_height=meniscus_height, well_geometry=well_geometry
|
|
1553
2096
|
)
|
|
2097
|
+
# if meniscus volume is a simulated value, comparisons aren't meaningful
|
|
2098
|
+
if isinstance(meniscus_volume, SimulatedProbeResult):
|
|
2099
|
+
return
|
|
1554
2100
|
remaining_volume = well_volumetric_capacity - meniscus_volume
|
|
1555
2101
|
if volume > remaining_volume:
|
|
1556
2102
|
raise errors.InvalidDispenseVolumeError(
|
|
@@ -1605,3 +2151,40 @@ class GeometryView:
|
|
|
1605
2151
|
target_well_name,
|
|
1606
2152
|
self._labware.get_definition(labware_id).ordering,
|
|
1607
2153
|
)
|
|
2154
|
+
|
|
2155
|
+
def get_height_of_labware_stack(
|
|
2156
|
+
self, definitions: list[LabwareDefinition]
|
|
2157
|
+
) -> float:
|
|
2158
|
+
"""Get the overall height of a stack of labware listed by definition in top-first order."""
|
|
2159
|
+
if len(definitions) == 0:
|
|
2160
|
+
return 0
|
|
2161
|
+
if len(definitions) == 1:
|
|
2162
|
+
return definitions[0].dimensions.zDimension
|
|
2163
|
+
total_height = 0.0
|
|
2164
|
+
upper_def: LabwareDefinition = definitions[0]
|
|
2165
|
+
for lower_def in definitions[1:]:
|
|
2166
|
+
overlap = self._labware.get_labware_overlap_offsets(
|
|
2167
|
+
upper_def, lower_def.parameters.loadName
|
|
2168
|
+
).z
|
|
2169
|
+
total_height += upper_def.dimensions.zDimension - overlap
|
|
2170
|
+
upper_def = lower_def
|
|
2171
|
+
return total_height + upper_def.dimensions.zDimension
|
|
2172
|
+
|
|
2173
|
+
def get_height_of_stacker_labware_pool(self, module_id: str) -> float:
|
|
2174
|
+
"""Get the overall height of a stack of labware in a Stacker module."""
|
|
2175
|
+
stacker = self._modules.get_flex_stacker_substate(module_id)
|
|
2176
|
+
pool_list = stacker.get_pool_definition_ordered_list()
|
|
2177
|
+
if not pool_list:
|
|
2178
|
+
return 0.0
|
|
2179
|
+
return self.get_height_of_labware_stack(pool_list)
|
|
2180
|
+
|
|
2181
|
+
def _get_or_default_labware(
|
|
2182
|
+
self, labware_id: str, pending_labware: dict[str, LoadedLabware]
|
|
2183
|
+
) -> LoadedLabware:
|
|
2184
|
+
try:
|
|
2185
|
+
return self._labware.get(labware_id)
|
|
2186
|
+
except LabwareNotLoadedError as lnle:
|
|
2187
|
+
try:
|
|
2188
|
+
return pending_labware[labware_id]
|
|
2189
|
+
except KeyError as ke:
|
|
2190
|
+
raise lnle from ke
|