diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index b567eaa2f5c..c56edf4de5f 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -560,12 +560,27 @@ def get_module_overlap_offsets( """Get the labware's overlap with requested module model.""" definition = self.get_definition(labware_id) stacking_overlap = definition.stackingOffsetWithModule.get( - str(module_model.value), OverlapOffset(x=0, y=0, z=0) + str(module_model.value) ) + if not stacking_overlap: + if self._is_thermocycler_on_ot2(module_model): + return OverlapOffset(x=0, y=0, z=10.7) + else: + return OverlapOffset(x=0, y=0, z=0) + return OverlapOffset( x=stacking_overlap.x, y=stacking_overlap.y, z=stacking_overlap.z ) + def _is_thermocycler_on_ot2(self, module_model: ModuleModel) -> bool: + """Whether the given module is a thermocycler with the current deck being an OT2 deck.""" + robot_model = self.get_deck_definition()["robot"]["model"] + return ( + module_model + in [ModuleModel.THERMOCYCLER_MODULE_V1, ModuleModel.THERMOCYCLER_MODULE_V2] + and robot_model == "OT-2 Standard" + ) + def get_default_magnet_height(self, module_id: str, offset: float) -> float: """Return a labware's default Magnetic Module engage height with added offset, if supplied. diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view.py b/api/tests/opentrons/protocol_engine/state/test_labware_view.py index 7d277d93b5a..6de6ba0d191 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view.py @@ -1,9 +1,10 @@ """Labware state store tests.""" import pytest from datetime import datetime -from typing import Dict, Optional, cast, ContextManager, Any, Union +from typing import Dict, Optional, cast, ContextManager, Any, Union, NamedTuple, List from contextlib import nullcontext as does_not_raise +from opentrons_shared_data.deck import load as load_deck from opentrons_shared_data.deck.dev_types import DeckDefinitionV3 from opentrons_shared_data.pipette.dev_types import LabwareUri from opentrons_shared_data.labware.labware_definition import ( @@ -13,6 +14,11 @@ GripperOffsets, OffsetVector, ) + +from opentrons.protocols.api_support.deck_type import ( + STANDARD_OT2_DECK, + STANDARD_OT3_DECK, +) from opentrons.protocols.models import LabwareDefinition from opentrons.types import DeckSlotName, Point, MountType @@ -39,7 +45,6 @@ LabwareLoadParams, ) - plate = LoadedLabware( id="plate-id", loadName="plate-load-name", @@ -686,26 +691,91 @@ def test_get_labware_overlap_offsets() -> None: assert result == OverlapOffset(x=1, y=2, z=3) -def test_get_module_overlap_offsets() -> None: +class ModuleOverlapSpec(NamedTuple): + """Spec data to test LabwareView.get_module_overlap_offsets.""" + + spec_deck_definition: DeckDefinitionV3 + module_model: ModuleModel + stacking_offset_with_module: Dict[str, SharedDataOverlapOffset] + expected_offset: OverlapOffset + + +module_overlap_specs: List[ModuleOverlapSpec] = [ + ModuleOverlapSpec( + # Labware on temp module on OT2, with stacking overlap for temp module + spec_deck_definition=load_deck(STANDARD_OT2_DECK, 3), + module_model=ModuleModel.TEMPERATURE_MODULE_V2, + stacking_offset_with_module={ + str(ModuleModel.TEMPERATURE_MODULE_V2.value): SharedDataOverlapOffset( + x=1, y=2, z=3 + ), + }, + expected_offset=OverlapOffset(x=1, y=2, z=3), + ), + ModuleOverlapSpec( + # Labware on TC Gen1 on OT2, with stacking overlap for TC Gen1 + spec_deck_definition=load_deck(STANDARD_OT2_DECK, 3), + module_model=ModuleModel.THERMOCYCLER_MODULE_V1, + stacking_offset_with_module={ + str(ModuleModel.THERMOCYCLER_MODULE_V1.value): SharedDataOverlapOffset( + x=11, y=22, z=33 + ), + }, + expected_offset=OverlapOffset(x=11, y=22, z=33), + ), + ModuleOverlapSpec( + # Labware on TC Gen2 on OT2, with no stacking overlap + spec_deck_definition=load_deck(STANDARD_OT2_DECK, 3), + module_model=ModuleModel.THERMOCYCLER_MODULE_V2, + stacking_offset_with_module={}, + expected_offset=OverlapOffset(x=0, y=0, z=10.7), + ), + ModuleOverlapSpec( + # Labware on TC Gen2 on Flex, with no stacking overlap + spec_deck_definition=load_deck(STANDARD_OT3_DECK, 3), + module_model=ModuleModel.THERMOCYCLER_MODULE_V2, + stacking_offset_with_module={}, + expected_offset=OverlapOffset(x=0, y=0, z=0), + ), + ModuleOverlapSpec( + # Labware on TC Gen2 on Flex, with stacking overlap for TC Gen2 + spec_deck_definition=load_deck(STANDARD_OT3_DECK, 3), + module_model=ModuleModel.THERMOCYCLER_MODULE_V2, + stacking_offset_with_module={ + str(ModuleModel.THERMOCYCLER_MODULE_V2.value): SharedDataOverlapOffset( + x=111, y=222, z=333 + ), + }, + expected_offset=OverlapOffset(x=111, y=222, z=333), + ), +] + + +@pytest.mark.parametrize( + argnames=ModuleOverlapSpec._fields, + argvalues=module_overlap_specs, +) +def test_get_module_overlap_offsets( + spec_deck_definition: DeckDefinitionV3, + module_model: ModuleModel, + stacking_offset_with_module: Dict[str, SharedDataOverlapOffset], + expected_offset: OverlapOffset, +) -> None: """It should get the labware overlap offsets.""" subject = get_labware_view( + deck_definition=spec_deck_definition, labware_by_id={"plate-id": plate}, definitions_by_uri={ "some-plate-uri": LabwareDefinition.construct( # type: ignore[call-arg] - stackingOffsetWithModule={ - str( - ModuleModel.TEMPERATURE_MODULE_V2.value - ): SharedDataOverlapOffset(x=1, y=2, z=3) - } + stackingOffsetWithModule=stacking_offset_with_module ) }, ) - result = subject.get_module_overlap_offsets( - labware_id="plate-id", module_model=ModuleModel.TEMPERATURE_MODULE_V2 + labware_id="plate-id", module_model=module_model ) - assert result == OverlapOffset(x=1, y=2, z=3) + assert result == expected_offset def test_get_default_magnet_height( diff --git a/app/src/App/hooks.ts b/app/src/App/hooks.ts index bc574975776..77e52119ca2 100644 --- a/app/src/App/hooks.ts +++ b/app/src/App/hooks.ts @@ -97,7 +97,7 @@ export function useProtocolReceiptToast(): void { { closeButton: true, disableTimeout: true, - displayType: 'odd' + displayType: 'odd', } ) }) diff --git a/protocol-designer/fixtures/protocol/7/doItAllV7.json b/protocol-designer/fixtures/protocol/7/doItAllV7.json index a2c759c5437..0fdbfd3b430 100644 --- a/protocol-designer/fixtures/protocol/7/doItAllV7.json +++ b/protocol-designer/fixtures/protocol/7/doItAllV7.json @@ -2415,6 +2415,13 @@ "stackingOffsetWithLabware": { "opentrons_96_pcr_adapter": { "x": 0, "y": 0, "z": 10.2 }, "opentrons_96_well_aluminum_block": { "x": 0, "y": 0, "z": 12.66 } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } } }, "opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1": { diff --git a/shared-data/labware/definitions/2/biorad_96_wellplate_200ul_pcr/2.json b/shared-data/labware/definitions/2/biorad_96_wellplate_200ul_pcr/2.json index a33a8a14d6c..b15f38bfc94 100644 --- a/shared-data/labware/definitions/2/biorad_96_wellplate_200ul_pcr/2.json +++ b/shared-data/labware/definitions/2/biorad_96_wellplate_200ul_pcr/2.json @@ -1025,5 +1025,12 @@ "y": 0, "z": 15.41 } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.75 + } } } diff --git a/shared-data/labware/definitions/2/nest_96_wellplate_100ul_pcr_full_skirt/2.json b/shared-data/labware/definitions/2/nest_96_wellplate_100ul_pcr_full_skirt/2.json index cc66043024a..5aba73ae4a4 100644 --- a/shared-data/labware/definitions/2/nest_96_wellplate_100ul_pcr_full_skirt/2.json +++ b/shared-data/labware/definitions/2/nest_96_wellplate_100ul_pcr_full_skirt/2.json @@ -1028,5 +1028,12 @@ "y": 0, "z": 12.66 } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } } }