From b2783caaa102d227c792c86be8f7966cfe5db52b Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Mon, 1 Apr 2024 19:03:38 -0400 Subject: [PATCH] addressable area offsets and engine consolidation of references to location --- .../protocol_api/core/engine/protocol.py | 16 +- api/src/opentrons/protocol_api/validation.py | 89 +------ .../protocol_engine/clients/sync_client.py | 2 +- .../protocol_engine/commands/load_module.py | 16 +- .../protocol_engine/execution/equipment.py | 2 +- .../protocol_engine/slot_standardization.py | 18 +- .../state/addressable_areas.py | 21 ++ .../protocol_engine/state/geometry.py | 5 +- .../protocol_engine/state/modules.py | 231 +++++++++++++----- .../opentrons/protocol_engine/state/state.py | 3 - .../deck/definitions/5/ot3_standard.json | 58 ++--- 11 files changed, 241 insertions(+), 220 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index aefe0490e5d..68b86cbfe34 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -412,21 +412,11 @@ def load_module( normalized_deck_slot = deck_slot.to_equivalent_for_robot_type(robot_type) self._ensure_module_location(normalized_deck_slot, module_type) - addressable_area = validation.ensure_and_convert_module_fixture_location( - deck_slot, self._api_version, robot_type, model + result = self._engine_client.load_module( + model=EngineModuleModel(model), + location=DeckSlotLocation(slotName=normalized_deck_slot), ) - if robot_type == "OT-3 Standard" and isinstance(addressable_area, str): - result = self._engine_client.load_module( - model=EngineModuleModel(model), - location=AddressableAreaLocation(addressableAreaName=addressable_area), - ) - else: - result = self._engine_client.load_module( - model=EngineModuleModel(model), - location=DeckSlotLocation(slotName=normalized_deck_slot), - ) - module_core = self._get_module_core(load_module_result=result, model=model) # FIXME(mm, 2023-02-21): diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index d0384937b02..2d8092bf20e 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -21,6 +21,7 @@ from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import APIVersionError from opentrons.protocols.models import LabwareDefinition +from opentrons.protocol_engine.types import DeckType from opentrons.types import Mount, DeckSlotName, StagingSlotName, Location from opentrons.hardware_control.modules.types import ( ModuleModel, @@ -336,94 +337,6 @@ def ensure_and_convert_trash_bin_location( return map_trash_bin_addressable_area[slot_name_ot3] -def ensure_and_convert_module_fixture_location( - deck_slot: DeckSlotName, - api_version: APIVersion, - robot_type: RobotType, - model: ModuleModel, -) -> Optional[str]: - """Ensure module fixture load location is valid. - - Also, convert the deck slot to a valid module fixture addressable area. - """ - - if robot_type == "OT-2 Standard": - # OT-2 Utilizes the existing compatibleModulelTypes list of traditional addressable areas - return None - - if isinstance(model, MagneticBlockModel): - valid_slots = [ - slot - for slot in [ - "A1", - "B1", - "C1", - "D1", - "A2", - "B2", - "C2", - "D2", - "A3", - "B3", - "C3", - "D3", - ] - ] - addressable_areas = [ - "magneticBlockV1A1", - "magneticBlockV1B1", - "magneticBlockV1C1", - "magneticBlockV1D1", - "magneticBlockV1A2", - "magneticBlockV1B2", - "magneticBlockV1C2", - "magneticBlockV1D2", - "magneticBlockV1A3", - "magneticBlockV1B3", - "magneticBlockV1C3", - "magneticBlockV1D3", - ] - - elif isinstance(model, HeaterShakerModuleModel): - valid_slots = [ - slot for slot in ["A1", "B1", "C1", "D1", "A3", "B3", "C3", "D3"] - ] - addressable_areas = [ - "heaterShakerV1A1", - "heaterShakerV1B1", - "heaterShakerV1C1", - "heaterShakerV1D1", - "heaterShakerV1A3", - "heaterShakerV1B3", - "heaterShakerV1C3", - "heaterShakerV1D3", - ] - elif isinstance(model, TemperatureModuleModel): - valid_slots = [ - slot for slot in ["A1", "B1", "C1", "D1", "A3", "B3", "C3", "D3"] - ] - addressable_areas = [ - "temperatureModuleV2A1", - "temperatureModuleV2B1", - "temperatureModuleV2C1", - "temperatureModuleV2D1", - "temperatureModuleV2A3", - "temperatureModuleV2B3", - "temperatureModuleV2C3", - "temperatureModuleV2D3", - ] - elif isinstance(model, ThermocyclerModuleModel): - return "thermocyclerModuleV2" - else: - return None - - map_addressable_area = { - slot: addressable_area - for slot, addressable_area in zip(valid_slots, addressable_areas) - } - return map_addressable_area[deck_slot.value] - - def ensure_hold_time_seconds( seconds: Optional[float], minutes: Optional[float] ) -> float: diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py index 0c72a47f5b5..d967c5c1bbe 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -265,7 +265,7 @@ def move_to_coordinates( def load_module( self, model: ModuleModel, - location: Union[DeckSlotLocation, AddressableAreaLocation], + location: DeckSlotLocation, ) -> commands.LoadModuleResult: """Execute a LoadModule command and return the result.""" request = commands.LoadModuleCreate( diff --git a/api/src/opentrons/protocol_engine/commands/load_module.py b/api/src/opentrons/protocol_engine/commands/load_module.py index 46965b5cf69..dcaa396a245 100644 --- a/api/src/opentrons/protocol_engine/commands/load_module.py +++ b/api/src/opentrons/protocol_engine/commands/load_module.py @@ -1,6 +1,6 @@ """Implementation, request models, and response models for the load module command.""" from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Type, Union +from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal from pydantic import BaseModel, Field @@ -9,7 +9,6 @@ DeckSlotLocation, ModuleModel, ModuleDefinition, - AddressableAreaLocation, ) if TYPE_CHECKING: @@ -42,7 +41,7 @@ class LoadModuleParams(BaseModel): # single deck slot precludes loading a Thermocycler in its special "shifted slightly # to the left" position. This is okay for now because neither the Python Protocol # API nor Protocol Designer attempt to support it, either. - location: Union[DeckSlotLocation, AddressableAreaLocation] = Field( + location: DeckSlotLocation = Field( ..., description=( "The location into which this module should be loaded." @@ -109,13 +108,18 @@ def __init__( async def execute(self, params: LoadModuleParams) -> LoadModuleResult: """Check that the requested module is attached and assign its identifier.""" - if isinstance(params.location, DeckSlotLocation): + if self._state_view.config.robot_type == "OT-2 Standard": self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.location.slotName.id ) - elif isinstance(params.location, AddressableAreaLocation): + else: + addressable_area = self._state_view.geometry._modules.ensure_and_convert_module_fixture_location( + deck_slot=params.location.slotName, + deck_type=self._state_view.config.deck_type, + model=params.model, + ) self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( - params.location.addressableAreaName + addressable_area ) verified_location = self._state_view.geometry.ensure_location_not_occupied( diff --git a/api/src/opentrons/protocol_engine/execution/equipment.py b/api/src/opentrons/protocol_engine/execution/equipment.py index 3d15954ac52..e29cde40bfe 100644 --- a/api/src/opentrons/protocol_engine/execution/equipment.py +++ b/api/src/opentrons/protocol_engine/execution/equipment.py @@ -284,7 +284,7 @@ async def load_magnetic_block( async def load_module( self, model: ModuleModel, - location: Union[DeckSlotLocation, AddressableAreaLocation], + location: DeckSlotLocation, module_id: Optional[str], ) -> LoadedModuleData: """Ensure the required module is attached. diff --git a/api/src/opentrons/protocol_engine/slot_standardization.py b/api/src/opentrons/protocol_engine/slot_standardization.py index c07de208d62..c4e733b3ca6 100644 --- a/api/src/opentrons/protocol_engine/slot_standardization.py +++ b/api/src/opentrons/protocol_engine/slot_standardization.py @@ -83,17 +83,13 @@ def _standardize_load_labware( def _standardize_load_module( original: commands.LoadModuleCreate, robot_type: RobotType ) -> commands.LoadModuleCreate: - if isinstance(original.params.location, DeckSlotLocation): - params = original.params.copy( - update={ - "location": _standardize_deck_slot_location( - original.params.location, robot_type - ) - } - ) - else: - params = original.params - + params = original.params.copy( + update={ + "location": _standardize_deck_slot_location( + original.params.location, robot_type + ) + } + ) return original.copy(update={"params": params}) diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index f8c42bfece5..034198aa142 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -458,6 +458,27 @@ def get_addressable_area_position( position = addressable_area.position return Point(x=position.x, y=position.y, z=position.z) + def get_addressable_area_offsets_from_cutout( + self, + addressable_area_name: str, + ) -> Point: + """Get the offset form cutout fixture of an addressable area.""" + + for addressable_area in self.state.deck_definition["locations"][ + "addressableAreas" + ]: + if addressable_area["id"] == addressable_area_name: + area_offset = addressable_area["offsetFromCutoutFixture"] + position = Point( + x=area_offset[0], + y=area_offset[1], + z=area_offset[2], + ) + return Point(x=position.x, y=position.y, z=position.z) + raise ValueError( + f"No matching addressable area named {addressable_area_name} identified." + ) + def get_addressable_area_bounding_box( self, addressable_area_name: str, diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 1822881eea2..4a37bf798c1 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -166,6 +166,7 @@ def get_highest_z_in_slot( except LabwareNotLoadedOnModuleError: return self._modules.get_module_highest_z( module_id=module_id, + addressable_areas=self._addressable_areas, ) else: return self.get_highest_z_of_labware_stack(labware_id) @@ -246,7 +247,9 @@ def _get_labware_position_offset( return LabwareOffsetVector(x=0, y=0, z=0) elif isinstance(labware_location, ModuleLocation): module_id = labware_location.moduleId - module_offset = self._modules.get_nominal_module_offset(module_id=module_id) + module_offset = self._modules.get_nominal_module_offset( + module_id=module_id, addressable_areas=self._addressable_areas + ) module_model = self._modules.get_connected_model(module_id) stacking_overlap = self._labware.get_module_overlap_offsets( labware_id, module_model diff --git a/api/src/opentrons/protocol_engine/state/modules.py b/api/src/opentrons/protocol_engine/state/modules.py index 0d56ec611d5..8179ec9d9f5 100644 --- a/api/src/opentrons/protocol_engine/state/modules.py +++ b/api/src/opentrons/protocol_engine/state/modules.py @@ -45,7 +45,6 @@ HeaterShakerMovementRestrictors, DeckType, LabwareMovementOffsetData, - AddressableAreaLocation, ) from .addressable_areas import AddressableAreaView from .. import errors @@ -172,9 +171,6 @@ class ModuleState: deck_type: DeckType """Type of deck that the modules are on.""" - addressable_area_view: AddressableAreaView - """Read-only view of the deck's addressable area state.""" - class ModuleStore(HasState[ModuleState], HandlesActions): """Module state container.""" @@ -184,7 +180,6 @@ class ModuleStore(HasState[ModuleState], HandlesActions): def __init__( self, config: Config, - addressable_area_view: AddressableAreaView, module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None, ) -> None: """Initialize a ModuleStore and its state.""" @@ -196,7 +191,6 @@ def __init__( substate_by_module_id={}, module_offset_by_serial=module_calibration_offsets or {}, deck_type=config.deck_type, - addressable_area_view=addressable_area_view, ) self._robot_type = config.robot_type @@ -217,14 +211,7 @@ def handle_action(self, action: Action) -> None: def _handle_command(self, command: Command) -> None: if isinstance(command.result, LoadModuleResult): - if isinstance(command.params.location, AddressableAreaLocation): - slot_name = ( - self._state.addressable_area_view.get_addressable_area_base_slot( - command.params.location.addressableAreaName - ) - ) - else: - slot_name = command.params.location.slotName + slot_name = command.params.location.slotName self._add_module_substate( module_id=command.result.moduleId, serial_number=command.result.serialNumber, @@ -722,35 +709,70 @@ def get_dimensions(self, module_id: str) -> ModuleDimensions: def get_nominal_module_offset( self, module_id: str, + addressable_areas: AddressableAreaView, ) -> LabwareOffsetVector: """Get the module's nominal offset vector computed with slot transform.""" - definition = self.get_definition(module_id) - slot = self.get_location(module_id).slotName.id - - pre_transform: NDArray[npdouble] = array( - ( - definition.labwareOffset.x, - definition.labwareOffset.y, - definition.labwareOffset.z, - 1, + if ( + self.state.deck_type == DeckType.OT2_STANDARD + or self.state.deck_type == DeckType.OT2_SHORT_TRASH + ): + definition = self.get_definition(module_id) + slot = self.get_location(module_id).slotName.id + + pre_transform: NDArray[npdouble] = array( + ( + definition.labwareOffset.x, + definition.labwareOffset.y, + definition.labwareOffset.z, + 1, + ) + ) + xforms_ser = definition.slotTransforms.get( + str(self._state.deck_type.value), {} + ).get( + slot, + { + "labwareOffset": [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ] + }, + ) + xforms_ser_offset = xforms_ser["labwareOffset"] + + # Apply the slot transform, if any + xform: NDArray[npdouble] = array(xforms_ser_offset) + xformed = dot(xform, pre_transform) + return LabwareOffsetVector( + x=xformed[0], + y=xformed[1], + z=xformed[2], + ) + else: + module = self.get(module_id) + if isinstance(module.location, DeckSlotLocation): + location = module.location.slotName + elif module.model == ModuleModel.THERMOCYCLER_MODULE_V2: + location = DeckSlotName.SLOT_B1 + else: + raise ValueError( + "Module location invalid for nominal module offset calculation." + ) + module_addressable_area = self.ensure_and_convert_module_fixture_location( + location, self.state.deck_type, module.model + ) + module_addressable_area_position = ( + addressable_areas.get_addressable_area_offsets_from_cutout( + module_addressable_area + ) + ) + return LabwareOffsetVector( + x=module_addressable_area_position.x, + y=module_addressable_area_position.y, + z=module_addressable_area_position.z, ) - ) - xforms_ser = definition.slotTransforms.get( - str(self._state.deck_type.value), {} - ).get( - slot, - {"labwareOffset": [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]}, - ) - xforms_ser_offset = xforms_ser["labwareOffset"] - - # Apply the slot transform, if any - xform: NDArray[npdouble] = array(xforms_ser_offset) - xformed = dot(xform, pre_transform) - return LabwareOffsetVector( - x=xformed[0], - y=xformed[1], - z=xformed[2], - ) def get_module_calibration_offset( self, module_id: str @@ -770,7 +792,9 @@ def get_height_over_labware(self, module_id: str) -> float: """Get the height of module parts above module labware base.""" return self.get_dimensions(module_id).overLabwareHeight - def get_module_highest_z(self, module_id: str) -> float: + def get_module_highest_z( + self, module_id: str, addressable_areas: AddressableAreaView + ) -> float: """Get the highest z point of the module, as placed on the robot. The highest Z of a module, unlike the bare overall height, depends on @@ -796,7 +820,7 @@ def get_module_highest_z(self, module_id: str) -> float: z_difference = module_height - default_lw_offset_point nominal_transformed_lw_offset_z = self.get_nominal_module_offset( - module_id=module_id + module_id=module_id, addressable_areas=addressable_areas ).z calibration_offset = self.get_module_calibration_offset(module_id) return ( @@ -961,7 +985,7 @@ def is_edge_move_unsafe(self, mount: MountType, target_slot: DeckSlotName) -> bo def select_hardware_module_to_load( # noqa: C901 self, model: ModuleModel, - location: Union[DeckSlotLocation, AddressableAreaLocation], + location: DeckSlotLocation, attached_modules: Sequence[HardwareModule], ) -> HardwareModule: """Get the next matching hardware module for the given model and location. @@ -988,20 +1012,9 @@ def select_hardware_module_to_load( # noqa: C901 existing_mod_in_slot = None for mod_id, slot in self._state.slot_by_module_id.items(): - if isinstance(location, DeckSlotLocation): - if slot == location.slotName: - existing_mod_in_slot = self._state.hardware_by_module_id.get(mod_id) - break - elif isinstance(location, AddressableAreaLocation): - if ( - slot - == self._state.addressable_area_view.get_addressable_area_base_slot( - location.addressableAreaName - ) - ): - existing_mod_in_slot = self._state.hardware_by_module_id.get(mod_id) - break - + if slot == location.slotName: + existing_mod_in_slot = self._state.hardware_by_module_id.get(mod_id) + break if existing_mod_in_slot: existing_def = existing_mod_in_slot.definition @@ -1009,16 +1022,10 @@ def select_hardware_module_to_load( # noqa: C901 return existing_mod_in_slot else: - if isinstance(location, AddressableAreaLocation): - raise errors.ModuleAlreadyPresentError( - f"A {existing_def.model.value} is already" - f" present in {location.addressableAreaName}" - ) - else: - raise errors.ModuleAlreadyPresentError( - f"A {existing_def.model.value} is already" - f" present in {location.slotName.value}" - ) + raise errors.ModuleAlreadyPresentError( + f"A {existing_def.model.value} is already" + f" present in {location.slotName.value}" + ) for m in attached_modules: if m not in self._state.hardware_by_module_id.values(): @@ -1094,3 +1101,93 @@ def is_flex_deck_with_thermocycler(self) -> bool: return True else: return False + + def ensure_and_convert_module_fixture_location( + self, + deck_slot: DeckSlotName, + deck_type: DeckType, + model: ModuleModel, + ) -> str: + """Ensure module fixture load location is valid. + + Also, convert the deck slot to a valid module fixture addressable area. + """ + + if deck_type == DeckType.OT2_STANDARD or deck_type == DeckType.OT2_SHORT_TRASH: + raise ValueError( + f"Invalid Deck Type: {deck_type.name} - Does not support modules as fixtures." + ) + + if model == ModuleModel.MAGNETIC_BLOCK_V1: + valid_slots = [ + slot + for slot in [ + "A1", + "B1", + "C1", + "D1", + "A2", + "B2", + "C2", + "D2", + "A3", + "B3", + "C3", + "D3", + ] + ] + addressable_areas = [ + "magneticBlockV1A1", + "magneticBlockV1B1", + "magneticBlockV1C1", + "magneticBlockV1D1", + "magneticBlockV1A2", + "magneticBlockV1B2", + "magneticBlockV1C2", + "magneticBlockV1D2", + "magneticBlockV1A3", + "magneticBlockV1B3", + "magneticBlockV1C3", + "magneticBlockV1D3", + ] + + elif model == ModuleModel.HEATER_SHAKER_MODULE_V1: + valid_slots = [ + slot for slot in ["A1", "B1", "C1", "D1", "A3", "B3", "C3", "D3"] + ] + addressable_areas = [ + "heaterShakerV1A1", + "heaterShakerV1B1", + "heaterShakerV1C1", + "heaterShakerV1D1", + "heaterShakerV1A3", + "heaterShakerV1B3", + "heaterShakerV1C3", + "heaterShakerV1D3", + ] + elif model == ModuleModel.TEMPERATURE_MODULE_V2: + valid_slots = [ + slot for slot in ["A1", "B1", "C1", "D1", "A3", "B3", "C3", "D3"] + ] + addressable_areas = [ + "temperatureModuleV2A1", + "temperatureModuleV2B1", + "temperatureModuleV2C1", + "temperatureModuleV2D1", + "temperatureModuleV2A3", + "temperatureModuleV2B3", + "temperatureModuleV2C3", + "temperatureModuleV2D3", + ] + elif model == ModuleModel.THERMOCYCLER_MODULE_V2: + return "thermocyclerModuleV2" + else: + raise ValueError( + f"Unknown module {model.name} has no addressable areas to provide." + ) + + map_addressable_area = { + slot: addressable_area + for slot, addressable_area in zip(valid_slots, addressable_areas) + } + return map_addressable_area[deck_slot.value] diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index f5bc96ce48a..ba856ef6f4b 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -175,9 +175,6 @@ def __init__( ) self._module_store = ModuleStore( config=config, - addressable_area_view=AddressableAreaView( - self._addressable_area_store.state - ), module_calibration_offsets=module_calibration_offsets, ) self._liquid_store = LiquidStore() diff --git a/shared-data/deck/definitions/5/ot3_standard.json b/shared-data/deck/definitions/5/ot3_standard.json index cf9d79a2ebd..14f3795d9c2 100644 --- a/shared-data/deck/definitions/5/ot3_standard.json +++ b/shared-data/deck/definitions/5/ot3_standard.json @@ -356,7 +356,7 @@ { "id": "thermocyclerModuleV2", "areaType": "thermocycler", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [-20.005, 67.9, 10.959999999999994], "matingSurfaceUnitVector": [-1, 1, -1], "boundingBox": { "xDimension": 128.0, @@ -368,7 +368,7 @@ { "id": "heaterShakerV1D1", "areaType": "heaterShaker", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0, 0, 18.95], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -379,7 +379,7 @@ { "id": "heaterShakerV1C1", "areaType": "heaterShaker", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -390,7 +390,7 @@ { "id": "heaterShakerV1B1", "areaType": "heaterShaker", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -401,7 +401,7 @@ { "id": "heaterShakerV1A1", "areaType": "heaterShaker", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -412,7 +412,7 @@ { "id": "heaterShakerV1D3", "areaType": "heaterShaker", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -423,7 +423,7 @@ { "id": "heaterShakerV1C3", "areaType": "heaterShaker", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -434,7 +434,7 @@ { "id": "heaterShakerV1B3", "areaType": "heaterShaker", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -445,7 +445,7 @@ { "id": "heaterShakerV1A3", "areaType": "heaterShaker", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -456,7 +456,7 @@ { "id": "temperatureModuleV2D1", "areaType": "temperatureModule", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -467,7 +467,7 @@ { "id": "temperatureModuleV2C1", "areaType": "temperatureModule", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -478,7 +478,7 @@ { "id": "temperatureModuleV2B1", "areaType": "temperatureModule", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -489,7 +489,7 @@ { "id": "temperatureModuleV2A1", "areaType": "temperatureModule", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -500,7 +500,7 @@ { "id": "temperatureModuleV2D3", "areaType": "temperatureModule", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -511,7 +511,7 @@ { "id": "temperatureModuleV2C3", "areaType": "temperatureModule", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -522,7 +522,7 @@ { "id": "temperatureModuleV2B3", "areaType": "temperatureModule", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -533,7 +533,7 @@ { "id": "temperatureModuleV2A3", "areaType": "temperatureModule", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -544,7 +544,7 @@ { "id": "magneticBlockV1D1", "areaType": "magneticBlock", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 38], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -555,7 +555,7 @@ { "id": "magneticBlockV1C1", "areaType": "magneticBlock", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 38], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -566,7 +566,7 @@ { "id": "magneticBlockV1B1", "areaType": "magneticBlock", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 38], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -577,7 +577,7 @@ { "id": "magneticBlockV1A1", "areaType": "magneticBlock", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 38], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -588,7 +588,7 @@ { "id": "magneticBlockV1D2", "areaType": "magneticBlock", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 38], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -599,7 +599,7 @@ { "id": "magneticBlockV1C2", "areaType": "magneticBlock", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 38], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -610,7 +610,7 @@ { "id": "magneticBlockV1B2", "areaType": "magneticBlock", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 38], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -621,7 +621,7 @@ { "id": "magneticBlockV1A2", "areaType": "magneticBlock", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 38], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -632,7 +632,7 @@ { "id": "magneticBlockV1D3", "areaType": "magneticBlock", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 38], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -643,7 +643,7 @@ { "id": "magneticBlockV1C3", "areaType": "magneticBlock", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 38], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -654,7 +654,7 @@ { "id": "magneticBlockV1B3", "areaType": "magneticBlock", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 38], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -665,7 +665,7 @@ { "id": "magneticBlockV1A3", "areaType": "magneticBlock", - "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "offsetFromCutoutFixture": [0.0, 0.0, 38], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0,