diff --git a/api-client/src/maintenance_runs/types.ts b/api-client/src/maintenance_runs/types.ts index 6696e3ba072..e17c834cd16 100644 --- a/api-client/src/maintenance_runs/types.ts +++ b/api-client/src/maintenance_runs/types.ts @@ -6,7 +6,7 @@ import type { } from '@opentrons/shared-data' import type { RunCommandSummary, - LabwareOffsetCreateData, + LegacyLabwareOffsetCreateData, RunStatus, RunAction, } from '../runs' @@ -42,7 +42,7 @@ export interface MaintenanceRunError { } export interface CreateMaintenanceRunData { - labwareOffsets?: LabwareOffsetCreateData[] + labwareOffsets?: LegacyLabwareOffsetCreateData[] } export interface LabwareDefinitionSummary { diff --git a/api-client/src/runs/createLabwareOffset.ts b/api-client/src/runs/createLabwareOffset.ts index 29da8b61922..0b91566cf46 100644 --- a/api-client/src/runs/createLabwareOffset.ts +++ b/api-client/src/runs/createLabwareOffset.ts @@ -2,14 +2,14 @@ import { POST, request } from '../request' import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' -import type { LabwareOffsetCreateData, Run } from './types' +import type { LegacyLabwareOffsetCreateData, Run } from './types' export function createLabwareOffset( config: HostConfig, runId: string, - data: LabwareOffsetCreateData + data: LegacyLabwareOffsetCreateData ): ResponsePromise { - return request( + return request( POST, `/runs/${runId}/labware_offsets`, { data }, diff --git a/api-client/src/runs/createRun.ts b/api-client/src/runs/createRun.ts index 6e8cd4b7525..e825e55e665 100644 --- a/api-client/src/runs/createRun.ts +++ b/api-client/src/runs/createRun.ts @@ -4,14 +4,14 @@ import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' import type { Run, - LabwareOffsetCreateData, + LegacyLabwareOffsetCreateData, RunTimeParameterValuesCreateData, RunTimeParameterFilesCreateData, } from './types' export interface CreateRunData { protocolId?: string - labwareOffsets?: LabwareOffsetCreateData[] + labwareOffsets?: LegacyLabwareOffsetCreateData[] runTimeParameterValues?: RunTimeParameterValuesCreateData runTimeParameterFiles?: RunTimeParameterFilesCreateData } diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index ea24c040ebc..1de9b97717f 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -85,7 +85,8 @@ export interface LabwareOffset { id: string createdAt: string definitionUri: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation + locationSequence?: LabwareOffsetLocationSequence vector: VectorOffset } @@ -156,14 +157,35 @@ export interface CreateRunActionData { actionType: RunActionType } -export interface LabwareOffsetLocation { +export interface OnAddressableAreaLabwareOffsetLocationSequenceComponent { + kind: 'onAddressableArea' + labware: string +} + +export interface OnModuleOffsetLocationSequenceComponent { + kind: 'onModule' + moduleModel: ModuleModel +} + +export interface OnLabwareOffsetLocationSequenceComponent { + kind: 'onLabware' + labwareUri: string +} + +export type LabwareOffsetLocationSequenceComponent = + | OnAddressableAreaLabwareOffsetLocationSequenceComponent + | OnModuleOffsetLocationSequenceComponent + | OnLabwareOffsetLocationSequenceComponent +export type LabwareOffsetLocationSequence = LabwareOffsetLocationSequenceComponent[] + +export interface LegacyLabwareOffsetLocation { slotName: string moduleModel?: ModuleModel definitionUri?: string } -export interface LabwareOffsetCreateData { +export interface LegacyLabwareOffsetCreateData { definitionUri: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation vector: VectorOffset } diff --git a/api/src/opentrons/protocol_api/core/engine/labware.py b/api/src/opentrons/protocol_api/core/engine/labware.py index d462401927f..6a9ea743355 100644 --- a/api/src/opentrons/protocol_api/core/engine/labware.py +++ b/api/src/opentrons/protocol_api/core/engine/labware.py @@ -122,7 +122,7 @@ def set_calibration(self, delta: Point) -> None: request = LabwareOffsetCreate.model_construct( definitionUri=self.get_uri(), - location=offset_location, + locationSequence=offset_location, vector=LabwareOffsetVector(x=delta.x, y=delta.y, z=delta.z), ) self._engine_client.add_labware_offset(request) diff --git a/api/src/opentrons/protocol_api/core/legacy/labware_offset_provider.py b/api/src/opentrons/protocol_api/core/legacy/labware_offset_provider.py index b808cc95add..e0a4b4f6bd2 100644 --- a/api/src/opentrons/protocol_api/core/legacy/labware_offset_provider.py +++ b/api/src/opentrons/protocol_api/core/legacy/labware_offset_provider.py @@ -3,7 +3,11 @@ from typing import Optional from opentrons.hardware_control.modules import ModuleModel as HardwareModuleModel -from opentrons.protocol_engine import ProtocolEngine, LabwareOffsetLocation, ModuleModel +from opentrons.protocol_engine import ( + ProtocolEngine, + LegacyLabwareOffsetLocation, + ModuleModel, +) from opentrons.types import DeckSlotName, Point from ..labware import LabwareLoadParams @@ -81,9 +85,9 @@ def find( See the parent class for param details. """ - offset = self._labware_view.find_applicable_labware_offset( + offset = self._labware_view.find_applicable_labware_offset_by_legacy_location( definition_uri=load_params.as_uri(), - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=deck_slot, moduleModel=( None diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py index 4b5b2a1f3c3..5e90c7235bf 100644 --- a/api/src/opentrons/protocol_engine/__init__.py +++ b/api/src/opentrons/protocol_engine/__init__.py @@ -26,9 +26,10 @@ from .types import ( LabwareOffset, + LegacyLabwareOffsetCreate, LabwareOffsetCreate, LabwareOffsetVector, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, LabwareMovementStrategy, AddressableOffsetVector, DeckPoint, @@ -96,7 +97,8 @@ "LabwareOffset", "LabwareOffsetCreate", "LabwareOffsetVector", - "LabwareOffsetLocation", + "LegacyLabwareOffsetCreate", + "LegacyLabwareOffsetLocation", "LabwareMovementStrategy", "AddressableOffsetVector", "DeckSlotLocation", diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index 0ec505d68e6..680994ce70c 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -23,7 +23,7 @@ from ..notes.notes import CommandNote from ..state.update_types import StateUpdate from ..types import ( - LabwareOffsetCreate, + LabwareOffsetCreateInternal, ModuleDefinition, Liquid, DeckConfigurationType, @@ -206,7 +206,7 @@ class AddLabwareOffsetAction: labware_offset_id: str created_at: datetime - request: LabwareOffsetCreate + request: LabwareOffsetCreateInternal @dataclasses.dataclass(frozen=True) diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index 2b0fb6a6060..85d89e8e2fb 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -82,6 +82,7 @@ InvalidLiquidError, LiquidClassDoesNotExistError, LiquidClassRedefinitionError, + OffsetLocationInvalidError, ) from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError @@ -160,6 +161,7 @@ "LocationIsLidDockSlotError", "InvalidAxisForRobotType", "NotSupportedOnRobotType", + "OffsetLocationInvalidError", # error occurrence models "ErrorOccurrence", "CommandNotAllowedError", diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index c3fddf99a61..3aa7c0562ab 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -433,6 +433,19 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) +class OffsetLocationInvalidError(ProtocolEngineError): + """Raised when encountering an invalid labware offset location sequence.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build an OffsetLocationSequenceDoesNotTerminateAtAnAddressableAreaError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + class SlotDoesNotExistError(ProtocolEngineError): """Raised when referencing a deck slot that does not exist.""" diff --git a/api/src/opentrons/protocol_engine/execution/equipment.py b/api/src/opentrons/protocol_engine/execution/equipment.py index b16e26cd6ae..4a487247e08 100644 --- a/api/src/opentrons/protocol_engine/execution/equipment.py +++ b/api/src/opentrons/protocol_engine/execution/equipment.py @@ -1,4 +1,5 @@ """Equipment command side-effect logic.""" + from dataclasses import dataclass from typing import Optional, overload, Union, List @@ -42,10 +43,7 @@ from ..types import ( LabwareLocation, DeckSlotLocation, - ModuleLocation, - OnLabwareLocation, LabwareOffset, - LabwareOffsetLocation, ModuleModel, ModuleDefinition, AddressableAreaLocation, @@ -633,8 +631,9 @@ def find_applicable_labware_offset_id( or None if no labware offset will apply. """ labware_offset_location = ( - self._get_labware_offset_location_from_labware_location(labware_location) + self._state_store.geometry.get_projected_offset_location(labware_location) ) + if labware_offset_location is None: # No offset for off-deck location. # Returning None instead of raising an exception allows loading a labware @@ -647,72 +646,6 @@ def find_applicable_labware_offset_id( ) return self._get_id_from_offset(offset) - def _get_labware_offset_location_from_labware_location( - self, labware_location: LabwareLocation - ) -> Optional[LabwareOffsetLocation]: - if isinstance(labware_location, DeckSlotLocation): - return LabwareOffsetLocation(slotName=labware_location.slotName) - elif isinstance(labware_location, ModuleLocation): - module_id = labware_location.moduleId - # Allow ModuleNotLoadedError to propagate. - # Note also that we match based on the module's requested model, not its - # actual model, to implement robot-server's documented HTTP API semantics. - module_model = self._state_store.modules.get_requested_model( - module_id=module_id - ) - - # If `module_model is None`, it probably means that this module was added by - # `ProtocolEngine.use_attached_modules()`, instead of an explicit - # `loadModule` command. - # - # This assert should never raise in practice because: - # 1. `ProtocolEngine.use_attached_modules()` is only used by - # robot-server's "stateless command" endpoints, under `/commands`. - # 2. Those endpoints don't support loading labware, so this code will - # never run. - # - # Nevertheless, if it does happen somehow, we do NOT want to pass the - # `None` value along to `LabwareView.find_applicable_labware_offset()`. - # `None` means something different there, which will cause us to return - # wrong results. - assert module_model is not None, ( - "Can't find offsets for labware" - " that are loaded on modules" - " that were loaded with ProtocolEngine.use_attached_modules()." - ) - - module_location = self._state_store.modules.get_location( - module_id=module_id - ) - slot_name = module_location.slotName - return LabwareOffsetLocation(slotName=slot_name, moduleModel=module_model) - elif isinstance(labware_location, OnLabwareLocation): - parent_labware_id = labware_location.labwareId - parent_labware_uri = self._state_store.labware.get_definition_uri( - parent_labware_id - ) - - base_location = self._state_store.labware.get_parent_location( - parent_labware_id - ) - base_labware_offset_location = ( - self._get_labware_offset_location_from_labware_location(base_location) - ) - if base_labware_offset_location is None: - # No offset for labware sitting on labware off-deck - return None - - # If labware is being stacked on itself, all labware in the stack will share a labware offset due to - # them sharing the same definitionUri in `LabwareOffsetLocation`. This will not be true for the - # bottom-most labware, which will have a `DeckSlotLocation` and have its definitionUri field empty. - return LabwareOffsetLocation( - slotName=base_labware_offset_location.slotName, - moduleModel=base_labware_offset_location.moduleModel, - definitionUri=parent_labware_uri, - ) - else: # Off deck - return None - @staticmethod def _get_id_from_offset(labware_offset: Optional[LabwareOffset]) -> Optional[str]: return None if labware_offset is None else labware_offset.id diff --git a/api/src/opentrons/protocol_engine/labware_offset_standardization.py b/api/src/opentrons/protocol_engine/labware_offset_standardization.py new file mode 100644 index 00000000000..836d40cb700 --- /dev/null +++ b/api/src/opentrons/protocol_engine/labware_offset_standardization.py @@ -0,0 +1,172 @@ +"""Convert labware offset creation requests and stored elements between legacy and new.""" + +from opentrons_shared_data.robot.types import RobotType +from opentrons_shared_data.deck.types import DeckDefinitionV5 +from .errors import ( + OffsetLocationInvalidError, + FixtureDoesNotExistError, +) +from .types import ( + LabwareOffsetCreate, + LegacyLabwareOffsetCreate, + LabwareOffsetCreateInternal, + LegacyLabwareOffsetLocation, + LabwareOffsetLocationSequence, + OnLabwareOffsetLocationSequenceComponent, + OnAddressableAreaOffsetLocationSequenceComponent, + OnModuleOffsetLocationSequenceComponent, + ModuleModel, +) +from .resources import deck_configuration_provider + + +def standardize_labware_offset_create( + request: LabwareOffsetCreate | LegacyLabwareOffsetCreate, + robot_type: RobotType, + deck_definition: DeckDefinitionV5, +) -> LabwareOffsetCreateInternal: + """Turn a union of old and new labware offset create requests into a new one.""" + location_sequence, legacy_location = _locations_for_create( + request, robot_type, deck_definition + ) + return LabwareOffsetCreateInternal( + definitionUri=request.definitionUri, + locationSequence=location_sequence, + legacyLocation=legacy_location, + vector=request.vector, + ) + + +def _legacy_offset_location_to_offset_location_sequence( + location: LegacyLabwareOffsetLocation, deck_definition: DeckDefinitionV5 +) -> LabwareOffsetLocationSequence: + sequence: LabwareOffsetLocationSequence = [] + if location.definitionUri: + sequence.append( + OnLabwareOffsetLocationSequenceComponent(labwareUri=location.definitionUri) + ) + if location.moduleModel: + sequence.append( + OnModuleOffsetLocationSequenceComponent(moduleModel=location.moduleModel) + ) + cutout_id = deck_configuration_provider.get_cutout_id_by_deck_slot_name( + location.slotName + ) + possible_cutout_fixture_id = location.moduleModel.value + try: + addressable_area = deck_configuration_provider.get_labware_hosting_addressable_area_name_for_cutout_and_cutout_fixture( + cutout_id, possible_cutout_fixture_id, deck_definition + ) + sequence.append( + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName=addressable_area + ) + ) + except FixtureDoesNotExistError: + # this is an OT-2 (or this module isn't supported in the deck definition) and we should use a + # slot addressable area name + sequence.append( + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName=location.slotName.value + ) + ) + + else: + # Slight hack: we should have a more formal association here. However, since the slot + # name is already standardized, and since the addressable areas for slots are just the + # name of the slots, we can rely on this. + sequence.append( + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName=location.slotName.value + ) + ) + return sequence + + +def _offset_location_sequence_head_to_labware_and_module( + location_sequence: LabwareOffsetLocationSequence, +) -> tuple[ModuleModel | None, str | None]: + labware_uri: str | None = None + module_model: ModuleModel | None = None + for location in location_sequence: + if isinstance(location, OnAddressableAreaOffsetLocationSequenceComponent): + raise OffsetLocationInvalidError( + "Addressable areas may only be the final element of an offset location." + ) + elif isinstance(location, OnLabwareOffsetLocationSequenceComponent): + if labware_uri is not None: + # We only take the first location + continue + if module_model is not None: + # Labware can't be underneath modules + raise OffsetLocationInvalidError( + "Labware must not be underneath a module." + ) + labware_uri = location.labwareUri + elif isinstance(location, OnModuleOffsetLocationSequenceComponent): + if module_model is not None: + # Bad, somebody put more than one module in here + raise OffsetLocationInvalidError( + "Only one module location may exist in an offset location." + ) + module_model = location.moduleModel + else: + raise OffsetLocationInvalidError( + f"Invalid location component in offset location: {repr(location)}" + ) + return module_model, labware_uri + + +def _offset_location_sequence_to_legacy_offset_location( + location_sequence: LabwareOffsetLocationSequence, deck_definition: DeckDefinitionV5 +) -> LegacyLabwareOffsetLocation: + if len(location_sequence) == 0: + raise OffsetLocationInvalidError( + "Offset locations must contain at least one component." + ) + last_element = location_sequence[-1] + if not isinstance(last_element, OnAddressableAreaOffsetLocationSequenceComponent): + raise OffsetLocationInvalidError( + "Offset locations must end with an addressable area." + ) + module_model, labware_uri = _offset_location_sequence_head_to_labware_and_module( + location_sequence[:-1] + ) + ( + cutout_id, + cutout_fixtures, + ) = deck_configuration_provider.get_potential_cutout_fixtures( + last_element.addressableAreaName, deck_definition + ) + slot_name = deck_configuration_provider.get_deck_slot_for_cutout_id(cutout_id) + return LegacyLabwareOffsetLocation( + slotName=slot_name, moduleModel=module_model, definitionUri=labware_uri + ) + + +def _locations_for_create( + request: LabwareOffsetCreate | LegacyLabwareOffsetCreate, + robot_type: RobotType, + deck_definition: DeckDefinitionV5, +) -> tuple[LabwareOffsetLocationSequence, LegacyLabwareOffsetLocation]: + if isinstance(request, LabwareOffsetCreate): + return ( + request.locationSequence, + _offset_location_sequence_to_legacy_offset_location( + request.locationSequence, deck_definition + ), + ) + else: + normalized = request.location.model_copy( + update={ + "slotName": request.location.slotName.to_equivalent_for_robot_type( + robot_type + ) + } + ) + return ( + _legacy_offset_location_to_offset_location_sequence( + normalized, deck_definition + ), + normalized, + ) diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index d1636d18001..04579efc590 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -1,4 +1,5 @@ """ProtocolEngine class definition.""" + from contextlib import AsyncExitStack from logging import getLogger from typing import Dict, Optional, Union, AsyncGenerator, Callable @@ -20,11 +21,12 @@ from .errors import ProtocolCommandFailedError, ErrorOccurrence, CommandNotAllowedError from .errors.exceptions import EStopActivatedError from .error_recovery_policy import ErrorRecoveryPolicy -from . import commands, slot_standardization +from . import commands, slot_standardization, labware_offset_standardization from .resources import ModelUtils, ModuleDataProvider, FileProvider from .types import ( LabwareOffset, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, LabwareUri, ModuleModel, Liquid, @@ -517,15 +519,21 @@ async def finish( ) ) - def add_labware_offset(self, request: LabwareOffsetCreate) -> LabwareOffset: + def add_labware_offset( + self, request: LabwareOffsetCreate | LegacyLabwareOffsetCreate + ) -> LabwareOffset: """Add a new labware offset and return it. The added offset will apply to subsequent `LoadLabwareCommand`s. To retrieve offsets later, see `.state_view.labware`. """ - request = slot_standardization.standardize_labware_offset( - request, self.state_view.config.robot_type + internal_request = ( + labware_offset_standardization.standardize_labware_offset_create( + request, + self.state_view.config.robot_type, + self.state_view.addressable_areas.deck_definition, + ) ) labware_offset_id = self._model_utils.generate_id() @@ -534,7 +542,7 @@ def add_labware_offset(self, request: LabwareOffsetCreate) -> LabwareOffset: AddLabwareOffsetAction( labware_offset_id=labware_offset_id, created_at=created_at, - request=request, + request=internal_request, ) ) return self.state_view.labware.get_labware_offset( diff --git a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py index 739d56ded00..6ec09136387 100644 --- a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py +++ b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py @@ -1,7 +1,11 @@ """Deck configuration resource provider.""" + from typing import List, Set, Tuple -from opentrons_shared_data.deck.types import DeckDefinitionV5, CutoutFixture +from opentrons_shared_data.deck.types import ( + DeckDefinitionV5, + CutoutFixture, +) from opentrons.types import DeckSlotName @@ -17,6 +21,7 @@ CutoutDoesNotExistError, FixtureDoesNotExistError, AddressableAreaDoesNotExistError, + SlotDoesNotExistError, ) @@ -98,12 +103,15 @@ def get_potential_cutout_fixtures( def get_addressable_area_from_name( addressable_area_name: str, cutout_position: DeckPoint, - base_slot: DeckSlotName, deck_definition: DeckDefinitionV5, ) -> AddressableArea: """Given a name and a cutout position, get an addressable area on the deck.""" for addressable_area in deck_definition["locations"]["addressableAreas"]: if addressable_area["id"] == addressable_area_name: + cutout_id, _ = get_potential_cutout_fixtures( + addressable_area_name, deck_definition + ) + base_slot = get_deck_slot_for_cutout_id(cutout_id) area_offset = addressable_area["offsetFromCutoutFixture"] position = AddressableOffsetVector( x=area_offset[0] + cutout_position.x, @@ -130,3 +138,87 @@ def get_addressable_area_from_name( raise AddressableAreaDoesNotExistError( f"Could not find addressable area with name {addressable_area_name}" ) + + +def get_deck_slot_for_cutout_id(cutout_id: str) -> DeckSlotName: + """Get the corresponding deck slot for an addressable area.""" + try: + return CUTOUT_TO_DECK_SLOT_MAP[cutout_id] + except KeyError: + raise CutoutDoesNotExistError(f"Could not find data for cutout {cutout_id}") + + +def get_cutout_id_by_deck_slot_name(slot_name: DeckSlotName) -> str: + """Get the Cutout ID of a given Deck Slot by Deck Slot Name.""" + try: + return DECK_SLOT_TO_CUTOUT_MAP[slot_name] + except KeyError: + raise SlotDoesNotExistError(f"Could not find data for slot {slot_name.value}") + + +def get_labware_hosting_addressable_area_name_for_cutout_and_cutout_fixture( + cutout_id: str, cutout_fixture_id: str, deck_definition: DeckDefinitionV5 +) -> str: + """Get the first addressable area that can contain labware for a cutout and fixture. + + This probably isn't relevant outside of labware offset locations, where (for now) nothing + provides more than one labware-containing addressable area. + """ + for cutoutFixture in deck_definition["cutoutFixtures"]: + if cutoutFixture["id"] != cutout_fixture_id: + continue + provided_aas = cutoutFixture["providesAddressableAreas"].get(cutout_id, None) + if provided_aas is None: + raise CutoutDoesNotExistError( + f"{cutout_fixture_id} does not go in {cutout_id}" + ) + for aa_id in provided_aas: + for addressable_area in deck_definition["locations"]["addressableAreas"]: + if addressable_area["id"] != aa_id: + continue + # TODO: In deck def v6 this will be easier, but as of right now there isn't really + # a way to tell from an addressable area whether it takes labware so let's take the + # first one + return aa_id + raise AddressableAreaDoesNotExistError( + f"Could not find an addressable area that allows labware from cutout fixture {cutout_fixture_id} in cutout {cutout_id}" + ) + + raise FixtureDoesNotExistError(f"Could not find entry for {cutout_fixture_id}") + + +# This is a temporary shim while Protocol Engine's conflict-checking code +# can only take deck slots as input. +# Long-term solution: Check for conflicts based on bounding boxes, not slot adjacencies. +# Shorter-term: Change the conflict-checking code to take cutouts instead of deck slots. +CUTOUT_TO_DECK_SLOT_MAP: dict[str, DeckSlotName] = { + # OT-2 + "cutout1": DeckSlotName.SLOT_1, + "cutout2": DeckSlotName.SLOT_2, + "cutout3": DeckSlotName.SLOT_3, + "cutout4": DeckSlotName.SLOT_4, + "cutout5": DeckSlotName.SLOT_5, + "cutout6": DeckSlotName.SLOT_6, + "cutout7": DeckSlotName.SLOT_7, + "cutout8": DeckSlotName.SLOT_8, + "cutout9": DeckSlotName.SLOT_9, + "cutout10": DeckSlotName.SLOT_10, + "cutout11": DeckSlotName.SLOT_11, + "cutout12": DeckSlotName.FIXED_TRASH, + # Flex + "cutoutA1": DeckSlotName.SLOT_A1, + "cutoutA2": DeckSlotName.SLOT_A2, + "cutoutA3": DeckSlotName.SLOT_A3, + "cutoutB1": DeckSlotName.SLOT_B1, + "cutoutB2": DeckSlotName.SLOT_B2, + "cutoutB3": DeckSlotName.SLOT_B3, + "cutoutC1": DeckSlotName.SLOT_C1, + "cutoutC2": DeckSlotName.SLOT_C2, + "cutoutC3": DeckSlotName.SLOT_C3, + "cutoutD1": DeckSlotName.SLOT_D1, + "cutoutD2": DeckSlotName.SLOT_D2, + "cutoutD3": DeckSlotName.SLOT_D3, +} +DECK_SLOT_TO_CUTOUT_MAP = { + deck_slot: cutout for cutout, deck_slot in CUTOUT_TO_DECK_SLOT_MAP.items() +} diff --git a/api/src/opentrons/protocol_engine/slot_standardization.py b/api/src/opentrons/protocol_engine/slot_standardization.py index 5943febc820..935bb54da3f 100644 --- a/api/src/opentrons/protocol_engine/slot_standardization.py +++ b/api/src/opentrons/protocol_engine/slot_standardization.py @@ -14,7 +14,6 @@ deck slot. """ - from typing import Any, Callable, Dict, Type from opentrons_shared_data.robot.types import RobotType @@ -26,29 +25,11 @@ DeckSlotLocation, LabwareLocation, AddressableAreaLocation, - LabwareOffsetCreate, ModuleLocation, OnLabwareLocation, ) -def standardize_labware_offset( - original: LabwareOffsetCreate, robot_type: RobotType -) -> LabwareOffsetCreate: - """Convert the deck slot in the given `LabwareOffsetCreate` to match the given robot type.""" - return original.model_copy( - update={ - "location": original.location.model_copy( - update={ - "slotName": original.location.slotName.to_equivalent_for_robot_type( - robot_type - ) - } - ) - } - ) - - def standardize_command( original: commands.CommandCreate, robot_type: RobotType ) -> commands.CommandCreate: diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index 16898ccb4ed..c227fa72285 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -1,4 +1,5 @@ """Basic addressable area data state and store.""" + from dataclasses import dataclass from functools import cached_property from typing import Dict, List, Optional, Set @@ -112,43 +113,6 @@ def _get_conflicting_addressable_areas_error_string( return ", ".join(display_names) -# This is a temporary shim while Protocol Engine's conflict-checking code -# can only take deck slots as input. -# Long-term solution: Check for conflicts based on bounding boxes, not slot adjacencies. -# Shorter-term: Change the conflict-checking code to take cutouts instead of deck slots. -CUTOUT_TO_DECK_SLOT_MAP: Dict[str, DeckSlotName] = { - # OT-2 - "cutout1": DeckSlotName.SLOT_1, - "cutout2": DeckSlotName.SLOT_2, - "cutout3": DeckSlotName.SLOT_3, - "cutout4": DeckSlotName.SLOT_4, - "cutout5": DeckSlotName.SLOT_5, - "cutout6": DeckSlotName.SLOT_6, - "cutout7": DeckSlotName.SLOT_7, - "cutout8": DeckSlotName.SLOT_8, - "cutout9": DeckSlotName.SLOT_9, - "cutout10": DeckSlotName.SLOT_10, - "cutout11": DeckSlotName.SLOT_11, - "cutout12": DeckSlotName.FIXED_TRASH, - # Flex - "cutoutA1": DeckSlotName.SLOT_A1, - "cutoutA2": DeckSlotName.SLOT_A2, - "cutoutA3": DeckSlotName.SLOT_A3, - "cutoutB1": DeckSlotName.SLOT_B1, - "cutoutB2": DeckSlotName.SLOT_B2, - "cutoutB3": DeckSlotName.SLOT_B3, - "cutoutC1": DeckSlotName.SLOT_C1, - "cutoutC2": DeckSlotName.SLOT_C2, - "cutoutC3": DeckSlotName.SLOT_C3, - "cutoutD1": DeckSlotName.SLOT_D1, - "cutoutD2": DeckSlotName.SLOT_D2, - "cutoutD3": DeckSlotName.SLOT_D3, -} -DECK_SLOT_TO_CUTOUT_MAP = { - deck_slot: cutout for cutout, deck_slot in CUTOUT_TO_DECK_SLOT_MAP.items() -} - - class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions): """Addressable area state container.""" @@ -221,13 +185,11 @@ def _get_addressable_areas_from_deck_configuration( cutout_position = deck_configuration_provider.get_cutout_position( cutout_id, deck_definition ) - base_slot = CUTOUT_TO_DECK_SLOT_MAP[cutout_id] for addressable_area_name in provided_addressable_areas: addressable_areas.append( deck_configuration_provider.get_addressable_area_from_name( addressable_area_name=addressable_area_name, cutout_position=cutout_position, - base_slot=base_slot, deck_definition=deck_definition, ) ) @@ -242,12 +204,10 @@ def _add_addressable_area(self, addressable_area_name: str) -> None: cutout_position = deck_configuration_provider.get_cutout_position( cutout_id, self._state.deck_definition ) - base_slot = CUTOUT_TO_DECK_SLOT_MAP[cutout_id] addressable_area = ( deck_configuration_provider.get_addressable_area_from_name( addressable_area_name=addressable_area_name, cutout_position=cutout_position, - base_slot=base_slot, deck_definition=self._state.deck_definition, ) ) @@ -300,6 +260,11 @@ def __init__(self, state: AddressableAreaState) -> None: """ self._state = state + @cached_property + def deck_definition(self) -> DeckDefinitionV5: + """The full deck definition.""" + return self._state.deck_definition + @cached_property def deck_extents(self) -> Point: """The maximum space on the deck.""" @@ -426,11 +391,9 @@ def _get_addressable_area_from_deck_data( cutout_position = deck_configuration_provider.get_cutout_position( cutout_id, self._state.deck_definition ) - base_slot = CUTOUT_TO_DECK_SLOT_MAP[cutout_id] return deck_configuration_provider.get_addressable_area_from_name( addressable_area_name=addressable_area_name, cutout_position=cutout_position, - base_slot=base_slot, deck_definition=self._state.deck_definition, ) @@ -526,7 +489,7 @@ def get_addressable_area_center(self, addressable_area_name: str) -> Point: def get_cutout_id_by_deck_slot_name(self, slot_name: DeckSlotName) -> str: """Get the Cutout ID of a given Deck Slot by Deck Slot Name.""" - return DECK_SLOT_TO_CUTOUT_MAP[slot_name] + return deck_configuration_provider.get_cutout_id_by_deck_slot_name(slot_name) def get_fixture_by_deck_slot_name( self, slot_name: DeckSlotName @@ -534,7 +497,9 @@ def get_fixture_by_deck_slot_name( """Get the Cutout Fixture currently loaded where a specific Deck Slot would be.""" deck_config = self._state.deck_configuration if deck_config: - slot_cutout_id = DECK_SLOT_TO_CUTOUT_MAP[slot_name] + slot_cutout_id = ( + deck_configuration_provider.get_cutout_id_by_deck_slot_name(slot_name) + ) slot_cutout_fixture = None # This will only ever be one under current assumptions for ( @@ -571,7 +536,9 @@ def get_fixture_serial_from_deck_configuration_by_deck_slot( """Get the serial number provided by the deck configuration for a Fixture at a given location.""" deck_config = self._state.deck_configuration if deck_config: - slot_cutout_id = DECK_SLOT_TO_CUTOUT_MAP[slot_name] + slot_cutout_id = ( + deck_configuration_provider.get_cutout_id_by_deck_slot_name(slot_name) + ) # This will only ever be one under current assumptions for ( cutout_id, diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 0344ef321fc..2a4b79571cc 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -51,7 +51,10 @@ AddressableAreaLocation, AddressableOffsetVector, StagingSlotLocation, - LabwareOffsetLocation, + LabwareOffsetLocationSequence, + OnModuleOffsetLocationSequenceComponent, + OnAddressableAreaOffsetLocationSequenceComponent, + OnLabwareOffsetLocationSequenceComponent, ModuleModel, ) from .config import Config @@ -1360,50 +1363,91 @@ def _labware_gripper_offsets( labware_id=labware_id, slot_name=None ) - def get_offset_location(self, labware_id: str) -> Optional[LabwareOffsetLocation]: - """Provide the LabwareOffsetLocation specifying the current position of the labware. + def get_offset_location( + self, labware_id: str + ) -> Optional[LabwareOffsetLocationSequence]: + """Provide the LegacyLabwareOffsetLocation specifying the current position of the labware. - If the labware is in a location that cannot be specified by a LabwareOffsetLocation + If the labware is in a location that cannot be specified by a LabwareOffsetLocationSequence (for instance, OFF_DECK) then return None. """ parent_location = self._labware.get_location(labware_id) - - if isinstance(parent_location, DeckSlotLocation): - return LabwareOffsetLocation( - slotName=parent_location.slotName, moduleModel=None, definitionUri=None - ) - elif isinstance(parent_location, ModuleLocation): - module_model = self._modules.get_requested_model(parent_location.moduleId) - module_location = self._modules.get_location(parent_location.moduleId) - return LabwareOffsetLocation( - slotName=module_location.slotName, - moduleModel=module_model, - definitionUri=None, - ) - elif isinstance(parent_location, OnLabwareLocation): - non_labware_parent_location = self._labware.get_parent_location(labware_id) - - parent_uri = self._labware.get_definition_uri(parent_location.labwareId) - if isinstance(non_labware_parent_location, DeckSlotLocation): - return LabwareOffsetLocation( - slotName=non_labware_parent_location.slotName, - moduleModel=None, - definitionUri=parent_uri, - ) - elif isinstance(non_labware_parent_location, ModuleLocation): - module_model = self._modules.get_requested_model( - non_labware_parent_location.moduleId + return self.get_projected_offset_location(parent_location) + + def get_projected_offset_location( + self, labware_location: LabwareLocation + ) -> Optional[LabwareOffsetLocationSequence]: + """Get the offset location that a labware loaded into this location would match.""" + return self._recurse_labware_offset_location(labware_location, []) + + def _recurse_labware_offset_location( + self, labware_location: LabwareLocation, building: LabwareOffsetLocationSequence + ) -> LabwareOffsetLocationSequence | None: + if isinstance(labware_location, DeckSlotLocation): + return building + [ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName=labware_location.slotName.value ) - module_location = self._modules.get_location( - non_labware_parent_location.moduleId - ) - return LabwareOffsetLocation( - slotName=module_location.slotName, - moduleModel=module_model, - definitionUri=parent_uri, + ] + + elif isinstance(labware_location, ModuleLocation): + module_id = labware_location.moduleId + # Allow ModuleNotLoadedError to propagate. + # Note also that we match based on the module's requested model, not its + # actual model, to implement robot-server's documented HTTP API semantics. + module_model = self._modules.get_requested_model(module_id=module_id) + + # If `module_model is None`, it probably means that this module was added by + # `ProtocolEngine.use_attached_modules()`, instead of an explicit + # `loadModule` command. + # + # This assert should never raise in practice because: + # 1. `ProtocolEngine.use_attached_modules()` is only used by + # robot-server's "stateless command" endpoints, under `/commands`. + # 2. Those endpoints don't support loading labware, so this code will + # never run. + # + # Nevertheless, if it does happen somehow, we do NOT want to pass the + # `None` value along to `LabwareView.find_applicable_labware_offset()`. + # `None` means something different there, which will cause us to return + # wrong results. + assert module_model is not None, ( + "Can't find offsets for labware" + " that are loaded on modules" + " that were loaded with ProtocolEngine.use_attached_modules()." + ) + + module_location = self._modules.get_location(module_id=module_id) + if self._modules.get_deck_supports_module_fixtures(): + module_aa = self._modules.ensure_and_convert_module_fixture_location( + module_location.slotName, module_model ) + else: + module_aa = module_location.slotName.value + return building + [ + OnModuleOffsetLocationSequenceComponent(moduleModel=module_model), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName=module_aa + ), + ] + + elif isinstance(labware_location, OnLabwareLocation): + parent_labware_id = labware_location.labwareId + parent_labware_uri = self._labware.get_definition_uri(parent_labware_id) + + base_location = self._labware.get_parent_location(parent_labware_id) + return self._recurse_labware_offset_location( + base_location, + building + + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri=parent_labware_uri + ) + ], + ) - return None + else: # Off deck + return None def get_well_offset_adjustment( self, diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index c4a2afcd62f..6063a46e6b8 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -1,4 +1,5 @@ """Basic labware data state and store.""" + from __future__ import annotations from dataclasses import dataclass @@ -41,7 +42,8 @@ Dimensions, LabwareOffset, LabwareOffsetVector, - LabwareOffsetLocation, + LabwareOffsetLocationSequence, + LegacyLabwareOffsetLocation, LabwareLocation, LoadedLabware, ModuleLocation, @@ -167,7 +169,8 @@ def handle_action(self, action: Action) -> None: id=action.labware_offset_id, createdAt=action.created_at, definitionUri=action.request.definitionUri, - location=action.request.location, + location=action.request.legacyLocation, + locationSequence=action.request.locationSequence, vector=action.request.vector, ) self._add_labware_offset(labware_offset) @@ -825,15 +828,32 @@ def get_labware_offsets(self) -> List[LabwareOffset]: """Get all labware offsets, in the order they were added.""" return list(self._state.labware_offsets_by_id.values()) - # TODO: Make this slightly more ergonomic for the caller by - # only returning the optional str ID, at the cost of baking redundant lookups - # into the API? def find_applicable_labware_offset( + self, definition_uri: str, location: LabwareOffsetLocationSequence + ) -> Optional[LabwareOffset]: + """Find a labware offset that applies to the given definition and location sequence. + + Returns the *most recently* added matching offset, so later ones can override earlier ones. + Returns ``None`` if no loaded offset matches the location. + + An offset matches a labware instance if the sequence of locations formed by following the + .location elements of the labware instance until you reach an addressable area has the same + definition URIs as the sequence of definition URIs stored by the offset. + """ + for candidate in reversed(list(self._state.labware_offsets_by_id.values())): + if ( + candidate.definitionUri == definition_uri + and candidate.locationSequence == location + ): + return candidate + return None + + def find_applicable_labware_offset_by_legacy_location( self, definition_uri: str, - location: LabwareOffsetLocation, + location: LegacyLabwareOffsetLocation, ) -> Optional[LabwareOffset]: - """Find a labware offset that applies to the given definition and location. + """Find a labware offset that applies to the given definition and legacy location. Returns the *most recently* added matching offset, so later offsets can override earlier ones. diff --git a/api/src/opentrons/protocol_engine/state/modules.py b/api/src/opentrons/protocol_engine/state/modules.py index 76d7a084b42..2da186555fd 100644 --- a/api/src/opentrons/protocol_engine/state/modules.py +++ b/api/src/opentrons/protocol_engine/state/modules.py @@ -1293,6 +1293,11 @@ def convert_absorbance_reader_data_points( "Only readings of 96 Well labware are supported for conversion to map of values by well." ) + def get_deck_supports_module_fixtures(self) -> bool: + """Check if the loaded deck supports modules as fixtures.""" + deck_type = self._state.deck_type + return deck_type not in [DeckType.OT2_STANDARD, DeckType.OT2_SHORT_TRASH] + def ensure_and_convert_module_fixture_location( self, deck_slot: DeckSlotName, @@ -1304,7 +1309,7 @@ def ensure_and_convert_module_fixture_location( """ deck_type = self._state.deck_type - if deck_type == DeckType.OT2_STANDARD or deck_type == DeckType.OT2_SHORT_TRASH: + if not self.get_deck_supports_module_fixtures(): raise ValueError( f"Invalid Deck Type: {deck_type.name} - Does not support modules as fixtures." ) diff --git a/api/src/opentrons/protocol_engine/types/__init__.py b/api/src/opentrons/protocol_engine/types/__init__.py index 9bf512a7a29..fbaef870f3e 100644 --- a/api/src/opentrons/protocol_engine/types/__init__.py +++ b/api/src/opentrons/protocol_engine/types/__init__.py @@ -83,10 +83,18 @@ OverlapOffset, LabwareOffset, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, + LabwareOffsetCreateInternal, LoadedLabware, ) from .liquid import HexColor, EmptyLiquidId, LiquidId, Liquid, FluidKind, AspiratedFluid -from .labware_offset_location import LabwareOffsetLocation +from .labware_offset_location import ( + LegacyLabwareOffsetLocation, + LabwareOffsetLocationSequence, + OnLabwareOffsetLocationSequenceComponent, + OnModuleOffsetLocationSequenceComponent, + OnAddressableAreaOffsetLocationSequenceComponent, +) from .labware_offset_vector import LabwareOffsetVector from .well_position import ( WellOrigin, @@ -194,13 +202,19 @@ "NonStackedLocation", "DeckPoint", # Labware offset location - "LabwareOffsetLocation", + "LegacyLabwareOffsetLocation", + "LabwareOffsetLocationSequence", + "OnLabwareOffsetLocationSequenceComponent", + "OnModuleOffsetLocationSequenceComponent", + "OnAddressableAreaOffsetLocationSequenceComponent", # Labware offset vector "LabwareOffsetVector", # Labware "OverlapOffset", "LabwareOffset", "LabwareOffsetCreate", + "LegacyLabwareOffsetCreate", + "LabwareOffsetCreateInternal", "LoadedLabware", "LabwareOffsetVector", # Liquids diff --git a/api/src/opentrons/protocol_engine/types/labware.py b/api/src/opentrons/protocol_engine/types/labware.py index b0dd5d52d31..bb8a4656d58 100644 --- a/api/src/opentrons/protocol_engine/types/labware.py +++ b/api/src/opentrons/protocol_engine/types/labware.py @@ -3,12 +3,16 @@ from __future__ import annotations from typing import Optional +from dataclasses import dataclass from datetime import datetime from pydantic import BaseModel, Field from .location import LabwareLocation -from .labware_offset_location import LabwareOffsetLocation +from .labware_offset_location import ( + LegacyLabwareOffsetLocation, + LabwareOffsetLocationSequence, +) from .labware_offset_vector import LabwareOffsetVector from .util import Vec3f @@ -28,9 +32,13 @@ class LabwareOffset(BaseModel): id: str = Field(..., description="Unique labware offset record identifier.") createdAt: datetime = Field(..., description="When this labware offset was added.") definitionUri: str = Field(..., description="The URI for the labware's definition.") - location: LabwareOffsetLocation = Field( + location: LegacyLabwareOffsetLocation = Field( ..., - description="Where the labware is located on the robot.", + description="Where the labware is located on the robot. Deprecated and present only for backwards compatibility; cannot represent certain locations. Use locationSequence instead.", + ) + locationSequence: Optional[LabwareOffsetLocationSequence] = Field( + default=None, + description="Where the labware is located on the robot. Can represent all locations, but may not be present for older runs.", ) vector: LabwareOffsetVector = Field( ..., @@ -38,11 +46,11 @@ class LabwareOffset(BaseModel): ) -class LabwareOffsetCreate(BaseModel): - """Create request data for a labware offset.""" +class LegacyLabwareOffsetCreate(BaseModel): + """Create request data for a labware offset with a legacy location field.""" definitionUri: str = Field(..., description="The URI for the labware's definition.") - location: LabwareOffsetLocation = Field( + location: LegacyLabwareOffsetLocation = Field( ..., description="Where the labware is located on the robot.", ) @@ -52,6 +60,28 @@ class LabwareOffsetCreate(BaseModel): ) +class LabwareOffsetCreate(BaseModel): + """Create request data for a labware offset with a modern location sequence.""" + + definitionUri: str = Field(..., description="The URI for the labware's definition.") + locationSequence: LabwareOffsetLocationSequence = Field( + ..., description="Where the labware is located on the robot." + ) + vector: LabwareOffsetVector = Field( + ..., description="The offset applied to matching labware." + ) + + +@dataclass(frozen=True) +class LabwareOffsetCreateInternal: + """An internal-only labware offset creator that captures both old and new location arguments.""" + + definitionUri: str + locationSequence: LabwareOffsetLocationSequence + legacyLocation: LegacyLabwareOffsetLocation + vector: LabwareOffsetVector + + class LoadedLabware(BaseModel): """A labware that has been loaded.""" diff --git a/api/src/opentrons/protocol_engine/types/labware_offset_location.py b/api/src/opentrons/protocol_engine/types/labware_offset_location.py index cf7496be2e0..2b992a4da01 100644 --- a/api/src/opentrons/protocol_engine/types/labware_offset_location.py +++ b/api/src/opentrons/protocol_engine/types/labware_offset_location.py @@ -3,7 +3,7 @@ This is its own module to fix circular imports. """ -from typing import Optional +from typing import Optional, Literal from pydantic import BaseModel, Field @@ -12,7 +12,52 @@ from .module import ModuleModel -class LabwareOffsetLocation(BaseModel): +class OnLabwareOffsetLocationSequenceComponent(BaseModel): + """Offset location sequence component for a labware on another labware.""" + + kind: Literal["onLabware"] = "onLabware" + labwareUri: str = Field( + ..., + description="The definition URI of a labware that a labware can be loaded onto.", + ) + + +class OnModuleOffsetLocationSequenceComponent(BaseModel): + """Offset location sequence component for a labware on a module.""" + + kind: Literal["onModule"] = "onModule" + moduleModel: ModuleModel = Field( + ..., description="The model of a module that a lwbare can be loaded on to." + ) + + +class OnAddressableAreaOffsetLocationSequenceComponent(BaseModel): + """Offset location sequence component for a labware on an addressable area.""" + + kind: Literal["onAddressableArea"] = "onAddressableArea" + addressableAreaName: str = Field( + ..., + description=( + 'The ID of an addressable area that a labware or module can be loaded onto, such as (on the OT-2) "2" ' + 'or (on the Flex) "C1". ' + "\n\n" + "On the Flex, this field must be correct for the kind of entity it hosts. For instance, if the prior entity " + "in the location sequence is an `OnModuleOffsetLocationSequenceComponent(moduleModel=temperatureModuleV2)`, " + "this entity must be temperatureModuleV2NN where NN is the slot name in which the module resides. " + ), + ) + + +LabwareOffsetLocationSequenceComponents = ( + OnLabwareOffsetLocationSequenceComponent + | OnModuleOffsetLocationSequenceComponent + | OnAddressableAreaOffsetLocationSequenceComponent +) + +LabwareOffsetLocationSequence = list[LabwareOffsetLocationSequenceComponents] + + +class LegacyLabwareOffsetLocation(BaseModel): """Parameters describing when a given offset may apply to a given labware load.""" slotName: DeckSlotName = Field( diff --git a/api/src/opentrons/protocol_engine/types/location.py b/api/src/opentrons/protocol_engine/types/location.py index 5397b17cfeb..cc174f4bdea 100644 --- a/api/src/opentrons/protocol_engine/types/location.py +++ b/api/src/opentrons/protocol_engine/types/location.py @@ -13,7 +13,7 @@ class DeckSlotLocation(BaseModel): slotName: DeckSlotName = Field( ..., description=( - # This description should be kept in sync with LabwareOffsetLocation.slotName. + # This description should be kept in sync with LegacyLabwareOffsetLocation.slotName. "A slot on the robot's deck." "\n\n" 'The plain numbers like `"5"` are for the OT-2,' @@ -33,7 +33,7 @@ class StagingSlotLocation(BaseModel): slotName: StagingSlotName = Field( ..., description=( - # This description should be kept in sync with LabwareOffsetLocation.slotName. + # This description should be kept in sync with LegacyLabwareOffsetLocation.slotName. "A slot on the robot's staging area." "\n\n" "These apply only to the Flex. The OT-2 has no staging slots." @@ -77,6 +77,45 @@ class OnLabwareLocation(BaseModel): OFF_DECK_LOCATION: _OffDeckLocationType = "offDeck" SYSTEM_LOCATION: _SystemLocationType = "systemLocation" + +class OnLabwareLocationSequenceComponent(BaseModel): + """Labware on another labware.""" + + kind: Literal["onLabware"] = "onLabware" + labwareId: str + lidId: str | None + + +class OnModuleLocationSequenceComponent(BaseModel): + """Labware on a module.""" + + kind: Literal["onModule"] = "onModule" + moduleId: str + + +class OnAddressableAreaLocationSequenceComponent(BaseModel): + """Labware on an addressable area.""" + + kind: Literal["onAddressableArea"] = "onAddressableArea" + addressableAreaName: str + slotName: str | None + + +class NotOnDeckLocationSequenceComponent(BaseModel): + """Labware on a system location.""" + + kind: Literal["notOnDeck"] = "notOnDeck" + logicalLocationName: _OffDeckLocationType | _SystemLocationType + + +LabwareLocationSequence = list[ + OnLabwareLocationSequenceComponent + | OnModuleLocationSequenceComponent + | OnAddressableAreaLocationSequenceComponent + | NotOnDeckLocationSequenceComponent +] +"""Labware location specifier.""" + LabwareLocation = Union[ DeckSlotLocation, ModuleLocation, diff --git a/api/src/opentrons/protocol_runner/run_orchestrator.py b/api/src/opentrons/protocol_runner/run_orchestrator.py index 28266a9c485..b45d8b9db94 100644 --- a/api/src/opentrons/protocol_runner/run_orchestrator.py +++ b/api/src/opentrons/protocol_runner/run_orchestrator.py @@ -32,6 +32,7 @@ PostRunHardwareState, EngineStatus, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, LabwareOffset, DeckConfigurationType, RunTimeParameter, @@ -346,7 +347,9 @@ def run_has_stopped(self) -> bool: """Get whether the run has stopped.""" return self._protocol_engine.state_view.commands.get_is_stopped() - def add_labware_offset(self, request: LabwareOffsetCreate) -> LabwareOffset: + def add_labware_offset( + self, request: LabwareOffsetCreate | LegacyLabwareOffsetCreate + ) -> LabwareOffset: """Add a new labware offset to state.""" return self._protocol_engine.add_labware_offset(request) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py index beca8fe99d1..06c2445d79e 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py @@ -23,8 +23,9 @@ from opentrons.protocol_engine.errors import LabwareNotOnDeckError from opentrons.protocol_engine.types import ( LabwareOffsetCreate, - LabwareOffsetLocation, + LabwareOffsetLocationSequence, LabwareOffsetVector, + OnAddressableAreaOffsetLocationSequenceComponent, ) from opentrons.protocol_api._liquid import Liquid from opentrons.protocol_api.core.labware import LabwareLoadParams @@ -106,16 +107,18 @@ def test_set_calibration_succeeds_in_ok_location( decoy.when( mock_engine_client.state.labware.get_display_name("cool-labware") ).then_return("what a cool labware") - location = LabwareOffsetLocation(slotName=DeckSlotName.SLOT_C2) + location = [ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="C2") + ] decoy.when( mock_engine_client.state.geometry.get_offset_location("cool-labware") - ).then_return(location) + ).then_return(cast(LabwareOffsetLocationSequence, location)) subject.set_calibration(Point(1, 2, 3)) decoy.verify( mock_engine_client.add_labware_offset( LabwareOffsetCreate( definitionUri="hello/world/42", - location=location, + locationSequence=cast(LabwareOffsetLocationSequence, location), vector=LabwareOffsetVector(x=1, y=2, z=3), ) ), diff --git a/api/tests/opentrons/protocol_api/core/legacy/test_labware_offset_provider.py b/api/tests/opentrons/protocol_api/core/legacy/test_labware_offset_provider.py index 1cf0bb360fb..a31abe5b06b 100644 --- a/api/tests/opentrons/protocol_api/core/legacy/test_labware_offset_provider.py +++ b/api/tests/opentrons/protocol_api/core/legacy/test_labware_offset_provider.py @@ -12,7 +12,7 @@ ProtocolEngine, LabwareOffset, LabwareOffsetVector, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, ModuleModel, ) from opentrons.protocol_engine.state.labware import LabwareView @@ -47,9 +47,9 @@ def test_find_something( ) -> None: """It should pass along simplified labware offset info from Protocol Engine.""" decoy.when( - labware_view.find_applicable_labware_offset( + labware_view.find_applicable_labware_offset_by_legacy_location( definition_uri="some_namespace/some_load_name/123", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_1, moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, ), @@ -61,7 +61,7 @@ def test_find_something( vector=LabwareOffsetVector(x=1, y=2, z=3), # Shouldn't matter; subject should throw these away: definitionUri="result_definition_uri_should_not_matter", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_11), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_11), createdAt=datetime(year=2021, month=1, day=1), ) ) @@ -82,12 +82,14 @@ def test_find_nothing( subject: LabwareOffsetProvider, labware_view: LabwareView, decoy: Decoy ) -> None: """It should return a zero offset when Protocol Engine has no offset to provide.""" - decoy_call_rehearsal = labware_view.find_applicable_labware_offset( - definition_uri="some_namespace/some_load_name/123", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_1, - moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, - ), + decoy_call_rehearsal = ( + labware_view.find_applicable_labware_offset_by_legacy_location( + definition_uri="some_namespace/some_load_name/123", + location=LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, + ), + ) ) decoy.when(decoy_call_rehearsal).then_return(None) diff --git a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py index 5d3edc307bd..29c1eaa6d35 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py @@ -33,7 +33,10 @@ LoadedPipette, LabwareOffset, LabwareOffsetVector, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, + OnAddressableAreaOffsetLocationSequenceComponent, + OnModuleOffsetLocationSequenceComponent, + OnLabwareOffsetLocationSequenceComponent, ModuleModel, ModuleDefinition, OFF_DECK_LOCATION, @@ -231,18 +234,34 @@ async def test_load_labware( version=1, ) ).then_return(minimal_labware_def) + decoy.when( + state_store.geometry.get_projected_offset_location( + DeckSlotLocation(slotName=DeckSlotName.SLOT_3) + ) + ).then_return( + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="3")] + ) decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name/1", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_3), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ) + ], ) ).then_return( LabwareOffset( id="labware-offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="opentrons-test/load-name/1", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_3), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_3), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ) + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) @@ -312,11 +331,21 @@ async def test_load_labware_uses_provided_id( version=1, ) ).then_return(minimal_labware_def) - + decoy.when( + state_store.geometry.get_projected_offset_location( + DeckSlotLocation(slotName=DeckSlotName.SLOT_3) + ) + ).then_return( + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="3")] + ) decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name/1", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_3), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ) + ], ) ).then_return(None) @@ -354,10 +383,22 @@ async def test_load_labware_uses_loaded_labware_def( minimal_labware_def ) + decoy.when( + state_store.geometry.get_projected_offset_location( + DeckSlotLocation(slotName=DeckSlotName.SLOT_3) + ) + ).then_return( + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="3")] + ) + decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name/1", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_3), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ) + ], ) ).then_return(None) @@ -406,23 +447,48 @@ async def test_load_labware_on_module( DeckSlotLocation(slotName=DeckSlotName.SLOT_3) ) + decoy.when( + state_store.geometry.get_projected_offset_location( + ModuleLocation(moduleId="module-id") + ) + ).then_return( + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1"), + ] + ) + decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_3, - moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1, - ), + location=[ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], ) ).then_return( LabwareOffset( id="labware-offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_3, moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1, ), + locationSequence=[ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) @@ -448,23 +514,38 @@ def test_find_offset_id_of_labware_on_deck_slot( subject: EquipmentHandler, ) -> None: """It should find the offset by resolving the provided location.""" + decoy.when( + state_store.geometry.get_projected_offset_location( + DeckSlotLocation(slotName=DeckSlotName.SLOT_3) + ) + ).then_return( + [ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="3"), + ] + ) decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_3, - moduleModel=None, - ), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ) + ], ) ).then_return( LabwareOffset( id="labware-offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_3, moduleModel=None, ), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ) + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) @@ -490,23 +571,48 @@ def test_find_offset_id_of_labware_on_module( DeckSlotLocation(slotName=DeckSlotName.SLOT_3) ) + decoy.when( + state_store.geometry.get_projected_offset_location( + ModuleLocation(moduleId="input-module-id") + ) + ).then_return( + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="3"), + ] + ) + decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_3, - moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1, - ), + location=[ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ), + ], ) ).then_return( LabwareOffset( id="labware-offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_3, moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1, ), + locationSequence=[ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ), + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) @@ -541,25 +647,49 @@ def test_find_offset_id_of_labware_on_labware( decoy.when(state_store.labware.get_parent_location("labware-id")).then_return( parent_location ) - + decoy.when( + state_store.geometry.get_projected_offset_location( + OnLabwareLocation(labwareId="labware-id") + ) + ).then_return( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1"), + ] + if parent_location is not OFF_DECK_LOCATION + else None + ) decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name-1/1", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_1, - moduleModel=None, - definitionUri="opentrons-test/load-name-2/1", - ), + location=[ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], ) ).then_return( LabwareOffset( id="labware-offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_1, definitionUri="opentrons-test/load-name-2/1", ), + locationSequence=[ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) @@ -594,25 +724,161 @@ def test_find_offset_id_of_labware_on_labware_on_modules( DeckSlotLocation(slotName=DeckSlotName.SLOT_1) ) + decoy.when( + state_store.geometry.get_projected_offset_location( + OnLabwareLocation(labwareId="labware-id") + ) + ).then_return( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1"), + ] + ) + decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name-1/1", - location=LabwareOffsetLocation( + location=[ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], + ) + ).then_return( + LabwareOffset( + id="labware-offset-id", + createdAt=datetime(year=2021, month=1, day=2), + definitionUri="opentrons-test/load-name/1", + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_1, moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1, definitionUri="opentrons-test/load-name-2/1", ), + locationSequence=[ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + ) + + result = subject.find_applicable_labware_offset_id( + labware_definition_uri="opentrons-test/load-name-1/1", + labware_location=OnLabwareLocation(labwareId="labware-id"), + ) + + assert result == "labware-offset-id" + + +def test_find_offset_id_of_labware_on_labware_on_labware_modules( + decoy: Decoy, + state_store: StateStore, + subject: EquipmentHandler, +) -> None: + """It should find an offset for a labware on a labware on a module.""" + decoy.when(state_store.labware.get_definition_uri("labware-id")).then_return( + LabwareUri("opentrons-test/load-name-2/1") + ) + + decoy.when(state_store.labware.get_parent_location("labware-id")).then_return( + ModuleLocation(moduleId="labware-id-2"), + ) + + decoy.when(state_store.labware.get_definition_uri("labware-id-2")).then_return( + LabwareUri("opentrons-test/load-name-3/1") + ) + + decoy.when(state_store.labware.get_parent_location("labware-id-2")).then_return( + ModuleLocation(moduleId="module-id"), + ) + + decoy.when(state_store.modules.get_requested_model("module-id")).then_return( + ModuleModel.HEATER_SHAKER_MODULE_V1 + ) + + decoy.when(state_store.modules.get_location("module-id")).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_1) + ) + + decoy.when( + state_store.geometry.get_projected_offset_location( + OnLabwareLocation(labwareId="labware-id") + ) + ).then_return( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-3/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1"), + ] + ) + + decoy.when( + state_store.labware.find_applicable_labware_offset( + definition_uri="opentrons-test/load-name-1/1", + location=[ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-3/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], ) ).then_return( LabwareOffset( id="labware-offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_1, moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1, definitionUri="opentrons-test/load-name-2/1", ), + locationSequence=[ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-3/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) diff --git a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py index 3377e39b666..ea400011b22 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py @@ -18,7 +18,7 @@ ModuleLocation, OnLabwareLocation, LabwareOffset, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, LabwareOffsetVector, LabwareLocation, NonStackedLocation, @@ -142,7 +142,7 @@ async def set_up_decoy_hardware_gripper( id="new-offset-id", createdAt=datetime(year=2022, month=10, day=20), definitionUri="my-labware", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_5 ), # this location doesn't matter for this test vector=LabwareOffsetVector(x=0.5, y=0.6, z=0.7), diff --git a/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py index ba9c93e03cf..294266f21a8 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py @@ -1,4 +1,5 @@ """Test deck configuration provider.""" + from typing import List, Set import pytest @@ -246,7 +247,7 @@ def test_get_potential_cutout_fixtures_raises( AddressableArea( area_name="1", area_type=AreaType.SLOT, - base_slot=DeckSlotName.SLOT_A1, + base_slot=DeckSlotName.SLOT_1, display_name="Slot 1", bounding_box=Dimensions(x=128.0, y=86.0, z=0), position=AddressableOffsetVector(x=1, y=2, z=3), @@ -263,7 +264,7 @@ def test_get_potential_cutout_fixtures_raises( AddressableArea( area_name="1", area_type=AreaType.SLOT, - base_slot=DeckSlotName.SLOT_A1, + base_slot=DeckSlotName.SLOT_1, display_name="Slot 1", bounding_box=Dimensions(x=128.0, y=86.0, z=0), position=AddressableOffsetVector(x=1, y=2, z=3), @@ -280,7 +281,7 @@ def test_get_potential_cutout_fixtures_raises( AddressableArea( area_name="D1", area_type=AreaType.SLOT, - base_slot=DeckSlotName.SLOT_A1, + base_slot=DeckSlotName.SLOT_D1, display_name="Slot D1", bounding_box=Dimensions(x=128.0, y=86.0, z=0), position=AddressableOffsetVector(x=1, y=2, z=3), @@ -293,7 +294,7 @@ def test_get_potential_cutout_fixtures_raises( AddressableArea( area_name="movableTrashB3", area_type=AreaType.MOVABLE_TRASH, - base_slot=DeckSlotName.SLOT_A1, + base_slot=DeckSlotName.SLOT_B3, display_name="Trash Bin in B3", bounding_box=Dimensions(x=225, y=78, z=40), position=AddressableOffsetVector(x=-5.25, y=6, z=3), @@ -306,7 +307,7 @@ def test_get_potential_cutout_fixtures_raises( AddressableArea( area_name="gripperWasteChute", area_type=AreaType.WASTE_CHUTE, - base_slot=DeckSlotName.SLOT_A1, + base_slot=DeckSlotName.SLOT_D3, display_name="Waste Chute", bounding_box=Dimensions(x=0, y=0, z=0), position=AddressableOffsetVector(x=65, y=31, z=139.5), @@ -323,7 +324,7 @@ def test_get_addressable_area_from_name( ) -> None: """It should get the deck position for the requested cutout id.""" addressable_area = subject.get_addressable_area_from_name( - addressable_area_name, DeckPoint(x=1, y=2, z=3), DeckSlotName.SLOT_A1, deck_def + addressable_area_name, DeckPoint(x=1, y=2, z=3), deck_def ) assert addressable_area == expected_addressable_area @@ -336,6 +337,5 @@ def test_get_addressable_area_from_name_raises( subject.get_addressable_area_from_name( "theFunArea", DeckPoint(x=1, y=2, z=3), - DeckSlotName.SLOT_A1, ot3_standard_deck_def, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view_old.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view_old.py index 5aa157c59db..a85f51a1891 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view_old.py @@ -214,7 +214,6 @@ def test_get_addressable_area_for_simulation_not_loaded(decoy: Decoy) -> None: deck_configuration_provider.get_addressable_area_from_name( "abc", DeckPoint(x=1, y=2, z=3), - DeckSlotName.SLOT_A1, sentinel.deck_definition, ) ).then_return(addressable_area) diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index bf82c17c6bc..aa644aa9430 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -64,6 +64,9 @@ ProbedHeightInfo, LoadedVolumeInfo, WellLiquidInfo, + OnAddressableAreaOffsetLocationSequenceComponent, + OnModuleOffsetLocationSequenceComponent, + OnLabwareOffsetLocationSequenceComponent, ) from opentrons.protocol_engine.commands import ( CommandStatus, @@ -3017,10 +3020,9 @@ def test_get_offset_location_deck_slot( ) labware_store.handle_action(action) offset_location = subject.get_offset_location("labware-id-1") - assert offset_location is not None - assert offset_location.slotName == DeckSlotName.SLOT_C2 - assert offset_location.definitionUri is None - assert offset_location.moduleModel is None + assert offset_location == [ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="C2") + ] @pytest.mark.parametrize("use_mocks", [False]) @@ -3037,7 +3039,7 @@ def test_get_offset_location_module( command=LoadModule( params=LoadModuleParams( location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A3), - model=ModuleModel.TEMPERATURE_MODULE_V1, + model=ModuleModel.TEMPERATURE_MODULE_V2, ), id="load-module-1", createdAt=datetime.now(), @@ -3082,10 +3084,14 @@ def test_get_offset_location_module( module_store.handle_action(load_module) labware_store.handle_action(load_labware) offset_location = subject.get_offset_location("labware-id-1") - assert offset_location is not None - assert offset_location.slotName == DeckSlotName.SLOT_A3 - assert offset_location.definitionUri is None - assert offset_location.moduleModel == ModuleModel.TEMPERATURE_MODULE_V1 + assert offset_location == [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2A3" + ), + ] @pytest.mark.parametrize("use_mocks", [False]) @@ -3103,8 +3109,8 @@ def test_get_offset_location_module_with_adapter( load_module = SucceedCommandAction( command=LoadModule( params=LoadModuleParams( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A2), - model=ModuleModel.TEMPERATURE_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A3), + model=ModuleModel.TEMPERATURE_MODULE_V2, ), id="load-module-1", createdAt=datetime.now(), @@ -3177,12 +3183,17 @@ def test_get_offset_location_module_with_adapter( labware_store.handle_action(load_adapter) labware_store.handle_action(load_labware) offset_location = subject.get_offset_location("labware-id-1") - assert offset_location is not None - assert offset_location.slotName == DeckSlotName.SLOT_A2 - assert offset_location.definitionUri == labware_view.get_uri_from_definition( - nice_adapter_definition - ) - assert offset_location.moduleModel == ModuleModel.TEMPERATURE_MODULE_V1 + assert offset_location == [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri=labware_view.get_uri_from_definition(nice_adapter_definition) + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2A3" + ), + ] @pytest.mark.parametrize("use_mocks", [False]) diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py b/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py index d5e7e41770e..75e3aeb7339 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py @@ -4,6 +4,7 @@ longer helpful. Try to add new tests to test_labware_state.py, where they can be tested together, treating LabwareState as a private implementation detail. """ + from typing import Optional from opentrons.protocol_engine.state import update_types import pytest @@ -17,9 +18,10 @@ from opentrons.protocol_engine.types import ( LabwareOffset, - LabwareOffsetCreate, + LabwareOffsetCreateInternal, LabwareOffsetVector, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, + OnAddressableAreaOffsetLocationSequenceComponent, DeckSlotLocation, LoadedLabware, OFF_DECK_LOCATION, @@ -64,9 +66,12 @@ def test_handles_add_labware_offset( subject: LabwareStore, ) -> None: """It should add the labware offset to the state and add the ID.""" - request = LabwareOffsetCreate( + request = LabwareOffsetCreateInternal( definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) @@ -74,7 +79,10 @@ def test_handles_add_labware_offset( id="offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) @@ -99,9 +107,12 @@ def test_handles_load_labware( offset_id: Optional[str], ) -> None: """It should add the labware data to the state.""" - offset_request = LabwareOffsetCreate( + offset_request = LabwareOffsetCreateInternal( definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) @@ -180,9 +191,12 @@ def test_handles_reload_labware( == expected_definition_uri ) - offset_request = LabwareOffsetCreate( + offset_request = LabwareOffsetCreateInternal( definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) subject.handle_action( @@ -242,9 +256,12 @@ def test_handles_move_labware( ) -> None: """It should update labware state with new location & offset.""" comment_command = create_comment_command() - offset_request = LabwareOffsetCreate( + offset_request = LabwareOffsetCreateInternal( definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) subject.handle_action( @@ -297,9 +314,12 @@ def test_handles_move_labware_off_deck( ) -> None: """It should update labware state with new location & offset.""" comment_command = create_comment_command() - offset_request = LabwareOffsetCreate( + offset_request = LabwareOffsetCreateInternal( definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) subject.handle_action( diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py index 770f24f3e7f..699893b47b4 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py @@ -4,6 +4,7 @@ longer helpful. Try to add new tests to test_labware_state.py, where they can be tested together, treating LabwareState as a private implementation detail. """ + import pytest from datetime import datetime from typing import Dict, Optional, cast, ContextManager, Any, Union, NamedTuple, List @@ -34,7 +35,7 @@ Dimensions, LabwareOffset, LabwareOffsetVector, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, LoadedLabware, ModuleModel, ModuleLocation, @@ -44,6 +45,8 @@ OFF_DECK_LOCATION, OverlapOffset, LabwareMovementOffsetData, + OnAddressableAreaOffsetLocationSequenceComponent, + OnModuleOffsetLocationSequenceComponent, ) from opentrons.protocol_engine.state._move_types import EdgePathType from opentrons.protocol_engine.state.labware import ( @@ -838,7 +841,10 @@ def test_get_labware_offset_vector() -> None: id="offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="some-labware-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=offset_vector, ) @@ -866,7 +872,10 @@ def test_get_labware_offset() -> None: id="id-a", createdAt=datetime(year=2021, month=1, day=1), definitionUri="uri-a", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=1, z=1), ) @@ -874,7 +883,10 @@ def test_get_labware_offset() -> None: id="id-b", createdAt=datetime(year=2022, month=2, day=2), definitionUri="uri-b", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="2") + ], vector=LabwareOffsetVector(x=2, y=2, z=2), ) @@ -894,7 +906,10 @@ def test_get_labware_offsets() -> None: id="id-a", createdAt=datetime(year=2021, month=1, day=1), definitionUri="uri-a", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=1, z=1), ) @@ -902,7 +917,10 @@ def test_get_labware_offsets() -> None: id="id-b", createdAt=datetime(year=2022, month=2, day=2), definitionUri="uri-b", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="2") + ], vector=LabwareOffsetVector(x=2, y=2, z=2), ) @@ -926,7 +944,10 @@ def test_find_applicable_labware_offset() -> None: id="id-1", createdAt=datetime(year=2021, month=1, day=1), definitionUri="definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=1, z=1), ) @@ -935,7 +956,10 @@ def test_find_applicable_labware_offset() -> None: id="id-2", createdAt=datetime(year=2022, month=2, day=2), definitionUri="definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=2, y=2, z=2), ) @@ -943,10 +967,16 @@ def test_find_applicable_labware_offset() -> None: id="id-3", createdAt=datetime(year=2023, month=3, day=3), definitionUri="on-module-definition-uri", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_1, moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, ), + locationSequence=[ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1"), + ], vector=LabwareOffsetVector(x=3, y=3, z=3), ) @@ -959,7 +989,11 @@ def test_find_applicable_labware_offset() -> None: assert ( subject.find_applicable_labware_offset( definition_uri="definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ) + ], ) == offset_2 ) @@ -967,10 +1001,14 @@ def test_find_applicable_labware_offset() -> None: assert ( subject.find_applicable_labware_offset( definition_uri="on-module-definition-uri", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_1, - moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, - ), + location=[ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], ) == offset_3 ) @@ -979,7 +1017,11 @@ def test_find_applicable_labware_offset() -> None: assert ( subject.find_applicable_labware_offset( definition_uri="different-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ) + ], ) is None ) @@ -988,7 +1030,11 @@ def test_find_applicable_labware_offset() -> None: assert ( subject.find_applicable_labware_offset( definition_uri="different-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="2" + ) + ], ) is None ) diff --git a/api/tests/opentrons/protocol_engine/test_labware_offset_standardization.py b/api/tests/opentrons/protocol_engine/test_labware_offset_standardization.py new file mode 100644 index 00000000000..f78885a8428 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/test_labware_offset_standardization.py @@ -0,0 +1,725 @@ +"""Tests for `labware_offset_standardization`.""" + +from functools import lru_cache +import pytest + +from opentrons_shared_data.robot.types import RobotType +from opentrons_shared_data.deck import load +from opentrons_shared_data.deck.types import DeckDefinitionV5 +from opentrons.types import DeckSlotName +from opentrons.protocol_engine import labware_offset_standardization as subject +from opentrons.protocol_engine.types import ( + LabwareOffsetCreate, + LegacyLabwareOffsetCreate, + LegacyLabwareOffsetLocation, + OnLabwareOffsetLocationSequenceComponent, + OnModuleOffsetLocationSequenceComponent, + OnAddressableAreaOffsetLocationSequenceComponent, + ModuleModel, + LabwareOffsetVector, + LabwareOffsetLocationSequence, + LabwareOffsetCreateInternal, +) + + +@lru_cache +def load_from_robot_type(robot_type: RobotType) -> DeckDefinitionV5: + """Get a deck from robot type.""" + if robot_type == "OT-3 Standard": + return load("ot3_standard") + else: + return load("ot2_standard") + + +@pytest.mark.parametrize( + ("location", "robot_type", "expected_modern_location", "expected_legacy_location"), + [ + # Directly on a slot + pytest.param( + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + "OT-2 Standard", + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="5")], + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + id="direct-slot-ot2-native", + ), + pytest.param( + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + "OT-3 Standard", + [ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="C2" + ) + ], + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_C2), + id="direct-slot-flex-ot2", + ), + pytest.param( + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_C2), + "OT-2 Standard", + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="5")], + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + id="direct-slot-ot2-flex", + ), + pytest.param( + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_C2), + "OT-3 Standard", + [ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="C2" + ) + ], + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_C2), + id="direct-slot-flex-native", + ), + # On a module with no adapter + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-3 Standard", + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="module-flex-native", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-3 Standard", + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="module-flex-ot2", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-2 Standard", + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="module-ot2-flex", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-2 Standard", + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="module-ot2-native", + ), + # On a labware (or stack...) on a slot + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, definitionUri="opentrons-test/foo/1" + ), + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="D1" + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, definitionUri="opentrons-test/foo/1" + ), + id="labware-slot-flex-native", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, definitionUri="opentrons-test/foo/1" + ), + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, definitionUri="opentrons-test/foo/1" + ), + id="labware-slot-ot2-flex", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, definitionUri="opentrons-test/foo/1" + ), + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="D1" + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, definitionUri="opentrons-test/foo/1" + ), + id="labware-slot-flex-ot2", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, definitionUri="opentrons-test/foo/1" + ), + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, definitionUri="opentrons-test/foo/1" + ), + id="labware-slot-ot2-native", + ), + # On an adapter on a module + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-module-flex-native", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-module-ot2-flex", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-module-flex-ot2", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-module-ot2-native", + ), + ], +) +def test_standardize_legacy_labware_offset( + location: LegacyLabwareOffsetLocation, + robot_type: RobotType, + expected_modern_location: LabwareOffsetLocationSequence, + expected_legacy_location: LegacyLabwareOffsetLocation, +) -> None: + """It should convert deck slots in `LegacyLabwareOffsetCreate`s and go to the new format.""" + deck_def = load_from_robot_type(robot_type) + original = LegacyLabwareOffsetCreate( + definitionUri="opentrons-test/foo/1", + location=location, + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + expected = LabwareOffsetCreateInternal( + definitionUri="opentrons-test/foo/1", + legacyLocation=expected_legacy_location, + locationSequence=expected_modern_location, + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + assert ( + subject.standardize_labware_offset_create(original, robot_type, deck_def) + == expected + ) + + +@pytest.mark.parametrize( + ("location", "robot_type", "expected_modern_location", "expected_legacy_location"), + [ + # Directly on a slot + pytest.param( + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="5")], + "OT-2 Standard", + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="5")], + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + id="slot-direct-ot2", + ), + pytest.param( + [ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="C2" + ) + ], + "OT-3 Standard", + [ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="C2" + ) + ], + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_C2), + id="slot-direct-flex", + ), + # On a module with no adapter + pytest.param( + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + "OT-3 Standard", + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="module-slot-flex", + ), + pytest.param( + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + "OT-2 Standard", + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="module-slot-ot2", + ), + # On a labware on a slot + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="D1" + ), + ], + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="D1" + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, definitionUri="opentrons-test/foo/1" + ), + id="labware-slot-flex", + ), + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, definitionUri="opentrons-test/foo/1" + ), + id="labware-slot-ot2", + ), + # On an adapter on a module + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-module-flex", + ), + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-slot-ot2", + ), + # On a stack of labware + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="A3", + ), + ], + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="A3", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_A3, + definitionUri="opentrons-test/foo/1", + ), + id="labware-stack-flex", + ), + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="2", + ), + ], + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="2", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_2, + definitionUri="opentrons-test/foo/1", + ), + id="labware-stack-ot2", + ), + # On a stack of labware on a module + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3", + ), + ], + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_3, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-stack-module-ot2", + ), + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="A1", + ), + ], + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="A1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_A1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-stack-module-flex", + ), + ], +) +def test_standardize_modern_labware_offset( + location: LabwareOffsetLocationSequence, + robot_type: RobotType, + expected_modern_location: LabwareOffsetLocationSequence, + expected_legacy_location: LegacyLabwareOffsetLocation, +) -> None: + """It should convert deck slots in `LabwareOffsetCreate`s and fill in the old format.""" + deck_def = load_from_robot_type(robot_type) + original = LabwareOffsetCreate( + definitionUri="opentrons-test/foo/1", + locationSequence=location, + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + expected = LabwareOffsetCreateInternal( + definitionUri="opentrons-test/foo/1", + legacyLocation=expected_legacy_location, + locationSequence=expected_modern_location, + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + assert ( + subject.standardize_labware_offset_create(original, robot_type, deck_def) + == expected + ) diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index 6c1efcc55d7..ed933e760d0 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -2,7 +2,7 @@ import inspect from datetime import datetime -from typing import Any +from typing import Any, cast from unittest.mock import sentinel import pytest @@ -10,6 +10,7 @@ from opentrons_shared_data.robot.types import RobotType from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.deck.types import DeckDefinitionV5 from opentrons.protocol_engine.actions.actions import SetErrorRecoveryPolicyAction from opentrons.protocol_engine.state.update_types import StateUpdate @@ -18,7 +19,12 @@ from opentrons.hardware_control.modules import MagDeck, TempDeck from opentrons.hardware_control.types import PauseType as HardwarePauseType -from opentrons.protocol_engine import ProtocolEngine, commands, slot_standardization +from opentrons.protocol_engine import ( + ProtocolEngine, + commands, + slot_standardization, + labware_offset_standardization, +) from opentrons.protocol_engine.errors.exceptions import ( CommandNotAllowedError, ) @@ -26,8 +32,11 @@ DeckType, LabwareOffset, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, LabwareOffsetVector, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, + OnAddressableAreaOffsetLocationSequenceComponent, + LabwareOffsetCreateInternal, LabwareUri, ModuleDefinition, ModuleModel, @@ -138,6 +147,17 @@ def _mock_slot_standardization_module( monkeypatch.setattr(slot_standardization, name, decoy.mock(func=func)) +@pytest.fixture(autouse=True) +def _mock_labware_offset_standardization_module( + decoy: Decoy, monkeypatch: pytest.MonkeyPatch +) -> None: + """Mock out opentrons.labware_offset_standardization functions.""" + for name, func in inspect.getmembers( + labware_offset_standardization, inspect.isfunction + ): + monkeypatch.setattr(labware_offset_standardization, name, decoy.mock(func=func)) + + @pytest.fixture(autouse=True) def _mock_hash_command_params_module( decoy: Decoy, monkeypatch: pytest.MonkeyPatch @@ -1020,6 +1040,81 @@ def test_add_plugin( decoy.verify(plugin_starter.start(plugin)) +def test_add_legacy_labware_offset( + decoy: Decoy, + action_dispatcher: ActionDispatcher, + model_utils: ModelUtils, + state_store: StateStore, + subject: ProtocolEngine, +) -> None: + """It should have the labware offset request resolved and added to state.""" + request = LegacyLabwareOffsetCreate( + definitionUri="definition-uri", + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + + standardized_request = LabwareOffsetCreateInternal( + definitionUri="standardized-definition-uri", + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="2") + ], + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + vector=LabwareOffsetVector(x=2, y=3, z=4), + ) + + id = "labware-offset-id" + + created_at = datetime(year=2021, month=11, day=15) + + expected_result = LabwareOffset( + id=id, + createdAt=created_at, + definitionUri=standardized_request.definitionUri, + location=standardized_request.legacyLocation, + locationSequence=standardized_request.locationSequence, + vector=standardized_request.vector, + ) + + robot_type: RobotType = "OT-3 Standard" + decoy.when(state_store.config).then_return( + Config(robot_type=robot_type, deck_type=DeckType.OT3_STANDARD) + ) + decoy.when(state_store.addressable_areas.deck_definition).then_return( + cast(DeckDefinitionV5, {}) + ) + decoy.when( + labware_offset_standardization.standardize_labware_offset_create( + request, robot_type, cast(DeckDefinitionV5, {}) + ) + ).then_return(standardized_request) + decoy.when(model_utils.generate_id()).then_return(id) + decoy.when(model_utils.get_timestamp()).then_return(created_at) + decoy.when( + state_store.labware.get_labware_offset(labware_offset_id=id) + ).then_return(expected_result) + + result = subject.add_labware_offset( + request=LegacyLabwareOffsetCreate( + definitionUri="definition-uri", + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + ) + + assert result == expected_result + + decoy.verify( + action_dispatcher.dispatch( + AddLabwareOffsetAction( + labware_offset_id=id, + created_at=created_at, + request=standardized_request, + ) + ) + ) + + def test_add_labware_offset( decoy: Decoy, action_dispatcher: ActionDispatcher, @@ -1030,23 +1125,31 @@ def test_add_labware_offset( """It should have the labware offset request resolved and added to state.""" request = LabwareOffsetCreate( definitionUri="definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) - standardized_request = LabwareOffsetCreate( + + standardized_request = LabwareOffsetCreateInternal( definitionUri="standardized-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), - vector=LabwareOffsetVector(x=2, y=3, z=4), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="3") + ], + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_3), + vector=LabwareOffsetVector(x=2, y=5, z=6), ) id = "labware-offset-id" + created_at = datetime(year=2021, month=11, day=15) expected_result = LabwareOffset( id=id, createdAt=created_at, definitionUri=standardized_request.definitionUri, - location=standardized_request.location, + location=standardized_request.legacyLocation, + locationSequence=standardized_request.locationSequence, vector=standardized_request.vector, ) @@ -1054,8 +1157,13 @@ def test_add_labware_offset( decoy.when(state_store.config).then_return( Config(robot_type=robot_type, deck_type=DeckType.OT3_STANDARD) ) + decoy.when(state_store.addressable_areas.deck_definition).then_return( + cast(DeckDefinitionV5, {}) + ) decoy.when( - slot_standardization.standardize_labware_offset(request, robot_type) + labware_offset_standardization.standardize_labware_offset_create( + request, robot_type, cast(DeckDefinitionV5, {}) + ) ).then_return(standardized_request) decoy.when(model_utils.generate_id()).then_return(id) decoy.when(model_utils.get_timestamp()).then_return(created_at) @@ -1066,7 +1174,11 @@ def test_add_labware_offset( result = subject.add_labware_offset( request=LabwareOffsetCreate( definitionUri="definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ) + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) diff --git a/api/tests/opentrons/protocol_engine/test_slot_standardization.py b/api/tests/opentrons/protocol_engine/test_slot_standardization.py index f97d09af242..78090e16b00 100644 --- a/api/tests/opentrons/protocol_engine/test_slot_standardization.py +++ b/api/tests/opentrons/protocol_engine/test_slot_standardization.py @@ -13,8 +13,6 @@ OnLabwareLocation, LabwareLocation, LabwareMovementStrategy, - LabwareOffsetCreate, - LabwareOffsetLocation, LabwareOffsetVector, ModuleLocation, ModuleModel, @@ -22,42 +20,6 @@ ) -@pytest.mark.parametrize("module_model", [None, ModuleModel.MAGNETIC_MODULE_V1]) -@pytest.mark.parametrize( - ("slot_name", "robot_type", "expected_slot_name"), - [ - (DeckSlotName.SLOT_5, "OT-2 Standard", DeckSlotName.SLOT_5), - (DeckSlotName.SLOT_C2, "OT-2 Standard", DeckSlotName.SLOT_5), - (DeckSlotName.SLOT_5, "OT-3 Standard", DeckSlotName.SLOT_C2), - (DeckSlotName.SLOT_C2, "OT-3 Standard", DeckSlotName.SLOT_C2), - ], -) -def test_standardize_labware_offset( - module_model: ModuleModel, - slot_name: DeckSlotName, - robot_type: RobotType, - expected_slot_name: DeckSlotName, -) -> None: - """It should convert deck slots in `LabwareOffsetCreate`s.""" - original = LabwareOffsetCreate( - definitionUri="opentrons-test/foo/1", - location=LabwareOffsetLocation( - moduleModel=module_model, - slotName=slot_name, - ), - vector=LabwareOffsetVector(x=1, y=2, z=3), - ) - expected = LabwareOffsetCreate( - definitionUri="opentrons-test/foo/1", - location=LabwareOffsetLocation( - moduleModel=module_model, - slotName=expected_slot_name, - ), - vector=LabwareOffsetVector(x=1, y=2, z=3), - ) - assert subject.standardize_labware_offset(original, robot_type) == expected - - @pytest.mark.parametrize( ("original_location", "robot_type", "expected_location"), [ diff --git a/app/src/molecules/PythonLabwareOffsetSnippet/index.tsx b/app/src/molecules/PythonLabwareOffsetSnippet/index.tsx index d6e1e4bbd7d..b416858d156 100644 --- a/app/src/molecules/PythonLabwareOffsetSnippet/index.tsx +++ b/app/src/molecules/PythonLabwareOffsetSnippet/index.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import styled from 'styled-components' import { TYPOGRAPHY, SPACING, BORDERS, COLORS } from '@opentrons/components' import { createSnippet } from './createSnippet' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' import type { LoadedLabware, LoadedModule, @@ -25,7 +25,7 @@ interface PythonLabwareOffsetSnippetProps { commands: RunTimeCommand[] labware: LoadedLabware[] modules: LoadedModule[] - labwareOffsets: LabwareOffsetCreateData[] | null + labwareOffsets: LegacyLabwareOffsetCreateData[] | null } export function PythonLabwareOffsetSnippet( diff --git a/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts b/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts index 58d1bdf08df..44ba2933189 100644 --- a/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts +++ b/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts @@ -12,7 +12,7 @@ import { getValidCustomLabwareFiles } from '/app/redux/custom-labware/selectors' import type { UseMutateFunction } from 'react-query' import type { HostConfig, - LabwareOffsetCreateData, + LegacyLabwareOffsetCreateData, Protocol, } from '@opentrons/api-client' import type { UseCreateRunMutationOptions } from '@opentrons/react-api-client/src/runs/useCreateRunMutation' @@ -35,7 +35,7 @@ export interface UseCreateRun { export function useCreateRunFromProtocol( options: UseCreateRunMutationOptions, hostOverride?: HostConfig | null, - labwareOffsets?: LabwareOffsetCreateData[] + labwareOffsets?: LegacyLabwareOffsetCreateData[] ): UseCreateRun { const contextHost = useHost() const host = diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts index 6fd4f57c1d3..8c5487a66e9 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts @@ -10,7 +10,7 @@ import type { CompletedProtocolAnalysis, CreateCommand, } from '@opentrons/shared-data' -import type { LabwareOffsetLocation } from '@opentrons/api-client' +import type { LegacyLabwareOffsetLocation } from '@opentrons/api-client' export interface BuildModulePrepCommandsParams { step: CheckPositionsStep @@ -128,7 +128,7 @@ const thermocyclerInitCommands = ( const heaterShakerCleanupCommands = ( moduleId: string | undefined, - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation ): CreateCommand[] => { const moduleType = (moduleId != null && diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts index 64c505d9fbd..29183c98f06 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts @@ -2,7 +2,7 @@ import { useState } from 'react' import { useCreateLabwareOffsetMutation } from '@opentrons/react-api-client' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' import type { UseLPCCommandChildProps } from './types' export interface UseApplyLPCOffsetsProps extends UseLPCCommandChildProps { @@ -11,7 +11,7 @@ export interface UseApplyLPCOffsetsProps extends UseLPCCommandChildProps { export interface UseApplyLPCOffsetsResult { handleApplyOffsetsAndClose: ( - offsets: LabwareOffsetCreateData[] + offsets: LegacyLabwareOffsetCreateData[] ) => Promise isApplyingOffsets: boolean } @@ -26,7 +26,7 @@ export function useApplyLPCOffsets({ const { createLabwareOffset } = useCreateLabwareOffsetMutation() const handleApplyOffsetsAndClose = ( - offsets: LabwareOffsetCreateData[] + offsets: LegacyLabwareOffsetCreateData[] ): Promise => { setIsApplyingOffsets(true) return Promise.all( diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts index 6b4b01d6632..56667853952 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts @@ -3,11 +3,11 @@ import { useStore } from 'react-redux' import { selectOffsetsToApply } from '/app/redux/protocol-runs' import type { State } from '/app/redux/types' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' import type { UseLPCCommandChildProps } from '/app/organisms/LabwarePositionCheck/hooks/useLPCCommands/types' export interface UseBuildOffsetsToApplyResult { - buildOffsetsToApply: () => LabwareOffsetCreateData[] + buildOffsetsToApply: () => LegacyLabwareOffsetCreateData[] } export interface UseApplyLPCOffsetsProps extends UseLPCCommandChildProps { diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx index d35c3d2f885..46d8d430bbb 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx @@ -18,7 +18,7 @@ import { import { selectLwDisplayName } from '/app/redux/protocol-runs' import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' import type { LPCStepProps, ResultsSummaryStep, @@ -27,7 +27,7 @@ import type { LPCWizardState } from '/app/redux/protocol-runs' import type { State } from '/app/redux/types' interface OffsetTableProps extends LPCStepProps { - offsets: LabwareOffsetCreateData[] + offsets: LegacyLabwareOffsetCreateData[] labwareDefinitions: LabwareDefinition2[] } diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx index 90ddb8edcf1..7e1dce4d58e 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx @@ -4,7 +4,7 @@ import { TerseOffsetTable } from '/app/organisms/TerseOffsetTable' import { OffsetTable } from './OffsetTable' import { getIsOnDevice } from '/app/redux/config' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' import type { LPCStepProps, ResultsSummaryStep, @@ -13,7 +13,7 @@ import type { State } from '/app/redux/types' import type { LPCWizardState } from '/app/redux/protocol-runs' interface TableComponentProps extends LPCStepProps { - offsetsToApply: LabwareOffsetCreateData[] + offsetsToApply: LegacyLabwareOffsetCreateData[] } export function TableComponent(props: TableComponentProps): JSX.Element { diff --git a/app/src/organisms/LabwarePositionCheck/types/steps.ts b/app/src/organisms/LabwarePositionCheck/types/steps.ts index 3cc781aebff..17caa519d1b 100644 --- a/app/src/organisms/LabwarePositionCheck/types/steps.ts +++ b/app/src/organisms/LabwarePositionCheck/types/steps.ts @@ -1,4 +1,4 @@ -import type { LabwareOffsetLocation } from '@opentrons/api-client' +import type { LegacyLabwareOffsetLocation } from '@opentrons/api-client' import type { NAV_STEPS } from '../constants' import type { LPCWizardContentProps } from './content' @@ -19,7 +19,7 @@ export type LPCStepProps = Omit< export interface PerformLPCStep { pipetteId: string labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string adapterId?: string moduleId?: string diff --git a/app/src/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts b/app/src/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts index 4dc49c5ca23..9422c3de1cf 100644 --- a/app/src/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts +++ b/app/src/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts @@ -9,10 +9,10 @@ import type { ProtocolAnalysisOutput, RunTimeCommand, } from '@opentrons/shared-data' -import type { LabwareOffsetLocation } from '@opentrons/api-client' +import type { LegacyLabwareOffsetLocation } from '@opentrons/api-client' export interface LabwareLocationCombo { - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string labwareId: string moduleId?: string @@ -167,7 +167,7 @@ function appendLocationComboIfUniq( function resolveModuleLocation( modules: ProtocolAnalysisOutput['modules'], moduleId: string -): LabwareOffsetLocation | null { +): LegacyLabwareOffsetLocation | null { const moduleEntity = modules.find(m => m.id === moduleId) if (moduleEntity == null) { console.warn( @@ -182,7 +182,7 @@ function resolveModuleLocation( } interface ResolveAdapterLocation { - adapterOffsetLocation: LabwareOffsetLocation | null + adapterOffsetLocation: LegacyLabwareOffsetLocation | null moduleIdUnderAdapter?: string } function resolveAdapterLocation( @@ -200,7 +200,7 @@ function resolveAdapterLocation( const labwareDefUri = labwareEntity.definitionUri let moduleIdUnderAdapter - let adapterOffsetLocation: LabwareOffsetLocation | null = null + let adapterOffsetLocation: LegacyLabwareOffsetLocation | null = null if ( labwareEntity.location === 'offDeck' || labwareEntity.location === 'systemLocation' @@ -211,7 +211,7 @@ function resolveAdapterLocation( return { adapterOffsetLocation: null } } else if ('moduleId' in labwareEntity.location) { const moduleId = labwareEntity.location.moduleId - const resolvedModuleLocation: LabwareOffsetLocation | null = resolveModuleLocation( + const resolvedModuleLocation: LegacyLabwareOffsetLocation | null = resolveModuleLocation( modules, moduleId ) diff --git a/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx index 6ff45018b1a..57309df764b 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx @@ -39,7 +39,7 @@ import type { RobotType, } from '@opentrons/shared-data' import type { - LabwareOffsetCreateData, + LegacyLabwareOffsetCreateData, LabwareOffset, CommandData, } from '@opentrons/api-client' @@ -310,7 +310,9 @@ export const LabwarePositionCheckComponent = ( robotType, } - const handleApplyOffsets = (offsets: LabwareOffsetCreateData[]): void => { + const handleApplyOffsets = ( + offsets: LegacyLabwareOffsetCreateData[] + ): void => { setIsApplyingOffsets(true) Promise.all(offsets.map(data => createLabwareOffset({ runId, data }))) .then(() => { diff --git a/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx b/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx index 22934c0f8a0..8e53f40c366 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx @@ -48,7 +48,7 @@ import type { } from '@opentrons/shared-data' import type { LabwareOffset, - LabwareOffsetCreateData, + LegacyLabwareOffsetCreateData, } from '@opentrons/api-client' import type { ResultsSummaryStep, WorkingOffset } from './types' import type { TFunction } from 'i18next' @@ -60,7 +60,7 @@ interface ResultsSummaryProps extends ResultsSummaryStep { protocolData: CompletedProtocolAnalysis workingOffsets: WorkingOffset[] existingOffsets: LabwareOffset[] - handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void + handleApplyOffsets: (offsets: LegacyLabwareOffsetCreateData[]) => void isApplyingOffsets: boolean isDeletingMaintenanceRun?: boolean } @@ -86,7 +86,7 @@ export const ResultsSummary = ( const isOnDevice = useSelector(getIsOnDevice) const offsetsToApply = useMemo(() => { - return workingOffsets.map( + return workingOffsets.map( ({ initialPosition, finalPosition, labwareId, location }) => { const definitionUri = protocolData.labware.find(l => l.id === labwareId)?.definitionUri ?? @@ -269,7 +269,7 @@ const ScrollContainer = styled(Flex)` ` interface OffsetTableProps { - offsets: LabwareOffsetCreateData[] + offsets: LegacyLabwareOffsetCreateData[] labwareDefinitions: LabwareDefinition2[] } diff --git a/app/src/organisms/LegacyLabwarePositionCheck/types.ts b/app/src/organisms/LegacyLabwarePositionCheck/types.ts index 2ddd14c25d6..4cb26dbfaed 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/types.ts +++ b/app/src/organisms/LegacyLabwarePositionCheck/types.ts @@ -1,6 +1,9 @@ import type { SECTIONS } from './constants' import type { useCreateCommandMutation } from '@opentrons/react-api-client' -import type { LabwareOffsetLocation, VectorOffset } from '@opentrons/api-client' +import type { + LegacyLabwareOffsetLocation, + VectorOffset, +} from '@opentrons/api-client' import type { LabwareDefinition2 } from '@opentrons/shared-data' export type LabwarePositionCheckStep = @@ -20,7 +23,7 @@ export interface CheckTipRacksStep { section: typeof SECTIONS.CHECK_TIP_RACKS pipetteId: string labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string adapterId?: string } @@ -32,7 +35,7 @@ export interface PickUpTipStep { section: typeof SECTIONS.PICK_UP_TIP pipetteId: string labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string adapterId?: string } @@ -40,7 +43,7 @@ export interface CheckPositionsStep { section: typeof SECTIONS.CHECK_POSITIONS pipetteId: string labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string moduleId?: string } @@ -48,7 +51,7 @@ export interface CheckLabwareStep { section: typeof SECTIONS.CHECK_LABWARE pipetteId: string labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string moduleId?: string adapterId?: string @@ -57,7 +60,7 @@ export interface ReturnTipStep { section: typeof SECTIONS.RETURN_TIP pipetteId: string labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string adapterId?: string } @@ -80,13 +83,13 @@ export type CreateRunCommand = ( interface InitialPositionAction { type: 'initialPosition' labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation position: VectorOffset | null } interface FinalPositionAction { type: 'finalPosition' labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation position: VectorOffset | null } interface TipPickUpOffsetAction { @@ -99,7 +102,7 @@ export type RegisterPositionAction = | TipPickUpOffsetAction export interface WorkingOffset { labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation initialPosition: VectorOffset | null finalPosition: VectorOffset | null } diff --git a/app/src/organisms/LegacyLabwarePositionCheck/utils/getDisplayLocation.ts b/app/src/organisms/LegacyLabwarePositionCheck/utils/getDisplayLocation.ts index d70b741c48d..88ab9976719 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/utils/getDisplayLocation.ts +++ b/app/src/organisms/LegacyLabwarePositionCheck/utils/getDisplayLocation.ts @@ -6,10 +6,10 @@ import { } from '@opentrons/shared-data' import type { i18n, TFunction } from 'i18next' import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { LabwareOffsetLocation } from '@opentrons/api-client' +import type { LegacyLabwareOffsetLocation } from '@opentrons/api-client' export function getDisplayLocation( - location: LabwareOffsetLocation, + location: LegacyLabwareOffsetLocation, labwareDefinitions: LabwareDefinition2[], t: TFunction, i18n: i18n, diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx index ec4df679049..519a5a32150 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx @@ -40,12 +40,15 @@ import type { CsvFileParameterFileData, } from '@opentrons/shared-data' import type { ProtocolSetupStepStatus } from '../ProtocolSetupStep' -import type { FileData, LabwareOffsetCreateData } from '@opentrons/api-client' +import type { + FileData, + LegacyLabwareOffsetCreateData, +} from '@opentrons/api-client' interface ProtocolSetupParametersProps { protocolId: string runTimeParameters: RunTimeParameter[] - labwareOffsets?: LabwareOffsetCreateData[] + labwareOffsets?: LegacyLabwareOffsetCreateData[] mostRecentAnalysis?: CompletedProtocolAnalysis | null } diff --git a/app/src/organisms/TerseOffsetTable/index.tsx b/app/src/organisms/TerseOffsetTable/index.tsx index 5fdbaf162a2..d1f4ac13da5 100644 --- a/app/src/organisms/TerseOffsetTable/index.tsx +++ b/app/src/organisms/TerseOffsetTable/index.tsx @@ -21,11 +21,11 @@ import { DIRECTION_ROW, } from '@opentrons/components' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' import type { LabwareDefinition2 } from '@opentrons/shared-data' export interface TerseOffsetTableProps { - offsets: LabwareOffsetCreateData[] + offsets: LegacyLabwareOffsetCreateData[] labwareDefinitions: LabwareDefinition2[] } diff --git a/app/src/redux/protocol-runs/selectors/lpc/labware.ts b/app/src/redux/protocol-runs/selectors/lpc/labware.ts index afce4ae46e1..fd831163cc2 100644 --- a/app/src/redux/protocol-runs/selectors/lpc/labware.ts +++ b/app/src/redux/protocol-runs/selectors/lpc/labware.ts @@ -14,7 +14,10 @@ import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analy import { getItemLabwareDef } from './transforms' import type { Selector } from 'reselect' -import type { VectorOffset, LabwareOffsetLocation } from '@opentrons/api-client' +import type { + VectorOffset, + LegacyLabwareOffsetLocation, +} from '@opentrons/api-client' import type { LabwareDefinition2, Coordinates } from '@opentrons/shared-data' import type { State } from '../../../types' @@ -88,7 +91,7 @@ export const selectActiveLwExistingOffset = ( export interface SelectOffsetsToApplyResult { definitionUri: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation vector: Coordinates } diff --git a/app/src/redux/protocol-runs/types/lpc.ts b/app/src/redux/protocol-runs/types/lpc.ts index aec42e6eb98..0e31a166bf1 100644 --- a/app/src/redux/protocol-runs/types/lpc.ts +++ b/app/src/redux/protocol-runs/types/lpc.ts @@ -4,7 +4,7 @@ import type { CompletedProtocolAnalysis, } from '@opentrons/shared-data' import type { - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, VectorOffset, LabwareOffset, } from '@opentrons/api-client' @@ -15,13 +15,13 @@ import type { StepsInfo } from '/app/organisms/LabwarePositionCheck/redux/types' export interface PositionParams { labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation position: VectorOffset | null } export interface WorkingOffset { labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation initialPosition: VectorOffset | null finalPosition: VectorOffset | null } diff --git a/app/src/transformations/analysis/getLabwareOffsetLocation.ts b/app/src/transformations/analysis/getLabwareOffsetLocation.ts index 526c3f780cb..6043c4c7eca 100644 --- a/app/src/transformations/analysis/getLabwareOffsetLocation.ts +++ b/app/src/transformations/analysis/getLabwareOffsetLocation.ts @@ -2,7 +2,7 @@ import { getModuleInitialLoadInfo, getLabwareLocation, } from '/app/transformations/commands' -import type { LabwareOffsetLocation } from '@opentrons/api-client' +import type { LegacyLabwareOffsetLocation } from '@opentrons/api-client' import type { LoadedModule, LoadedLabware, @@ -17,7 +17,7 @@ export const getLabwareOffsetLocation = ( commands: ProtocolAnalysisOutput['commands'], modules: LoadedModule[], labware: LoadedLabware[] -): LabwareOffsetLocation | null => { +): LegacyLabwareOffsetLocation | null => { const labwareLocation = getLabwareLocation(labwareId, commands) if (labwareLocation === 'offDeck' || labwareLocation === 'systemLocation') { diff --git a/react-api-client/src/runs/__tests__/useCreateLabwareOffsetsMutation.test.tsx b/react-api-client/src/runs/__tests__/useCreateLabwareOffsetsMutation.test.tsx index beb57a9ca72..e2b83c5741c 100644 --- a/react-api-client/src/runs/__tests__/useCreateLabwareOffsetsMutation.test.tsx +++ b/react-api-client/src/runs/__tests__/useCreateLabwareOffsetsMutation.test.tsx @@ -6,7 +6,10 @@ import { createLabwareOffset } from '@opentrons/api-client' import { useHost } from '../../api' import { useCreateLabwareOffsetMutation } from '../useCreateLabwareOffsetMutation' -import type { HostConfig, LabwareOffsetCreateData } from '@opentrons/api-client' +import type { + HostConfig, + LegacyLabwareOffsetCreateData, +} from '@opentrons/api-client' vi.mock('@opentrons/api-client') vi.mock('../../api/useHost') @@ -19,7 +22,7 @@ const OFFSET = { x: 1, y: 2, z: 3 } describe('useCreateLabwareOffsetMutation hook', () => { let wrapper: React.FunctionComponent<{ children: React.ReactNode }> - let labwareOffset: LabwareOffsetCreateData + let labwareOffset: LegacyLabwareOffsetCreateData beforeEach(() => { const queryClient = new QueryClient() diff --git a/react-api-client/src/runs/useCreateLabwareOffsetMutation.ts b/react-api-client/src/runs/useCreateLabwareOffsetMutation.ts index adf92f9657c..f1b04505b30 100644 --- a/react-api-client/src/runs/useCreateLabwareOffsetMutation.ts +++ b/react-api-client/src/runs/useCreateLabwareOffsetMutation.ts @@ -4,13 +4,13 @@ import { useHost } from '../api' import type { HostConfig, Run, - LabwareOffsetCreateData, + LegacyLabwareOffsetCreateData, } from '@opentrons/api-client' import type { UseMutationResult, UseMutateAsyncFunction } from 'react-query' interface CreateLabwareOffsetParams { runId: string - data: LabwareOffsetCreateData + data: LegacyLabwareOffsetCreateData } export type UseCreateLabwareOffsetMutationResult = UseMutationResult< diff --git a/robot-server/robot_server/labware_offsets/router.py b/robot-server/robot_server/labware_offsets/router.py index 3a01ca73278..e75bb27d926 100644 --- a/robot-server/robot_server/labware_offsets/router.py +++ b/robot-server/robot_server/labware_offsets/router.py @@ -1,6 +1,5 @@ """FastAPI endpoint functions for the `/labwareOffsets` endpoints.""" - from datetime import datetime import textwrap from typing import Annotated, Literal @@ -10,7 +9,11 @@ from pydantic.json_schema import SkipJsonSchema from server_utils.fastapi_utils.light_router import LightRouter -from opentrons.protocol_engine import LabwareOffset, LabwareOffsetCreate, ModuleModel +from opentrons.protocol_engine import ( + LabwareOffset, + LegacyLabwareOffsetCreate, + ModuleModel, +) from opentrons.types import DeckSlotName from robot_server.labware_offsets.models import LabwareOffsetNotFound @@ -54,7 +57,7 @@ async def post_labware_offset( # noqa: D103 store: Annotated[LabwareOffsetStore, fastapi.Depends(get_labware_offset_store)], new_offset_id: Annotated[str, fastapi.Depends(get_unique_id)], new_offset_created_at: Annotated[datetime, fastapi.Depends(get_current_time)], - request_body: Annotated[RequestModel[LabwareOffsetCreate], fastapi.Body()], + request_body: Annotated[RequestModel[LegacyLabwareOffsetCreate], fastapi.Body()], ) -> PydanticResponse[SimpleBody[LabwareOffset]]: new_offset = LabwareOffset.model_construct( id=new_offset_id, diff --git a/robot-server/robot_server/labware_offsets/store.py b/robot-server/robot_server/labware_offsets/store.py index a605f75da3e..dbeccc728a1 100644 --- a/robot-server/robot_server/labware_offsets/store.py +++ b/robot-server/robot_server/labware_offsets/store.py @@ -5,7 +5,7 @@ from opentrons.protocol_engine.types import ( LabwareOffset, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, LabwareOffsetVector, ModuleModel, ) @@ -154,7 +154,7 @@ def _sql_to_pydantic(row: sqlalchemy.engine.Row) -> LabwareOffset: id=row.offset_id, createdAt=row.created_at, definitionUri=row.definition_uri, - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName(row.location_slot_name), moduleModel=row.location_module_model, definitionUri=row.location_definition_uri, diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py index dfc76945f81..2a27e0c9a67 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py @@ -1,9 +1,11 @@ """Manage current maintenance run data.""" + from datetime import datetime -from typing import List, Optional, Callable +from typing import Optional, Callable, Sequence from opentrons.protocol_engine import ( EngineStatus, + LegacyLabwareOffsetCreate, LabwareOffsetCreate, StateSummary, CommandSlice, @@ -87,7 +89,7 @@ async def create( self, run_id: str, created_at: datetime, - labware_offsets: List[LabwareOffsetCreate], + labware_offsets: Sequence[LabwareOffsetCreate | LegacyLabwareOffsetCreate], deck_configuration: DeckConfigurationType, notify_publishers: Callable[[], None], ) -> MaintenanceRun: diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_models.py b/robot-server/robot_server/maintenance_runs/maintenance_run_models.py index 8bde7ea7aff..25b3b40ae74 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_models.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_models.py @@ -1,4 +1,5 @@ """Request and response models for maintenance run resources.""" + from datetime import datetime from pydantic import BaseModel, Field from typing import List, Optional @@ -11,6 +12,7 @@ LoadedModule, LabwareOffset, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, Liquid, LiquidClassRecordWithId, ) @@ -89,7 +91,7 @@ class MaintenanceRun(ResourceModel): class MaintenanceRunCreate(BaseModel): """Create request data for a new maintenance run.""" - labwareOffsets: List[LabwareOffsetCreate] = Field( + labwareOffsets: List[LegacyLabwareOffsetCreate | LabwareOffsetCreate] = Field( default_factory=list, description="Labware offsets to apply as labware are loaded.", ) diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_orchestrator_store.py b/robot-server/robot_server/maintenance_runs/maintenance_run_orchestrator_store.py index 530cdc87563..31ab9228d74 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_orchestrator_store.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_orchestrator_store.py @@ -1,14 +1,16 @@ """In-memory storage of ProtocolEngine instances.""" + import asyncio import logging from datetime import datetime -from typing import List, Optional, Callable +from typing import Optional, Callable, Sequence from opentrons.protocol_engine.errors.exceptions import EStopActivatedError from opentrons.protocol_engine.types import PostRunHardwareState, DeckConfigurationType from opentrons.protocol_engine import ( DeckType, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, StateSummary, CommandSlice, CommandPointer, @@ -146,7 +148,7 @@ async def create( self, run_id: str, created_at: datetime, - labware_offsets: List[LabwareOffsetCreate], + labware_offsets: Sequence[LegacyLabwareOffsetCreate | LabwareOffsetCreate], notify_publishers: Callable[[], None], deck_configuration: Optional[DeckConfigurationType] = [], ) -> StateSummary: @@ -265,7 +267,9 @@ async def add_command_and_wait_for_interval( command=request, wait_until_complete=wait_until_complete, timeout=timeout ) - def add_labware_offset(self, request: LabwareOffsetCreate) -> LabwareOffset: + def add_labware_offset( + self, request: LegacyLabwareOffsetCreate | LabwareOffsetCreate + ) -> LabwareOffset: """Add a new labware offset to state.""" return self.run_orchestrator.add_labware_offset(request) diff --git a/robot-server/robot_server/maintenance_runs/router/labware_router.py b/robot-server/robot_server/maintenance_runs/router/labware_router.py index 53dce4ae6eb..c64e8b7db97 100644 --- a/robot-server/robot_server/maintenance_runs/router/labware_router.py +++ b/robot-server/robot_server/maintenance_runs/router/labware_router.py @@ -1,4 +1,5 @@ """Router for /maintenance_runs endpoints dealing with labware offsets and definitions.""" + from typing import Annotated import logging @@ -7,7 +8,11 @@ from opentrons_shared_data.labware.labware_definition import LabwareDefinition from server_utils.fastapi_utils.light_router import LightRouter -from opentrons.protocol_engine import LabwareOffsetCreate, LabwareOffset +from opentrons.protocol_engine import ( + LabwareOffsetCreate, + LegacyLabwareOffsetCreate, + LabwareOffset, +) from robot_server.errors.error_responses import ErrorBody from robot_server.service.json_api import RequestModel, SimpleBody, PydanticResponse @@ -40,7 +45,7 @@ }, ) async def add_labware_offset( - request_body: RequestModel[LabwareOffsetCreate], + request_body: RequestModel[LabwareOffsetCreate | LegacyLabwareOffsetCreate], run_orchestrator_store: Annotated[ MaintenanceRunOrchestratorStore, Depends(get_maintenance_run_orchestrator_store) ], diff --git a/robot-server/robot_server/runs/router/labware_router.py b/robot-server/robot_server/runs/router/labware_router.py index f9264da51e6..78c880a2df5 100644 --- a/robot-server/robot_server/runs/router/labware_router.py +++ b/robot-server/robot_server/runs/router/labware_router.py @@ -9,7 +9,11 @@ from server_utils.fastapi_utils.light_router import LightRouter -from opentrons.protocol_engine import LabwareOffsetCreate, LabwareOffset +from opentrons.protocol_engine import ( + LabwareOffsetCreate, + LegacyLabwareOffsetCreate, + LabwareOffset, +) from robot_server.errors.error_responses import ErrorBody from robot_server.service.json_api import ( @@ -47,7 +51,7 @@ }, ) async def add_labware_offset( - request_body: RequestModel[LabwareOffsetCreate], + request_body: RequestModel[LegacyLabwareOffsetCreate | LabwareOffsetCreate], run_orchestrator_store: Annotated[ RunOrchestratorStore, Depends(get_run_orchestrator_store) ], diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index fa937f7cb68..86473667987 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -1,7 +1,7 @@ """Manage current and historical run data.""" from datetime import datetime -from typing import Dict, List, Optional, Callable, Union, Mapping +from typing import Dict, List, Optional, Callable, Union, Mapping, Sequence from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.errors.exceptions import InvalidStoredData, EnumeratedError @@ -10,6 +10,7 @@ from opentrons.protocol_engine import ( EngineStatus, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, StateSummary, CommandSlice, CommandErrorSlice, @@ -181,7 +182,7 @@ async def create( self, run_id: str, created_at: datetime, - labware_offsets: List[LabwareOffsetCreate], + labware_offsets: Sequence[LabwareOffsetCreate | LegacyLabwareOffsetCreate], deck_configuration: DeckConfigurationType, file_provider: FileProvider, run_time_param_values: Optional[PrimitiveRunTimeParamValuesType], diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index 4d5da7560c0..f530707d4c3 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -1,4 +1,5 @@ """Request and response models for run resources.""" + from datetime import datetime from enum import Enum @@ -16,6 +17,7 @@ LoadedLabware, LoadedModule, LabwareOffset, + LegacyLabwareOffsetCreate, LabwareOffsetCreate, Liquid, LiquidClassRecordWithId, @@ -265,7 +267,7 @@ class RunCreate(BaseModel): None, description="Protocol resource ID that this run will be using, if applicable.", ) - labwareOffsets: List[LabwareOffsetCreate] = Field( + labwareOffsets: List[LegacyLabwareOffsetCreate | LabwareOffsetCreate] = Field( default_factory=list, description="Labware offsets to apply as labware are loaded.", ) diff --git a/robot-server/robot_server/runs/run_orchestrator_store.py b/robot-server/robot_server/runs/run_orchestrator_store.py index adb7cac151e..38a6c2e23c6 100644 --- a/robot-server/robot_server/runs/run_orchestrator_store.py +++ b/robot-server/robot_server/runs/run_orchestrator_store.py @@ -2,7 +2,7 @@ import asyncio import logging -from typing import Dict, List, Optional, Callable, Mapping +from typing import Dict, List, Optional, Callable, Mapping, Sequence from opentrons.types import NozzleMapInterface from opentrons.protocol_engine.errors.exceptions import EStopActivatedError @@ -33,6 +33,7 @@ from opentrons.protocol_engine import ( DeckType, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, StateSummary, CommandSlice, CommandErrorSlice, @@ -192,7 +193,7 @@ async def get_default_orchestrator(self) -> RunOrchestrator: async def create( self, run_id: str, - labware_offsets: List[LabwareOffsetCreate], + labware_offsets: Sequence[LabwareOffsetCreate | LegacyLabwareOffsetCreate], initial_error_recovery_policy: error_recovery_policy.ErrorRecoveryPolicy, deck_configuration: DeckConfigurationType, file_provider: FileProvider, @@ -408,7 +409,9 @@ def run_was_started(self) -> bool: """Get whether the run has started.""" return self.run_orchestrator.run_has_started() - def add_labware_offset(self, request: LabwareOffsetCreate) -> LabwareOffset: + def add_labware_offset( + self, request: LabwareOffsetCreate | LegacyLabwareOffsetCreate + ) -> LabwareOffset: """Add a new labware offset to state.""" return self.run_orchestrator.add_labware_offset(request) diff --git a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml index 732726d39e9..038f510d1dc 100644 --- a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml @@ -105,6 +105,9 @@ stages: definitionUri: opentrons/biorad_96_wellplate_200ul_pcr/1 location: slotName: '1' + locationSequence: + - kind: 'onAddressableArea' + addressableAreaName: '1' vector: x: 1.11 y: 2.22 @@ -264,6 +267,9 @@ stages: definitionUri: opentrons/biorad_96_wellplate_200ul_pcr/1 location: slotName: '1' + locationSequence: + - kind: 'onAddressableArea' + addressableAreaName: '1' vector: x: 1.11 y: 2.22 diff --git a/robot-server/tests/labware_offsets/test_store.py b/robot-server/tests/labware_offsets/test_store.py index a23b55aff9e..0b6048da86b 100644 --- a/robot-server/tests/labware_offsets/test_store.py +++ b/robot-server/tests/labware_offsets/test_store.py @@ -7,7 +7,7 @@ from opentrons.protocol_engine import ( LabwareOffset, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, LabwareOffsetVector, ) from opentrons.protocol_engine.types import ModuleModel @@ -35,7 +35,7 @@ def test_filter_fields(subject: LabwareOffsetStore) -> None: id="a", createdAt=datetime.now(timezone.utc), definitionUri="definitionUri a", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_A1, moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1, definitionUri="location.definitionUri a", @@ -46,7 +46,7 @@ def test_filter_fields(subject: LabwareOffsetStore) -> None: id="b", createdAt=datetime.now(timezone.utc), definitionUri="definitionUri b", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_B1, moduleModel=ModuleModel.MAGNETIC_BLOCK_V1, definitionUri="location.definitionUri b", @@ -100,7 +100,7 @@ def test_filter_combinations(subject: LabwareOffsetStore) -> None: id=id, createdAt=datetime.now(timezone.utc), definitionUri=definition_uri, - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_A1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_A1), vector=LabwareOffsetVector(x=1, y=2, z=3), ) for (id, definition_uri) in ids_and_definition_uris @@ -141,7 +141,7 @@ def test_delete(subject: LabwareOffsetStore) -> None: id=id, createdAt=datetime.now(timezone.utc), definitionUri="", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_A1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_A1), vector=LabwareOffsetVector(x=1, y=2, z=3), ) for id in ["id-a", "id-b", "id-c"] diff --git a/robot-server/tests/maintenance_runs/router/test_base_router.py b/robot-server/tests/maintenance_runs/router/test_base_router.py index 29a9c81a3b7..b7b1182fa91 100644 --- a/robot-server/tests/maintenance_runs/router/test_base_router.py +++ b/robot-server/tests/maintenance_runs/router/test_base_router.py @@ -1,10 +1,11 @@ """Tests for base /runs routes.""" + import pytest from datetime import datetime from decoy import Decoy from opentrons.types import DeckSlotName -from opentrons.protocol_engine import LabwareOffsetCreate, types as pe_types +from opentrons.protocol_engine import types as pe_types from robot_server.errors.error_responses import ApiError from robot_server.service.json_api import ( @@ -44,11 +45,11 @@ def mock_notify_publishers() -> None: @pytest.fixture -def labware_offset_create() -> LabwareOffsetCreate: +def labware_offset_create() -> pe_types.LegacyLabwareOffsetCreate: """Get a labware offset create request value object.""" - return pe_types.LabwareOffsetCreate( + return pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace_1/load_name_1/123", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) diff --git a/robot-server/tests/maintenance_runs/router/test_labware_router.py b/robot-server/tests/maintenance_runs/router/test_labware_router.py index 2b03b13c9e8..72b1e95e2f7 100644 --- a/robot-server/tests/maintenance_runs/router/test_labware_router.py +++ b/robot-server/tests/maintenance_runs/router/test_labware_router.py @@ -1,4 +1,5 @@ """Tests for /runs routes dealing with labware offsets and definitions.""" + import pytest from datetime import datetime from decoy import Decoy @@ -55,9 +56,9 @@ async def test_add_labware_offset( run: MaintenanceRun, ) -> None: """It should add the labware offset to the engine, assuming the run is current.""" - labware_offset_request = pe_types.LabwareOffsetCreate( + labware_offset_request = pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace_1/load_name_1/123", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) @@ -65,7 +66,7 @@ async def test_add_labware_offset( id="labware-offset-id", createdAt=datetime(year=2022, month=2, day=2), definitionUri="labware-definition-uri", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=0, y=0, z=0), ) diff --git a/robot-server/tests/maintenance_runs/test_engine_store.py b/robot-server/tests/maintenance_runs/test_engine_store.py index ed9987f5e77..019bb4a913d 100644 --- a/robot-server/tests/maintenance_runs/test_engine_store.py +++ b/robot-server/tests/maintenance_runs/test_engine_store.py @@ -1,4 +1,5 @@ """Tests for the MaintenanceRunOrchestratorStore interface.""" + from datetime import datetime import pytest @@ -95,9 +96,9 @@ async def test_create_engine_with_labware_offsets( subject: MaintenanceRunOrchestratorStore, ) -> None: """It should create an engine for a run with labware offsets.""" - labware_offset = pe_types.LabwareOffsetCreate( + labware_offset = pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace/load_name/version", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) @@ -113,7 +114,12 @@ async def test_create_engine_with_labware_offsets( id=matchers.IsA(str), createdAt=matchers.IsA(datetime), definitionUri="namespace/load_name/version", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + locationSequence=[ + pe_types.OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="5" + ) + ], vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) ] diff --git a/robot-server/tests/maintenance_runs/test_run_data_manager.py b/robot-server/tests/maintenance_runs/test_run_data_manager.py index 634eaab6ce5..768bdbe1029 100644 --- a/robot-server/tests/maintenance_runs/test_run_data_manager.py +++ b/robot-server/tests/maintenance_runs/test_run_data_manager.py @@ -160,9 +160,9 @@ async def test_create_with_options( run_id = "hello world" created_at = datetime(year=2021, month=1, day=1) - labware_offset = pe_types.LabwareOffsetCreate( + labware_offset = pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace/load_name/version", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index 0350bb4d0b0..bf475b5a0c0 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -9,7 +9,6 @@ from opentrons.types import DeckSlotName, Point, NozzleConfigurationType from opentrons.protocol_engine import ( - LabwareOffsetCreate, types as pe_types, errors as pe_errors, CommandErrorSlice, @@ -101,11 +100,11 @@ def mock_data_files_directory(decoy: Decoy) -> Path: @pytest.fixture -def labware_offset_create() -> LabwareOffsetCreate: +def labware_offset_create() -> pe_types.LegacyLabwareOffsetCreate: """Get a labware offset create request value object.""" - return pe_types.LabwareOffsetCreate( + return pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace_1/load_name_1/123", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) @@ -114,7 +113,7 @@ async def test_create_run( decoy: Decoy, mock_run_data_manager: RunDataManager, mock_run_auto_deleter: RunAutoDeleter, - labware_offset_create: pe_types.LabwareOffsetCreate, + labware_offset_create: pe_types.LegacyLabwareOffsetCreate, mock_deck_configuration_store: DeckConfigurationStore, mock_file_provider_wrapper: FileProviderWrapper, mock_protocol_store: ProtocolStore, diff --git a/robot-server/tests/runs/router/test_labware_router.py b/robot-server/tests/runs/router/test_labware_router.py index bf55021a6ad..2b55b4097f6 100644 --- a/robot-server/tests/runs/router/test_labware_router.py +++ b/robot-server/tests/runs/router/test_labware_router.py @@ -59,9 +59,9 @@ async def test_add_labware_offset( run: Run, ) -> None: """It should add the labware offset to the engine, assuming the run is current.""" - labware_offset_request = pe_types.LabwareOffsetCreate( + labware_offset_request = pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace_1/load_name_1/123", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) @@ -69,7 +69,7 @@ async def test_add_labware_offset( id="labware-offset-id", createdAt=datetime(year=2022, month=2, day=2), definitionUri="labware-definition-uri", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=0, y=0, z=0), ) @@ -95,9 +95,9 @@ async def test_add_labware_offset_not_current( """It should 409 if the run is not current.""" not_current_run = run.model_copy(update={"current": False}) - labware_offset_request = pe_types.LabwareOffsetCreate( + labware_offset_request = pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace_1/load_name_1/123", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) diff --git a/robot-server/tests/runs/test_run_orchestrator_store.py b/robot-server/tests/runs/test_run_orchestrator_store.py index b0f8354e494..a8564cece99 100644 --- a/robot-server/tests/runs/test_run_orchestrator_store.py +++ b/robot-server/tests/runs/test_run_orchestrator_store.py @@ -1,4 +1,5 @@ """Tests for the EngineStore interface.""" + from datetime import datetime import pytest from decoy import Decoy, matchers @@ -103,9 +104,9 @@ async def test_create_engine_with_labware_offsets( subject: RunOrchestratorStore, ) -> None: """It should create an engine for a run with labware offsets.""" - labware_offset = pe_types.LabwareOffsetCreate( + labware_offset = pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace/load_name/version", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) @@ -124,7 +125,12 @@ async def test_create_engine_with_labware_offsets( id=matchers.IsA(str), createdAt=matchers.IsA(datetime), definitionUri="namespace/load_name/version", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + locationSequence=[ + pe_types.OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="5" + ) + ], vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) ] diff --git a/shared-data/python/opentrons_shared_data/deck/__init__.py b/shared-data/python/opentrons_shared_data/deck/__init__.py index 38607263418..f2136d6e97a 100644 --- a/shared-data/python/opentrons_shared_data/deck/__init__.py +++ b/shared-data/python/opentrons_shared_data/deck/__init__.py @@ -55,6 +55,11 @@ def load(name: str, version: "DeckSchemaVersion3") -> "DeckDefinitionV3": ... +@overload +def load(name: str) -> "DeckDefinitionV5": + ... + + def load(name: str, version: int = DEFAULT_DECK_DEFINITION_VERSION) -> "DeckDefinition": return json.loads( # type: ignore[no-any-return] load_shared_data(f"deck/definitions/{version}/{name}.json")