Skip to content

Commit

Permalink
Merge branch 'edge' into EXEC-643-add-WellVolumeOffset
Browse files Browse the repository at this point in the history
pmoegenburg committed Oct 10, 2024
2 parents f03bcb4 + e33a247 commit 67d53a4
Showing 45 changed files with 1,620 additions and 169 deletions.
8 changes: 2 additions & 6 deletions api/src/opentrons/protocol_engine/commands/pick_up_tip.py
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
from ..errors import ErrorOccurrence, TipNotAttachedError
from ..resources import ModelUtils
from ..state import update_types
from ..types import PickUpTipWellLocation, DeckPoint, TipGeometry
from ..types import PickUpTipWellLocation, DeckPoint
from .pipetting_common import (
PipetteIdMixin,
DestinationPositionResult,
@@ -138,11 +138,7 @@ async def execute(
)
state_update.update_tip_state(
pipette_id=pipette_id,
tip_geometry=TipGeometry(
volume=tip_geometry.volume,
length=tip_geometry.length,
diameter=tip_geometry.diameter,
),
tip_geometry=tip_geometry,
)
except TipNotAttachedError as e:
return DefinedErrorData(
4 changes: 3 additions & 1 deletion api/src/opentrons/protocol_engine/execution/gantry_mover.py
Original file line number Diff line number Diff line change
@@ -273,7 +273,9 @@ def get_max_travel_z(self, pipette_id: str) -> float:
)
else:
instrument_height = VIRTUAL_MAX_OT3_HEIGHT
tip_length = self._state_view.tips.get_tip_length(pipette_id)

tip = self._state_view.pipettes.get_attached_tip(pipette_id=pipette_id)
tip_length = tip.length if tip is not None else 0
return instrument_height - tip_length

async def move_to(
Original file line number Diff line number Diff line change
@@ -126,6 +126,7 @@ async def move_labware_with_gripper(
current_location=current_location,
)

current_labware = self._state_store.labware.get_definition(labware_id)
async with self._thermocycler_plate_lifter.lift_plate_for_labware_movement(
labware_location=current_location
):
@@ -134,6 +135,7 @@ async def move_labware_with_gripper(
from_location=current_location,
to_location=new_location,
additional_offset_vector=user_offset_data,
current_labware=current_labware,
)
)
from_labware_center = self._state_store.geometry.get_labware_grip_point(
Original file line number Diff line number Diff line change
@@ -27,6 +27,11 @@ def validate_definition_is_adapter(definition: LabwareDefinition) -> bool:
return LabwareRole.adapter in definition.allowedRoles


def validate_definition_is_lid(definition: LabwareDefinition) -> bool:
"""Validate that one of the definition's allowed roles is `lid`."""
return LabwareRole.lid in definition.allowedRoles


def validate_labware_can_be_stacked(
top_labware_definition: LabwareDefinition, below_labware_load_name: str
) -> bool:
83 changes: 76 additions & 7 deletions api/src/opentrons/protocol_engine/state/geometry.py
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@
from opentrons_shared_data.deck.types import CutoutFixture
from opentrons_shared_data.pipette import PIPETTE_X_SPAN
from opentrons_shared_data.pipette.types import ChannelCount
from opentrons.protocols.models import LabwareDefinition

from .. import errors
from ..errors import (
@@ -21,7 +22,7 @@
LabwareMovementNotAllowedError,
OperationLocationNotInWellError,
)
from ..resources import fixture_validation
from ..resources import fixture_validation, labware_validation
from ..types import (
OFF_DECK_LOCATION,
LoadedLabware,
@@ -49,6 +50,7 @@
AddressableOffsetVector,
StagingSlotLocation,
LabwareOffsetLocation,
ModuleModel,
)
from .config import Config
from .labware import LabwareView
@@ -1053,17 +1055,22 @@ def get_final_labware_movement_offset_vectors(
from_location: OnDeckLabwareLocation,
to_location: OnDeckLabwareLocation,
additional_offset_vector: LabwareMovementOffsetData,
current_labware: LabwareDefinition,
) -> LabwareMovementOffsetData:
"""Calculate the final labware offset vector to use in labware movement."""
pick_up_offset = (
self.get_total_nominal_gripper_offset_for_move_type(
location=from_location, move_type=_GripperMoveType.PICK_UP_LABWARE
location=from_location,
move_type=_GripperMoveType.PICK_UP_LABWARE,
current_labware=current_labware,
)
+ additional_offset_vector.pickUpOffset
)
drop_offset = (
self.get_total_nominal_gripper_offset_for_move_type(
location=to_location, move_type=_GripperMoveType.DROP_LABWARE
location=to_location,
move_type=_GripperMoveType.DROP_LABWARE,
current_labware=current_labware,
)
+ additional_offset_vector.dropOffset
)
@@ -1094,7 +1101,10 @@ def ensure_valid_gripper_location(
return location

def get_total_nominal_gripper_offset_for_move_type(
self, location: OnDeckLabwareLocation, move_type: _GripperMoveType
self,
location: OnDeckLabwareLocation,
move_type: _GripperMoveType,
current_labware: LabwareDefinition,
) -> LabwareOffsetVector:
"""Get the total of the offsets to be used to pick up labware in its current location."""
if move_type == _GripperMoveType.PICK_UP_LABWARE:
@@ -1110,14 +1120,39 @@ def get_total_nominal_gripper_offset_for_move_type(
location
)
ancestor = self._labware.get_parent_location(location.labwareId)
extra_offset = LabwareOffsetVector(x=0, y=0, z=0)
if (
isinstance(ancestor, ModuleLocation)
and self._modules._state.requested_model_by_id[ancestor.moduleId]
== ModuleModel.THERMOCYCLER_MODULE_V2
and labware_validation.validate_definition_is_lid(current_labware)
):
if "lidOffsets" in current_labware.gripperOffsets.keys():
extra_offset = LabwareOffsetVector(
x=current_labware.gripperOffsets[
"lidOffsets"
].pickUpOffset.x,
y=current_labware.gripperOffsets[
"lidOffsets"
].pickUpOffset.y,
z=current_labware.gripperOffsets[
"lidOffsets"
].pickUpOffset.z,
)
else:
raise errors.LabwareOffsetDoesNotExistError(
f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
)

assert isinstance(
ancestor, (DeckSlotLocation, ModuleLocation)
ancestor, (DeckSlotLocation, ModuleLocation, OnLabwareLocation)
), "No gripper offsets for off-deck labware"
return (
direct_parent_offset.pickUpOffset
+ self._nominal_gripper_offsets_for_location(
location=ancestor
).pickUpOffset
+ extra_offset
)
else:
if isinstance(
@@ -1132,14 +1167,39 @@ def get_total_nominal_gripper_offset_for_move_type(
location
)
ancestor = self._labware.get_parent_location(location.labwareId)
extra_offset = LabwareOffsetVector(x=0, y=0, z=0)
if (
isinstance(ancestor, ModuleLocation)
and self._modules._state.requested_model_by_id[ancestor.moduleId]
== ModuleModel.THERMOCYCLER_MODULE_V2
and labware_validation.validate_definition_is_lid(current_labware)
):
if "lidOffsets" in current_labware.gripperOffsets.keys():
extra_offset = LabwareOffsetVector(
x=current_labware.gripperOffsets[
"lidOffsets"
].pickUpOffset.x,
y=current_labware.gripperOffsets[
"lidOffsets"
].pickUpOffset.y,
z=current_labware.gripperOffsets[
"lidOffsets"
].pickUpOffset.z,
)
else:
raise errors.LabwareOffsetDoesNotExistError(
f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
)

assert isinstance(
ancestor, (DeckSlotLocation, ModuleLocation)
ancestor, (DeckSlotLocation, ModuleLocation, OnLabwareLocation)
), "No gripper offsets for off-deck labware"
return (
direct_parent_offset.dropOffset
+ self._nominal_gripper_offsets_for_location(
location=ancestor
).dropOffset
+ extra_offset
)

def check_gripper_labware_tip_collision(
@@ -1203,11 +1263,20 @@ def _labware_gripper_offsets(
"""
parent_location = self._labware.get_parent_location(labware_id)
assert isinstance(
parent_location, (DeckSlotLocation, ModuleLocation)
parent_location,
(
DeckSlotLocation,
ModuleLocation,
AddressableAreaLocation,
),
), "No gripper offsets for off-deck labware"

if isinstance(parent_location, DeckSlotLocation):
slot_name = parent_location.slotName
elif isinstance(parent_location, AddressableAreaLocation):
slot_name = self._addressable_areas.get_addressable_area_base_slot(
parent_location.addressableAreaName
)
else:
module_loc = self._modules.get_location(parent_location.moduleId)
slot_name = module_loc.slotName
66 changes: 61 additions & 5 deletions api/src/opentrons/protocol_engine/state/labware.py
Original file line number Diff line number Diff line change
@@ -408,6 +408,16 @@ def get_parent_location(self, labware_id: str) -> NonStackedLocation:
return self.get_parent_location(parent.labwareId)
return parent

def get_labware_stack(
self, labware_stack: List[LoadedLabware]
) -> List[LoadedLabware]:
"""Get the a stack of labware starting from a given labware or existing stack."""
parent = self.get_location(labware_stack[-1].id)
if isinstance(parent, OnLabwareLocation):
labware_stack.append(self.get(parent.labwareId))
return self.get_labware_stack(labware_stack)
return labware_stack

def get_all(self) -> List[LoadedLabware]:
"""Get a list of all labware entries in state."""
return list(self._state.labware_by_id.values())
@@ -432,6 +442,27 @@ def get_should_center_column_on_target_well(self, labware_id: str) -> bool:
and len(self.get_definition(labware_id).wells) < 96
)

def get_labware_stacking_maximum(self, labware: LabwareDefinition) -> int:
"""Returns the maximum number of labware allowed in a stack for a given labware definition.
If not defined within a labware, defaults to one.
"""
stacking_quirks = {
"stackingMaxFive": 5,
"stackingMaxFour": 4,
"stackingMaxThree": 3,
"stackingMaxTwo": 2,
"stackingMaxOne": 1,
"stackingMaxZero": 0,
}
for quirk in stacking_quirks.keys():
if (
labware.parameters.quirks is not None
and quirk in labware.parameters.quirks
):
return stacking_quirks[quirk]
return 1

def get_should_center_pipette_on_target_well(self, labware_id: str) -> bool:
"""True if a pipette moving to a well of this labware should center its body on the target.
@@ -622,9 +653,14 @@ def get_labware_overlap_offsets(
) -> OverlapOffset:
"""Get the labware's overlap with requested labware's load name."""
definition = self.get_definition(labware_id)
stacking_overlap = definition.stackingOffsetWithLabware.get(
below_labware_name, OverlapOffset(x=0, y=0, z=0)
)
if below_labware_name in definition.stackingOffsetWithLabware.keys():
stacking_overlap = definition.stackingOffsetWithLabware.get(
below_labware_name, OverlapOffset(x=0, y=0, z=0)
)
else:
stacking_overlap = definition.stackingOffsetWithLabware.get(
"default", OverlapOffset(x=0, y=0, z=0)
)
return OverlapOffset(
x=stacking_overlap.x, y=stacking_overlap.y, z=stacking_overlap.z
)
@@ -793,7 +829,7 @@ def raise_if_labware_in_location(
f"Labware {labware.loadName} is already present at {location}."
)

def raise_if_labware_cannot_be_stacked(
def raise_if_labware_cannot_be_stacked( # noqa: C901
self, top_labware_definition: LabwareDefinition, bottom_labware_id: str
) -> None:
"""Raise if the specified labware definition cannot be placed on top of the bottom labware."""
@@ -812,17 +848,37 @@ def raise_if_labware_cannot_be_stacked(
)
elif isinstance(below_labware.location, ModuleLocation):
below_definition = self.get_definition(labware_id=below_labware.id)
if not labware_validation.validate_definition_is_adapter(below_definition):
if not labware_validation.validate_definition_is_adapter(
below_definition
) and not labware_validation.validate_definition_is_lid(
top_labware_definition
):
raise errors.LabwareCannotBeStackedError(
f"Labware {top_labware_definition.parameters.loadName} cannot be loaded"
f" onto a labware on top of a module"
)
elif isinstance(below_labware.location, OnLabwareLocation):
labware_stack = self.get_labware_stack([below_labware])
stack_without_adapters = []
for lw in labware_stack:
if not labware_validation.validate_definition_is_adapter(
self.get_definition(lw.id)
):
stack_without_adapters.append(lw)
if len(stack_without_adapters) >= self.get_labware_stacking_maximum(
top_labware_definition
):
raise errors.LabwareCannotBeStackedError(
f"Labware {top_labware_definition.parameters.loadName} cannot be loaded to stack of more than {self.get_labware_stacking_maximum(top_labware_definition)} labware."
)

further_below_definition = self.get_definition(
labware_id=below_labware.location.labwareId
)
if labware_validation.validate_definition_is_adapter(
further_below_definition
) and not labware_validation.validate_definition_is_lid(
top_labware_definition
):
raise errors.LabwareCannotBeStackedError(
f"Labware {top_labware_definition.parameters.loadName} cannot be loaded"
10 changes: 1 addition & 9 deletions api/src/opentrons/protocol_engine/state/tips.py
Original file line number Diff line number Diff line change
@@ -44,8 +44,8 @@ class TipState:

tips_by_labware_id: Dict[str, TipRackStateByWellName]
column_by_labware_id: Dict[str, List[List[str]]]

channels_by_pipette_id: Dict[str, int]
length_by_pipette_id: Dict[str, float]
active_channels_by_pipette_id: Dict[str, int]
nozzle_map_by_pipette_id: Dict[str, NozzleMap]

@@ -61,7 +61,6 @@ def __init__(self) -> None:
tips_by_labware_id={},
column_by_labware_id={},
channels_by_pipette_id={},
length_by_pipette_id={},
active_channels_by_pipette_id={},
nozzle_map_by_pipette_id={},
)
@@ -121,18 +120,15 @@ def _handle_succeeded_command(self, command: Command) -> None:
labware_id = command.params.labwareId
well_name = command.params.wellName
pipette_id = command.params.pipetteId
length = command.result.tipLength
self._set_used_tips(
pipette_id=pipette_id, well_name=well_name, labware_id=labware_id
)
self._state.length_by_pipette_id[pipette_id] = length

elif isinstance(
command.result,
(DropTipResult, DropTipInPlaceResult, unsafe.UnsafeDropTipInPlaceResult),
):
pipette_id = command.params.pipetteId
self._state.length_by_pipette_id.pop(pipette_id, None)

def _handle_failed_command(
self,
@@ -506,10 +502,6 @@ def has_clean_tip(self, labware_id: str, well_name: str) -> bool:

return well_state == TipRackWellState.CLEAN

def get_tip_length(self, pipette_id: str) -> float:
"""Return the given pipette's tip length."""
return self._state.length_by_pipette_id.get(pipette_id, 0)


def _drop_wells_before_starting_tip(
wells: TipRackStateByWellName, starting_tip_name: str
Loading

0 comments on commit 67d53a4

Please sign in to comment.