From ecccea0b6eddfc44945f8c495732847db6190884 Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Fri, 15 Mar 2024 17:04:52 -0400 Subject: [PATCH] schema and deck def updates with addressable area logic added --- .../hardware_control/modules/types.py | 17 ++ .../protocol_api/core/engine/protocol.py | 32 ++- .../protocol_api/core/legacy/deck.py | 3 + .../state/addressable_areas.py | 3 + api/src/opentrons/protocol_engine/types.py | 4 + .../deck_configuration/validation.py | 9 +- .../deck/definitions/5/ot3_standard.json | 235 ++++++++++++++++-- shared-data/deck/schemas/5.json | 9 +- .../opentrons_shared_data/deck/dev_types.py | 2 +- 9 files changed, 280 insertions(+), 34 deletions(-) diff --git a/api/src/opentrons/hardware_control/modules/types.py b/api/src/opentrons/hardware_control/modules/types.py index 653b0b08e4f..c035795d5bc 100644 --- a/api/src/opentrons/hardware_control/modules/types.py +++ b/api/src/opentrons/hardware_control/modules/types.py @@ -9,6 +9,7 @@ Tuple, Awaitable, Union, + Set, cast, TYPE_CHECKING, ) @@ -64,6 +65,22 @@ def from_model(cls, model: ModuleModel) -> ModuleType: if isinstance(model, MagneticBlockModel): return cls.MAGNETIC_BLOCK + @classmethod + def to_module_fixture_id(cls, module_type: ModuleType) -> str: + if module_type == ModuleType.THERMOCYCLER: + # Thermocyclers are "loaded" in B1 only + return "thermocyclerModuleFront" + if module_type == ModuleType.TEMPERATURE: + return "temperatureModule" + if module_type == ModuleType.HEATER_SHAKER: + return "heaterShakerModule" + if module_type == ModuleType.MAGNETIC_BLOCK: + return "magneticBlockModule" + else: + raise ValueError( + f"Module Type {module_type} does not have a related fixture ID." + ) + class MagneticModuleModel(str, Enum): MAGNETIC_V1: str = "magneticModuleV1" diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index d8684ba46d2..74876c26d4a 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -3,7 +3,11 @@ from typing import Dict, Optional, Type, Union, List, Tuple, TYPE_CHECKING from opentrons.protocol_engine.commands import LoadModuleResult -from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, SlotDefV3 +from opentrons_shared_data.deck.dev_types import ( + DeckDefinitionV5, + SlotDefV3, + CutoutFixture, +) from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.labware.dev_types import LabwareDefinition as LabwareDefDict from opentrons_shared_data.pipette.dev_types import PipetteNameType @@ -625,10 +629,28 @@ def get_staging_slot_definitions(self) -> Dict[str, SlotDefV3]: def _ensure_module_location( self, slot: DeckSlotName, module_type: ModuleType ) -> None: - slot_def = self.get_slot_definition(slot) - compatible_modules = slot_def["compatibleModuleTypes"] - if module_type.value not in compatible_modules: - raise ValueError(f"A {module_type.value} cannot be loaded into slot {slot}") + if self._engine_client.state.config.robot_type == "OT-2 Standard": + slot_def = self.get_slot_definition(slot) + compatible_modules = slot_def["compatibleModuleTypes"] + if module_type.value not in compatible_modules: + raise ValueError( + f"A {module_type.value} cannot be loaded into slot {slot}" + ) + else: + cutout_fixture_id = ModuleType.to_module_fixture_id(module_type) + potential_fixtures_in_slot = self._engine_client.state.addressable_areas._state.potential_cutout_fixtures_by_cutout_id[ + self._engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name( + slot + ) + ] + module_fixture_found = False + for fixture in potential_fixtures_in_slot: + if fixture.cutout_fixture_id == cutout_fixture_id: + module_fixture_found = True + if not module_fixture_found: + raise ValueError( + f"A {module_type.value} cannot be loaded into slot {slot}" + ) def get_slot_item( self, slot_name: Union[DeckSlotName, StagingSlotName] diff --git a/api/src/opentrons/protocol_api/core/legacy/deck.py b/api/src/opentrons/protocol_api/core/legacy/deck.py index 9a9092af5ae..2cadb20abb6 100644 --- a/api/src/opentrons/protocol_api/core/legacy/deck.py +++ b/api/src/opentrons/protocol_api/core/legacy/deck.py @@ -278,8 +278,11 @@ def resolve_module_location( if isinstance(location, str) or isinstance(location, int): slot_def = self.get_slot_definition(str(location)) compatible_modules = slot_def["compatibleModuleTypes"] + cutout_fixture_id = ModuleType.to_module_fixture_id(module_type) if module_type.value in compatible_modules: return location + elif cutout_fixture_id == slot_def["id"]: + return location else: raise ValueError( f"A {dn_from_type[module_type]} cannot be loaded" diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index bb87af08c37..a7bc0d7e00d 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -499,6 +499,9 @@ def get_addressable_area_center(self, addressable_area_name: str) -> Point: z=position.z, ) + def get_cutout_id_by_deck_slot_name(self, slot_name: DeckSlotName) -> str: + return DECK_SLOT_TO_CUTOUT_MAP[slot_name] + def get_fixture_by_deck_slot_name( self, slot_name: DeckSlotName ) -> Optional[CutoutFixture]: diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 4d352858fff..0eeb8dfac35 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -706,6 +706,10 @@ class AreaType(Enum): MOVABLE_TRASH = "movableTrash" FIXED_TRASH = "fixedTrash" WASTE_CHUTE = "wasteChute" + THERMOCYCLER = "thermocycler" + HEATER_SHAKER = "heaterShaker" + TEMPERATURE = "temperatureModule" + @dataclass(frozen=True) diff --git a/robot-server/robot_server/deck_configuration/validation.py b/robot-server/robot_server/deck_configuration/validation.py index 0b30b8b609f..5edfbb57416 100644 --- a/robot-server/robot_server/deck_configuration/validation.py +++ b/robot-server/robot_server/deck_configuration/validation.py @@ -118,12 +118,9 @@ def get_configuration_errors( allowed_cutout_ids=allowed_cutout_ids, ) ) - # replace this with a serial number check set instead - # if serial number in def is null, we expect nothing from the put statement, raise if there is something - # if it is not null, we expect something from the put statement! if theres nothing then raise an error if found_cutout_fixture[ - "opentronsModuleSerialNumber" - ] is None and isinstance(placement.opentrons_modules_serial_number, str): + "expectOpentronsModuleSerialNumber" + ] == False and isinstance(placement.opentrons_modules_serial_number, str): errors.add( UnexpectedSerialNumberError( cutout_id=placement.cutout_id, @@ -132,7 +129,7 @@ def get_configuration_errors( ) ) elif ( - isinstance(found_cutout_fixture["opentronsModuleSerialNumber"], str) + found_cutout_fixture["expectOpentronsModuleSerialNumber"] == True and placement.opentrons_modules_serial_number is None ): errors.add( diff --git a/shared-data/deck/definitions/5/ot3_standard.json b/shared-data/deck/definitions/5/ot3_standard.json index 257b1c2f2fc..a478ed808bb 100644 --- a/shared-data/deck/definitions/5/ot3_standard.json +++ b/shared-data/deck/definitions/5/ot3_standard.json @@ -362,7 +362,212 @@ }, "displayName": "Waste Chute", "ableToDropLabware": true - } + }, + { + "id": "thermocyclerModule", + "areaType": "thermocycler", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Thermocycler Module Slot", + "compatibleModuleTypes": [] + }, + { + "id": "heaterShakerD1", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in D1", + "ableToDropTips": true + }, + { + "id": "heaterShakerC1", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in C1", + "ableToDropTips": true + }, + { + "id": "heaterShakerB1", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in B1", + "ableToDropTips": true + }, + { + "id": "heaterShakerA1", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in A1", + "ableToDropTips": true + }, + { + "id": "heaterShakerD3", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in D3", + "ableToDropTips": true + }, + { + "id": "heaterShakerC3", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in C3", + "ableToDropTips": true + }, + { + "id": "heaterShakerB3", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in B3", + "ableToDropTips": true + }, + { + "id": "heaterShakerA3", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in A3", + "ableToDropTips": true + }, + { + "id": "temperatureModuleD1", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in D1", + "ableToDropTips": true + }, + { + "id": "temperatureModuleC1", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in C1", + "ableToDropTips": true + }, + { + "id": "temperatureModuleB1", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in B1", + "ableToDropTips": true + }, + { + "id": "temperatureModuleA1", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in A1", + "ableToDropTips": true + }, + { + "id": "temperatureModuleD3", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in D3", + "ableToDropTips": true + }, + { + "id": "temperatureModuleC3", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in C3", + "ableToDropTips": true + }, + { + "id": "temperatureModuleB3", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in B3", + "ableToDropTips": true + }, + { + "id": "temperatureModuleA3", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in A3", + "ableToDropTips": true + }, ], "cutouts": [ { @@ -439,7 +644,7 @@ "cutoutFixtures": [ { "id": "singleLeftSlot", - "opentronsModuleSerialNumber": null, + "expectOpentronsModuleSerialNumber": false, "mayMountTo": ["cutoutD1", "cutoutC1", "cutoutB1", "cutoutA1"], "displayName": "Standard Slot Left", "providesAddressableAreas": { @@ -452,7 +657,7 @@ }, { "id": "singleCenterSlot", - "opentronsModuleSerialNumber": null, + "expectOpentronsModuleSerialNumber": false, "mayMountTo": ["cutoutD2", "cutoutC2", "cutoutB2", "cutoutA2"], "displayName": "Standard Slot Center", "providesAddressableAreas": { @@ -465,7 +670,7 @@ }, { "id": "singleRightSlot", - "opentronsModuleSerialNumber": null, + "expectOpentronsModuleSerialNumber": false, "mayMountTo": ["cutoutD3", "cutoutC3", "cutoutB3", "cutoutA3"], "displayName": "Standard Slot Right", "providesAddressableAreas": { @@ -478,7 +683,7 @@ }, { "id": "stagingAreaRightSlot", - "opentronsModuleSerialNumber": null, + "expectOpentronsModuleSerialNumber": false, "mayMountTo": ["cutoutD3", "cutoutC3", "cutoutB3", "cutoutA3"], "displayName": "Staging Area Slot", "providesAddressableAreas": { @@ -491,7 +696,7 @@ }, { "id": "trashBinAdapter", - "opentronsModuleSerialNumber": null, + "expectOpentronsModuleSerialNumber": false, "mayMountTo": [ "cutoutD1", "cutoutC1", @@ -517,7 +722,7 @@ }, { "id": "wasteChuteRightAdapterCovered", - "opentronsModuleSerialNumber": null, + "expectOpentronsModuleSerialNumber": false, "mayMountTo": ["cutoutD3"], "displayName": "Waste Chute Adapter for 1 or 8 Channel Pipettes", "providesAddressableAreas": { @@ -527,7 +732,7 @@ }, { "id": "wasteChuteRightAdapterNoCover", - "opentronsModuleSerialNumber": null, + "expectOpentronsModuleSerialNumber": false, "mayMountTo": ["cutoutD3"], "displayName": "Waste Chute Adapter for 96 Channel Pipette or Gripper", "providesAddressableAreas": { @@ -542,7 +747,7 @@ }, { "id": "stagingAreaSlotWithWasteChuteRightAdapterCovered", - "opentronsModuleSerialNumber": null, + "expectOpentronsModuleSerialNumber": false, "mayMountTo": ["cutoutD3"], "displayName": "Staging Slot With Waste Chute Adapter for 96 Channel Pipette or Gripper", "providesAddressableAreas": { @@ -552,7 +757,7 @@ }, { "id": "stagingAreaSlotWithWasteChuteRightAdapterNoCover", - "opentronsModuleSerialNumber": null, + "expectOpentronsModuleSerialNumber": false, "mayMountTo": ["cutoutD3"], "displayName": "Staging Slot With Waste Chute Adapter and Staging Area Slot", "providesAddressableAreas": { @@ -568,7 +773,7 @@ }, { "id": "thermocyclerModuleRear", - "opentronsModuleSerialNumber": "", + "expectOpentronsModuleSerialNumber": true, "mayMountTo": ["cutoutA1"], "displayName": "Rear Slot portion of the Thermocycler Moduler", "providesAddressableAreas": {}, @@ -576,7 +781,7 @@ }, { "id": "thermocyclerModuleFront", - "opentronsModuleSerialNumber": "", + "expectOpentronsModuleSerialNumber": true, "mayMountTo": ["cutoutB1"], "displayName": "Front Slot portion of the Thermocycler Moduler", "providesAddressableAreas": { @@ -586,7 +791,7 @@ }, { "id": "heaterShakerModule", - "opentronsModuleSerialNumber": "", + "expectOpentronsModuleSerialNumber": true, "mayMountTo": [ "cutoutD1", "cutoutC1", @@ -612,7 +817,7 @@ }, { "id": "temperatureModule", - "opentronsModuleSerialNumber": "", + "expectOpentronsModuleSerialNumber": true, "mayMountTo": [ "cutoutD1", "cutoutC1", @@ -638,7 +843,7 @@ }, { "id": "magneticBlockModule", - "opentronsModuleSerialNumber": "", + "expectOpentronsModuleSerialNumber": false, "mayMountTo": [ "cutoutD1", "cutoutC1", diff --git a/shared-data/deck/schemas/5.json b/shared-data/deck/schemas/5.json index 8581f34ab41..a11ca88ba61 100644 --- a/shared-data/deck/schemas/5.json +++ b/shared-data/deck/schemas/5.json @@ -256,8 +256,7 @@ "type": "object", "required": [ "id", - "opentronsModuleSerialNumber", - "expectRobotConnection", + "expectOpentronsModuleSerialNumber", "mayMountTo", "displayName", "providesAddressableAreas", @@ -268,12 +267,8 @@ "description": "Unique identifier for the cutout fixture.", "type": "string" }, - "opentronsModuleSerialNumber": { + "expectOpentronsModuleSerialNumber": { "description": "Serial number identified for a connected Opentrons Module.", - "type": "string" - }, - "expectRobotConnection": { - "description": "Indicates whether or not this fixture is expected to provide a connection to robotics hardware.", "type": "boolean" }, "mayMountTo": { diff --git a/shared-data/python/opentrons_shared_data/deck/dev_types.py b/shared-data/python/opentrons_shared_data/deck/dev_types.py index e21e4f00320..f6b40cdc159 100644 --- a/shared-data/python/opentrons_shared_data/deck/dev_types.py +++ b/shared-data/python/opentrons_shared_data/deck/dev_types.py @@ -112,7 +112,7 @@ class Cutout(TypedDict): class CutoutFixture(TypedDict): id: str - opentronsModuleSerialNumber: Optional[str] + expectOpentronsModuleSerialNumber: bool mayMountTo: List[str] displayName: str providesAddressableAreas: Dict[str, List[str]]