opentrons 8.4.0a6__py2.py3-none-any.whl → 8.4.0a8__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/protocol_api/core/engine/instrument.py +4 -4
- opentrons/protocol_api/core/engine/well.py +12 -1
- opentrons/protocol_api/core/instrument.py +1 -1
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +1 -1
- opentrons/protocol_api/core/legacy/legacy_well_core.py +2 -1
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +1 -1
- opentrons/protocol_api/core/well.py +2 -1
- opentrons/protocol_api/instrument_context.py +87 -33
- opentrons/protocol_api/labware.py +3 -1
- opentrons/protocol_api/protocol_context.py +2 -0
- opentrons/protocol_engine/commands/liquid_probe.py +6 -0
- opentrons/protocol_engine/commands/seal_pipette_to_tip.py +44 -23
- opentrons/protocol_engine/commands/unseal_pipette_from_tip.py +8 -11
- opentrons/protocol_engine/errors/__init__.py +2 -0
- opentrons/protocol_engine/errors/exceptions.py +12 -0
- opentrons/protocol_engine/execution/pipetting.py +3 -1
- opentrons/protocol_engine/state/geometry.py +161 -83
- opentrons/protocol_engine/state/modules.py +33 -16
- opentrons/protocol_engine/types/__init__.py +2 -0
- opentrons/protocol_engine/types/liquid_level_detection.py +14 -20
- opentrons/protocol_engine/types/module.py +32 -0
- {opentrons-8.4.0a6.dist-info → opentrons-8.4.0a8.dist-info}/METADATA +4 -4
- {opentrons-8.4.0a6.dist-info → opentrons-8.4.0a8.dist-info}/RECORD +27 -27
- {opentrons-8.4.0a6.dist-info → opentrons-8.4.0a8.dist-info}/LICENSE +0 -0
- {opentrons-8.4.0a6.dist-info → opentrons-8.4.0a8.dist-info}/WHEEL +0 -0
- {opentrons-8.4.0a6.dist-info → opentrons-8.4.0a8.dist-info}/entry_points.txt +0 -0
- {opentrons-8.4.0a6.dist-info → opentrons-8.4.0a8.dist-info}/top_level.txt +0 -0
|
@@ -17,7 +17,10 @@ from opentrons.types import (
|
|
|
17
17
|
MeniscusTrackingTarget,
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
from opentrons_shared_data.errors.exceptions import
|
|
20
|
+
from opentrons_shared_data.errors.exceptions import (
|
|
21
|
+
InvalidStoredData,
|
|
22
|
+
PipetteLiquidNotFoundError,
|
|
23
|
+
)
|
|
21
24
|
from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN
|
|
22
25
|
from opentrons_shared_data.labware.labware_definition import LabwareDefinition
|
|
23
26
|
from opentrons_shared_data.deck.types import CutoutFixture
|
|
@@ -31,6 +34,8 @@ from ..errors import (
|
|
|
31
34
|
LabwareNotLoadedOnModuleError,
|
|
32
35
|
LabwareMovementNotAllowedError,
|
|
33
36
|
OperationLocationNotInWellError,
|
|
37
|
+
InvalidLabwarePositionError,
|
|
38
|
+
LabwareNotOnDeckError,
|
|
34
39
|
)
|
|
35
40
|
from ..errors.exceptions import InvalidLiquidHeightFound
|
|
36
41
|
from ..resources import (
|
|
@@ -373,6 +378,59 @@ class GeometryView:
|
|
|
373
378
|
"Either it has been loaded off-deck or its been moved off-deck."
|
|
374
379
|
)
|
|
375
380
|
|
|
381
|
+
def _get_offset_from_parent_addressable_area(
|
|
382
|
+
self, child_definition: LabwareDefinition, parent: LabwareLocation
|
|
383
|
+
) -> LabwareOffsetVector:
|
|
384
|
+
"""Gets the offset vector of a labware from its eventual parent addressable area.
|
|
385
|
+
|
|
386
|
+
This returns the sum of the offsets for any labware-on-labware pairs plus the
|
|
387
|
+
"base offset", which is (0, 0, 0) in all cases except for modules on the
|
|
388
|
+
OT-2. See
|
|
389
|
+
protocol_engine.state.modules.get_nominal_offset_to_child_from_addressable_area
|
|
390
|
+
for more.
|
|
391
|
+
|
|
392
|
+
This does not incorporate LPC offsets or module calibration offsets.
|
|
393
|
+
"""
|
|
394
|
+
if isinstance(parent, (AddressableAreaLocation, DeckSlotLocation)):
|
|
395
|
+
return LabwareOffsetVector(x=0, y=0, z=0)
|
|
396
|
+
elif isinstance(parent, ModuleLocation):
|
|
397
|
+
module_id = parent.moduleId
|
|
398
|
+
module_model = self._modules.get_connected_model(module_id)
|
|
399
|
+
stacking_overlap = self._labware.get_module_overlap_offsets(
|
|
400
|
+
child_definition, module_model
|
|
401
|
+
)
|
|
402
|
+
module_to_child = (
|
|
403
|
+
self._modules.get_nominal_offset_to_child_from_addressable_area(
|
|
404
|
+
module_id=module_id
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
return LabwareOffsetVector(
|
|
408
|
+
x=module_to_child.x - stacking_overlap.x,
|
|
409
|
+
y=module_to_child.y - stacking_overlap.y,
|
|
410
|
+
z=module_to_child.z - stacking_overlap.z,
|
|
411
|
+
)
|
|
412
|
+
elif isinstance(parent, OnLabwareLocation):
|
|
413
|
+
on_labware = self._labware.get(parent.labwareId)
|
|
414
|
+
on_labware_dimensions = self._labware.get_dimensions(
|
|
415
|
+
labware_id=on_labware.id
|
|
416
|
+
)
|
|
417
|
+
stacking_overlap = self._labware.get_labware_overlap_offsets(
|
|
418
|
+
definition=child_definition, below_labware_name=on_labware.loadName
|
|
419
|
+
)
|
|
420
|
+
labware_offset = LabwareOffsetVector(
|
|
421
|
+
x=stacking_overlap.x,
|
|
422
|
+
y=stacking_overlap.y,
|
|
423
|
+
z=on_labware_dimensions.z - stacking_overlap.z,
|
|
424
|
+
)
|
|
425
|
+
return labware_offset + self._get_offset_from_parent_addressable_area(
|
|
426
|
+
self._labware.get_definition(on_labware.id), on_labware.location
|
|
427
|
+
)
|
|
428
|
+
else:
|
|
429
|
+
raise errors.LabwareNotOnDeckError(
|
|
430
|
+
"Cannot access labware since it is not on the deck. "
|
|
431
|
+
"Either it has been loaded off-deck or it has been moved off-deck."
|
|
432
|
+
)
|
|
433
|
+
|
|
376
434
|
def _normalize_module_calibration_offset(
|
|
377
435
|
self,
|
|
378
436
|
module_location: DeckSlotLocation,
|
|
@@ -498,6 +556,30 @@ class GeometryView:
|
|
|
498
556
|
f"Specifying {well_location.origin} with an offset of {well_location.offset} results in an operation location below the bottom of the well"
|
|
499
557
|
)
|
|
500
558
|
|
|
559
|
+
def validate_probed_height(
|
|
560
|
+
self,
|
|
561
|
+
labware_id: str,
|
|
562
|
+
well_name: str,
|
|
563
|
+
pipette_id: str,
|
|
564
|
+
probed_height: LiquidTrackingType,
|
|
565
|
+
) -> None:
|
|
566
|
+
"""Raise an error if a probed liquid height is not within well bounds."""
|
|
567
|
+
if isinstance(probed_height, SimulatedProbeResult):
|
|
568
|
+
return
|
|
569
|
+
lld_min_height = self._pipettes.get_current_tip_lld_settings(
|
|
570
|
+
pipette_id=pipette_id
|
|
571
|
+
)
|
|
572
|
+
well_def = self._labware.get_well_definition(labware_id, well_name)
|
|
573
|
+
well_depth = well_def.depth
|
|
574
|
+
if probed_height < lld_min_height:
|
|
575
|
+
raise PipetteLiquidNotFoundError(
|
|
576
|
+
f"Liquid Height of {probed_height} mm is lower minumum allowed lld height {lld_min_height} mm."
|
|
577
|
+
)
|
|
578
|
+
if probed_height > well_depth:
|
|
579
|
+
raise PipetteLiquidNotFoundError(
|
|
580
|
+
f"Liquid Height of {probed_height} mm is greater than maximum well height {well_depth} mm."
|
|
581
|
+
)
|
|
582
|
+
|
|
501
583
|
def get_well_position(
|
|
502
584
|
self,
|
|
503
585
|
labware_id: str,
|
|
@@ -520,6 +602,7 @@ class GeometryView:
|
|
|
520
602
|
well_location=well_location,
|
|
521
603
|
well_depth=well_depth,
|
|
522
604
|
operation_volume=operation_volume,
|
|
605
|
+
pipette_id=pipette_id,
|
|
523
606
|
)
|
|
524
607
|
if not isinstance(offset_adjustment, SimulatedProbeResult):
|
|
525
608
|
offset = offset.model_copy(update={"z": offset.z + offset_adjustment})
|
|
@@ -694,6 +777,7 @@ class GeometryView:
|
|
|
694
777
|
labware_id: str,
|
|
695
778
|
well_location: DropTipWellLocation,
|
|
696
779
|
partially_configured: bool = False,
|
|
780
|
+
override_default_offset: float | None = None,
|
|
697
781
|
) -> WellLocation:
|
|
698
782
|
"""Get tip drop location given labware and hardware pipette.
|
|
699
783
|
|
|
@@ -712,8 +796,9 @@ class GeometryView:
|
|
|
712
796
|
origin=WellOrigin(well_location.origin.value),
|
|
713
797
|
offset=well_location.offset,
|
|
714
798
|
)
|
|
715
|
-
|
|
716
|
-
|
|
799
|
+
if override_default_offset is not None:
|
|
800
|
+
z_offset = override_default_offset
|
|
801
|
+
elif self._labware.get_definition(labware_id).parameters.isTiprack:
|
|
717
802
|
z_offset = self._labware.get_tip_drop_z_offset(
|
|
718
803
|
labware_id=labware_id,
|
|
719
804
|
length_scale=self._pipettes.get_return_tip_scale(pipette_id),
|
|
@@ -794,6 +879,32 @@ class GeometryView:
|
|
|
794
879
|
|
|
795
880
|
return slot_name
|
|
796
881
|
|
|
882
|
+
def get_ancestor_addressable_area_name(self, labware_id: str) -> str:
|
|
883
|
+
"""Get the name of the addressable area the labware is eventually on."""
|
|
884
|
+
labware = self._labware.get(labware_id)
|
|
885
|
+
original_display_name = self._labware.get_display_name(labware_id)
|
|
886
|
+
seen: Set[str] = set((labware_id,))
|
|
887
|
+
while isinstance(labware.location, OnLabwareLocation):
|
|
888
|
+
labware = self._labware.get(labware.location.labwareId)
|
|
889
|
+
if labware.id in seen:
|
|
890
|
+
raise InvalidLabwarePositionError(
|
|
891
|
+
f"Cycle detected in labware positioning for {original_display_name}"
|
|
892
|
+
)
|
|
893
|
+
seen.add(labware.id)
|
|
894
|
+
if isinstance(labware.location, DeckSlotLocation):
|
|
895
|
+
return labware.location.slotName.id
|
|
896
|
+
elif isinstance(labware.location, AddressableAreaLocation):
|
|
897
|
+
return labware.location.addressableAreaName
|
|
898
|
+
elif isinstance(labware.location, ModuleLocation):
|
|
899
|
+
return self._modules.get_provided_addressable_area(
|
|
900
|
+
labware.location.moduleId
|
|
901
|
+
)
|
|
902
|
+
else:
|
|
903
|
+
raise LabwareNotOnDeckError(
|
|
904
|
+
f"Labware {original_display_name} is not loaded on deck",
|
|
905
|
+
details={"eventual-location": repr(labware.location)},
|
|
906
|
+
)
|
|
907
|
+
|
|
797
908
|
def ensure_location_not_occupied(
|
|
798
909
|
self,
|
|
799
910
|
location: _LabwareLocation,
|
|
@@ -961,70 +1072,23 @@ class GeometryView:
|
|
|
961
1072
|
self._labware.get_grip_height_from_labware_bottom(labware_definition)
|
|
962
1073
|
)
|
|
963
1074
|
location_name: str
|
|
964
|
-
|
|
965
|
-
|
|
1075
|
+
offset = self._get_offset_from_parent_addressable_area(
|
|
1076
|
+
child_definition=labware_definition, parent=location
|
|
1077
|
+
) + self._get_calibrated_module_offset(location)
|
|
966
1078
|
if isinstance(location, DeckSlotLocation):
|
|
967
1079
|
location_name = location.slotName.id
|
|
968
|
-
offset = LabwareOffsetVector(x=0, y=0, z=0)
|
|
969
1080
|
elif isinstance(location, AddressableAreaLocation):
|
|
970
1081
|
location_name = location.addressableAreaName
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
location_name
|
|
975
|
-
)
|
|
976
|
-
)
|
|
977
|
-
return drop_labware_location + Point(z=grip_height_from_labware_bottom)
|
|
978
|
-
# Location should have been pre-validated so this will be a deck/staging area slot
|
|
979
|
-
else:
|
|
980
|
-
offset = LabwareOffsetVector(x=0, y=0, z=0)
|
|
981
|
-
else:
|
|
982
|
-
if isinstance(location, ModuleLocation):
|
|
983
|
-
location_name = self._modules.get_provided_addressable_area(
|
|
984
|
-
location.moduleId
|
|
985
|
-
)
|
|
986
|
-
module_location = location
|
|
987
|
-
else: # OnLabwareLocation
|
|
988
|
-
labware_loc = self._labware.get(location.labwareId).location
|
|
989
|
-
if isinstance(labware_loc, ModuleLocation):
|
|
990
|
-
location_name = self._modules.get_provided_addressable_area(
|
|
991
|
-
labware_loc.moduleId
|
|
992
|
-
)
|
|
993
|
-
module_location = labware_loc
|
|
994
|
-
else:
|
|
995
|
-
location_name = self.get_ancestor_slot_name(location.labwareId).id
|
|
996
|
-
labware_offset = self._get_offset_from_parent(
|
|
997
|
-
child_definition=labware_definition, parent=location
|
|
998
|
-
)
|
|
999
|
-
# Get the calibrated offset if the on labware location is on top of a module, otherwise return empty one
|
|
1000
|
-
cal_offset = self._get_calibrated_module_offset(location)
|
|
1001
|
-
offset = LabwareOffsetVector(
|
|
1002
|
-
x=labware_offset.x + cal_offset.x,
|
|
1003
|
-
y=labware_offset.y + cal_offset.y,
|
|
1004
|
-
z=labware_offset.z + cal_offset.z,
|
|
1005
|
-
)
|
|
1006
|
-
|
|
1007
|
-
if module_location is not None:
|
|
1008
|
-
# Location center must be determined from the cutout the Module is loaded in
|
|
1009
|
-
position = deck_configuration_provider.get_cutout_position(
|
|
1010
|
-
cutout_id=self._addressable_areas.get_cutout_id_by_deck_slot_name(
|
|
1011
|
-
self._modules.get_location(module_location.moduleId).slotName
|
|
1012
|
-
),
|
|
1013
|
-
deck_definition=self._addressable_areas.deck_definition,
|
|
1014
|
-
)
|
|
1015
|
-
bounding_box = self._addressable_areas.get_addressable_area(
|
|
1016
|
-
location_name
|
|
1017
|
-
).bounding_box
|
|
1018
|
-
location_center = Point(
|
|
1019
|
-
position.x + bounding_box.x / 2,
|
|
1020
|
-
position.y + bounding_box.y / 2,
|
|
1021
|
-
position.z,
|
|
1082
|
+
elif isinstance(location, ModuleLocation):
|
|
1083
|
+
location_name = self._modules.get_provided_addressable_area(
|
|
1084
|
+
location.moduleId
|
|
1022
1085
|
)
|
|
1086
|
+
else: # OnLabwareLocation
|
|
1087
|
+
location_name = self.get_ancestor_addressable_area_name(location.labwareId)
|
|
1023
1088
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
)
|
|
1089
|
+
location_center = self._addressable_areas.get_addressable_area_center(
|
|
1090
|
+
location_name
|
|
1091
|
+
)
|
|
1028
1092
|
|
|
1029
1093
|
return Point(
|
|
1030
1094
|
location_center.x + offset.x,
|
|
@@ -1463,10 +1527,6 @@ class GeometryView:
|
|
|
1463
1527
|
# * The "additional offset" or "user offset", e.g. the `pickUpOffset` and `dropOffset`
|
|
1464
1528
|
# params in the `moveLabware` command.
|
|
1465
1529
|
#
|
|
1466
|
-
# And this *does* take these extra offsets into account:
|
|
1467
|
-
#
|
|
1468
|
-
# * The labware's Labware Position Check offset
|
|
1469
|
-
#
|
|
1470
1530
|
# For robustness, we should combine this with `get_gripper_labware_movement_waypoints()`.
|
|
1471
1531
|
#
|
|
1472
1532
|
# We should also be more explicit about which offsets act to move the gripper paddles
|
|
@@ -1488,23 +1548,17 @@ class GeometryView:
|
|
|
1488
1548
|
return
|
|
1489
1549
|
|
|
1490
1550
|
tip = self._pipettes.get_attached_tip(pipette.id)
|
|
1491
|
-
if tip:
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1551
|
+
if not tip:
|
|
1552
|
+
continue
|
|
1553
|
+
labware_top_z_when_gripped = gripper_homed_position_z + (
|
|
1554
|
+
self._labware.get_dimensions(labware_definition=labware_definition).z
|
|
1555
|
+
- self._labware.get_grip_height_from_labware_bottom(labware_definition)
|
|
1556
|
+
)
|
|
1557
|
+
# 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)
|
|
1558
|
+
if (_PIPETTE_HOMED_POSITION_Z - tip.length) < labware_top_z_when_gripped:
|
|
1559
|
+
raise LabwareMovementNotAllowedError(
|
|
1560
|
+
f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
|
|
1500
1561
|
)
|
|
1501
|
-
# 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)
|
|
1502
|
-
if (
|
|
1503
|
-
_PIPETTE_HOMED_POSITION_Z - tip.length
|
|
1504
|
-
) < labware_top_z_when_gripped:
|
|
1505
|
-
raise LabwareMovementNotAllowedError(
|
|
1506
|
-
f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
|
|
1507
|
-
)
|
|
1508
1562
|
return
|
|
1509
1563
|
|
|
1510
1564
|
def _nominal_gripper_offsets_for_location(
|
|
@@ -1844,6 +1898,7 @@ class GeometryView:
|
|
|
1844
1898
|
self,
|
|
1845
1899
|
labware_id: str,
|
|
1846
1900
|
well_name: str,
|
|
1901
|
+
pipette_id: str,
|
|
1847
1902
|
operation_volume: float,
|
|
1848
1903
|
) -> float:
|
|
1849
1904
|
"""Get the change in height from a liquid handling operation."""
|
|
@@ -1853,6 +1908,7 @@ class GeometryView:
|
|
|
1853
1908
|
final_height = self.get_well_height_after_liquid_handling(
|
|
1854
1909
|
labware_id=labware_id,
|
|
1855
1910
|
well_name=well_name,
|
|
1911
|
+
pipette_id=pipette_id,
|
|
1856
1912
|
initial_height=initial_handling_height,
|
|
1857
1913
|
volume=operation_volume,
|
|
1858
1914
|
)
|
|
@@ -1873,6 +1929,7 @@ class GeometryView:
|
|
|
1873
1929
|
well_name: str,
|
|
1874
1930
|
well_location: WellLocationType,
|
|
1875
1931
|
well_depth: float,
|
|
1932
|
+
pipette_id: Optional[str] = None,
|
|
1876
1933
|
operation_volume: Optional[float] = None,
|
|
1877
1934
|
) -> LiquidTrackingType:
|
|
1878
1935
|
"""Return a z-axis distance that accounts for well handling height and operation volume.
|
|
@@ -1906,9 +1963,14 @@ class GeometryView:
|
|
|
1906
1963
|
volume = well_location.volumeOffset
|
|
1907
1964
|
|
|
1908
1965
|
if volume:
|
|
1966
|
+
if pipette_id is None:
|
|
1967
|
+
raise ValueError(
|
|
1968
|
+
"cannot get liquid handling offset without pipette id."
|
|
1969
|
+
)
|
|
1909
1970
|
liquid_height_after = self.get_well_height_after_liquid_handling(
|
|
1910
1971
|
labware_id=labware_id,
|
|
1911
1972
|
well_name=well_name,
|
|
1973
|
+
pipette_id=pipette_id,
|
|
1912
1974
|
initial_height=initial_handling_height,
|
|
1913
1975
|
volume=volume,
|
|
1914
1976
|
)
|
|
@@ -2030,6 +2092,7 @@ class GeometryView:
|
|
|
2030
2092
|
self,
|
|
2031
2093
|
labware_id: str,
|
|
2032
2094
|
well_name: str,
|
|
2095
|
+
pipette_id: str,
|
|
2033
2096
|
initial_height: LiquidTrackingType,
|
|
2034
2097
|
volume: float,
|
|
2035
2098
|
) -> LiquidTrackingType:
|
|
@@ -2044,7 +2107,14 @@ class GeometryView:
|
|
|
2044
2107
|
initial_volume = find_volume_at_well_height(
|
|
2045
2108
|
target_height=initial_height, well_geometry=well_geometry
|
|
2046
2109
|
)
|
|
2047
|
-
final_volume = initial_volume +
|
|
2110
|
+
final_volume = initial_volume + (
|
|
2111
|
+
volume
|
|
2112
|
+
* self.get_nozzles_per_well(
|
|
2113
|
+
labware_id=labware_id,
|
|
2114
|
+
target_well_name=well_name,
|
|
2115
|
+
pipette_id=pipette_id,
|
|
2116
|
+
)
|
|
2117
|
+
)
|
|
2048
2118
|
return find_height_at_well_volume(
|
|
2049
2119
|
target_volume=final_volume, well_geometry=well_geometry
|
|
2050
2120
|
)
|
|
@@ -2058,6 +2128,7 @@ class GeometryView:
|
|
|
2058
2128
|
self,
|
|
2059
2129
|
labware_id: str,
|
|
2060
2130
|
well_name: str,
|
|
2131
|
+
pipette_id: str,
|
|
2061
2132
|
initial_height: LiquidTrackingType,
|
|
2062
2133
|
volume: float,
|
|
2063
2134
|
) -> LiquidTrackingType:
|
|
@@ -2073,7 +2144,14 @@ class GeometryView:
|
|
|
2073
2144
|
initial_volume = find_volume_at_well_height(
|
|
2074
2145
|
target_height=initial_height, well_geometry=well_geometry
|
|
2075
2146
|
)
|
|
2076
|
-
final_volume = initial_volume +
|
|
2147
|
+
final_volume = initial_volume + (
|
|
2148
|
+
volume
|
|
2149
|
+
* self.get_nozzles_per_well(
|
|
2150
|
+
labware_id=labware_id,
|
|
2151
|
+
target_well_name=well_name,
|
|
2152
|
+
pipette_id=pipette_id,
|
|
2153
|
+
)
|
|
2154
|
+
)
|
|
2077
2155
|
well_volume = find_height_at_well_volume(
|
|
2078
2156
|
target_volume=final_volume,
|
|
2079
2157
|
well_geometry=well_geometry,
|
|
@@ -930,10 +930,39 @@ class ModuleView:
|
|
|
930
930
|
Includes the slot-specific transform. Does not include the child's
|
|
931
931
|
Labware Position Check offset.
|
|
932
932
|
"""
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
933
|
+
base = self.get_nominal_offset_to_child_from_addressable_area(module_id)
|
|
934
|
+
if self.get_deck_supports_module_fixtures():
|
|
935
|
+
module_addressable_area = self.get_provided_addressable_area(module_id)
|
|
936
|
+
module_addressable_area_position = (
|
|
937
|
+
addressable_areas.get_addressable_area_offsets_from_cutout(
|
|
938
|
+
module_addressable_area
|
|
939
|
+
)
|
|
940
|
+
)
|
|
941
|
+
return base + LabwareOffsetVector(
|
|
942
|
+
x=module_addressable_area_position.x,
|
|
943
|
+
y=module_addressable_area_position.y,
|
|
944
|
+
z=module_addressable_area_position.z,
|
|
945
|
+
)
|
|
946
|
+
else:
|
|
947
|
+
return base
|
|
948
|
+
|
|
949
|
+
def get_nominal_offset_to_child_from_addressable_area(
|
|
950
|
+
self, module_id: str
|
|
951
|
+
) -> LabwareOffsetVector:
|
|
952
|
+
"""Get the position offset for a child of this module from the nearest AA.
|
|
953
|
+
|
|
954
|
+
On the Flex, this is always (0, 0, 0); on the OT-2, since modules load on top
|
|
955
|
+
of addressable areas rather than providing addressable areas, the offset is
|
|
956
|
+
the labwareOffset from the module definition, rotated by the module's
|
|
957
|
+
slotTransform if appropriate.
|
|
958
|
+
"""
|
|
959
|
+
if self.get_deck_supports_module_fixtures():
|
|
960
|
+
return LabwareOffsetVector(
|
|
961
|
+
x=0,
|
|
962
|
+
y=0,
|
|
963
|
+
z=0,
|
|
964
|
+
)
|
|
965
|
+
else:
|
|
937
966
|
definition = self.get_definition(module_id)
|
|
938
967
|
slot = self.get_location(module_id).slotName.id
|
|
939
968
|
|
|
@@ -968,18 +997,6 @@ class ModuleView:
|
|
|
968
997
|
y=xformed[1],
|
|
969
998
|
z=xformed[2],
|
|
970
999
|
)
|
|
971
|
-
else:
|
|
972
|
-
module_addressable_area = self.get_provided_addressable_area(module_id)
|
|
973
|
-
module_addressable_area_position = (
|
|
974
|
-
addressable_areas.get_addressable_area_offsets_from_cutout(
|
|
975
|
-
module_addressable_area
|
|
976
|
-
)
|
|
977
|
-
)
|
|
978
|
-
return LabwareOffsetVector(
|
|
979
|
-
x=module_addressable_area_position.x,
|
|
980
|
-
y=module_addressable_area_position.y,
|
|
981
|
-
z=module_addressable_area_position.z,
|
|
982
|
-
)
|
|
983
1000
|
|
|
984
1001
|
def get_module_calibration_offset(
|
|
985
1002
|
self, module_id: str
|
|
@@ -135,6 +135,7 @@ from .liquid_level_detection import (
|
|
|
135
135
|
WellInfoSummary,
|
|
136
136
|
WellLiquidInfo,
|
|
137
137
|
LiquidTrackingType,
|
|
138
|
+
SimulatedProbeResult,
|
|
138
139
|
)
|
|
139
140
|
from .liquid_handling import FlowRates
|
|
140
141
|
from .labware_movement import LabwareMovementStrategy, LabwareMovementOffsetData
|
|
@@ -280,6 +281,7 @@ __all__ = [
|
|
|
280
281
|
"WellInfoSummary",
|
|
281
282
|
"WellLiquidInfo",
|
|
282
283
|
"LiquidTrackingType",
|
|
284
|
+
"SimulatedProbeResult",
|
|
283
285
|
# Liquid handling
|
|
284
286
|
"FlowRates",
|
|
285
287
|
# Labware movement
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Protocol Engine types to do with liquid level detection."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from datetime import datetime
|
|
5
|
-
from typing import Optional, List
|
|
6
|
-
from pydantic import BaseModel, model_serializer,
|
|
6
|
+
from typing import Optional, List, Any
|
|
7
|
+
from pydantic import BaseModel, model_serializer, model_validator
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class SimulatedProbeResult(BaseModel):
|
|
@@ -17,6 +18,14 @@ class SimulatedProbeResult(BaseModel):
|
|
|
17
18
|
"""Serialize instances of this class as a string."""
|
|
18
19
|
return "SimulatedProbeResult"
|
|
19
20
|
|
|
21
|
+
@model_validator(mode="before")
|
|
22
|
+
@classmethod
|
|
23
|
+
def validate_model(cls, data: object) -> Any:
|
|
24
|
+
"""Handle deserializing from a simulated probe result."""
|
|
25
|
+
if isinstance(data, str) and data == "SimulatedProbeResult":
|
|
26
|
+
return {}
|
|
27
|
+
return data
|
|
28
|
+
|
|
20
29
|
def __add__(
|
|
21
30
|
self, other: float | SimulatedProbeResult
|
|
22
31
|
) -> float | SimulatedProbeResult:
|
|
@@ -75,7 +84,9 @@ class SimulatedProbeResult(BaseModel):
|
|
|
75
84
|
self.operations_after_probe.append(volume)
|
|
76
85
|
|
|
77
86
|
|
|
78
|
-
|
|
87
|
+
# Work around https://github.com/pydantic/pydantic/issues/6830 - do not change the order of
|
|
88
|
+
# this union
|
|
89
|
+
LiquidTrackingType = float | SimulatedProbeResult
|
|
79
90
|
|
|
80
91
|
|
|
81
92
|
class LoadedVolumeInfo(BaseModel):
|
|
@@ -104,23 +115,6 @@ class ProbedVolumeInfo(BaseModel):
|
|
|
104
115
|
class WellInfoSummary(BaseModel):
|
|
105
116
|
"""Payload for a well's liquid info in StateSummary."""
|
|
106
117
|
|
|
107
|
-
# TODO(cm): 3/21/25: refactor SimulatedLiquidProbe in a way that
|
|
108
|
-
# doesn't require models like this one that are just using it to
|
|
109
|
-
# need a custom validator
|
|
110
|
-
@field_validator("probed_height", "probed_volume", mode="before")
|
|
111
|
-
@classmethod
|
|
112
|
-
def validate_simulated_probe_result(
|
|
113
|
-
cls, input_val: object
|
|
114
|
-
) -> LiquidTrackingType | None:
|
|
115
|
-
"""Return the appropriate input to WellInfoSummary from json data."""
|
|
116
|
-
if input_val is None:
|
|
117
|
-
return None
|
|
118
|
-
if isinstance(input_val, LiquidTrackingType):
|
|
119
|
-
return input_val
|
|
120
|
-
if isinstance(input_val, str) and input_val == "SimulatedProbeResult":
|
|
121
|
-
return SimulatedProbeResult()
|
|
122
|
-
raise ValueError(f"Invalid input value {input_val} to WellInfoSummary")
|
|
123
|
-
|
|
124
118
|
labware_id: str
|
|
125
119
|
well_name: str
|
|
126
120
|
loaded_volume: Optional[float] = None
|
|
@@ -253,6 +253,38 @@ class ModuleOffsetVector(BaseModel):
|
|
|
253
253
|
y: float
|
|
254
254
|
z: float
|
|
255
255
|
|
|
256
|
+
def __add__(self, other: Any) -> ModuleOffsetVector:
|
|
257
|
+
"""Adds two vectors together."""
|
|
258
|
+
if not isinstance(other, (LabwareOffsetVector, ModuleOffsetVector)):
|
|
259
|
+
return NotImplemented
|
|
260
|
+
return ModuleOffsetVector(
|
|
261
|
+
x=self.x + other.x, y=self.y + other.y, z=self.z + other.z
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def __radd__(self, other: Any) -> ModuleOffsetVector:
|
|
265
|
+
"""Adds two vectors together, the other way."""
|
|
266
|
+
if not isinstance(other, (LabwareOffsetVector, ModuleOffsetVector)):
|
|
267
|
+
return NotImplemented
|
|
268
|
+
return ModuleOffsetVector(
|
|
269
|
+
x=other.x + self.x, y=other.y + self.y, z=other.z + self.z
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def __sub__(self, other: Any) -> ModuleOffsetVector:
|
|
273
|
+
"""Subtracts two vectors."""
|
|
274
|
+
if not isinstance(other, (LabwareOffsetVector, ModuleOffsetVector)):
|
|
275
|
+
return NotImplemented
|
|
276
|
+
return ModuleOffsetVector(
|
|
277
|
+
x=self.x - other.x, y=self.y - other.y, z=self.z - other.z
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def __rsub__(self, other: Any) -> ModuleOffsetVector:
|
|
281
|
+
"""Subtracts two vectors, the other way."""
|
|
282
|
+
if not isinstance(other, (LabwareOffsetVector, ModuleOffsetVector)):
|
|
283
|
+
return NotImplemented
|
|
284
|
+
return ModuleOffsetVector(
|
|
285
|
+
x=other.x - self.x, y=other.y - self.y, z=other.z - self.z
|
|
286
|
+
)
|
|
287
|
+
|
|
256
288
|
|
|
257
289
|
@dataclass
|
|
258
290
|
class ModuleOffsetData:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: opentrons
|
|
3
|
-
Version: 8.4.
|
|
3
|
+
Version: 8.4.0a8
|
|
4
4
|
Summary: The Opentrons API is a simple framework designed to make writing automated biology lab protocols easy.
|
|
5
5
|
Author: Opentrons
|
|
6
6
|
Author-email: engineering@opentrons.com
|
|
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
21
21
|
Classifier: Topic :: Scientific/Engineering
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
23
|
License-File: ../LICENSE
|
|
24
|
-
Requires-Dist: opentrons-shared-data (==8.4.
|
|
24
|
+
Requires-Dist: opentrons-shared-data (==8.4.0a8)
|
|
25
25
|
Requires-Dist: aionotify (==0.3.1)
|
|
26
26
|
Requires-Dist: anyio (<4.0.0,>=3.6.1)
|
|
27
27
|
Requires-Dist: jsonschema (<4.18.0,>=3.0.1)
|
|
@@ -35,9 +35,9 @@ Requires-Dist: pyusb (==1.2.1)
|
|
|
35
35
|
Requires-Dist: packaging (>=21.0)
|
|
36
36
|
Requires-Dist: importlib-metadata (>=1.0) ; python_version < "3.8"
|
|
37
37
|
Provides-Extra: flex-hardware
|
|
38
|
-
Requires-Dist: opentrons-hardware[flex] (==8.4.
|
|
38
|
+
Requires-Dist: opentrons-hardware[flex] (==8.4.0a8) ; extra == 'flex-hardware'
|
|
39
39
|
Provides-Extra: ot2-hardware
|
|
40
|
-
Requires-Dist: opentrons-hardware (==8.4.
|
|
40
|
+
Requires-Dist: opentrons-hardware (==8.4.0a8) ; extra == 'ot2-hardware'
|
|
41
41
|
|
|
42
42
|
.. _Full API Documentation: http://docs.opentrons.com
|
|
43
43
|
|