diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index 15b04048699..a9dcc3e7dc3 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -27,7 +27,6 @@ ModuleDefinition, Liquid, DeckConfigurationType, - AddressableAreaLocation, ) @@ -235,12 +234,12 @@ class SetDeckConfigurationAction: class AddAddressableAreaAction: """Add a single addressable area to state. - This differs from the deck configuration in ProvideDeckConfigurationAction which + This differs from the deck configuration in SetDeckConfigurationAction which sends over a mapping of cutout fixtures. This action will only load one addressable area and that should be pre-validated before being sent via the action. """ - addressable_area: AddressableAreaLocation + addressable_area_name: str @dataclasses.dataclass(frozen=True) diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py index 069c2803b22..14c1f0f9ea3 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py @@ -95,7 +95,6 @@ async def execute(self, params: CloseLidParams) -> SuccessData[CloseLidResult]: deck_slot=self._state_view.modules.get_location( params.moduleId ).slotName, - deck_type=self._state_view.config.deck_type, model=absorbance_model, ) ) diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py index 1ad56413f9a..96495a2bcde 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py @@ -91,7 +91,6 @@ async def execute(self, params: OpenLidParams) -> SuccessData[OpenLidResult]: deck_slot=self._state_view.modules.get_location( params.moduleId ).slotName, - deck_type=self._state_view.config.deck_type, model=absorbance_model, ) ) diff --git a/api/src/opentrons/protocol_engine/commands/load_labware.py b/api/src/opentrons/protocol_engine/commands/load_labware.py index fb97f5d2c87..2d8b8c3df78 100644 --- a/api/src/opentrons/protocol_engine/commands/load_labware.py +++ b/api/src/opentrons/protocol_engine/commands/load_labware.py @@ -104,6 +104,8 @@ async def execute( self, params: LoadLabwareParams ) -> SuccessData[LoadLabwareResult]: """Load definition and calibration data necessary for a labware.""" + state_update = StateUpdate() + # TODO (tz, 8-15-2023): extend column validation to column 1 when working # on https://opentrons.atlassian.net/browse/RSS-258 and completing # https://opentrons.atlassian.net/browse/RSS-255 @@ -128,10 +130,12 @@ async def execute( self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( area_name ) + state_update.set_addressable_area_used(area_name) elif isinstance(params.location, DeckSlotLocation): self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.location.slotName.id ) + state_update.set_addressable_area_used(params.location.slotName.id) verified_location = self._state_view.geometry.ensure_location_not_occupied( params.location @@ -144,8 +148,6 @@ async def execute( labware_id=params.labwareId, ) - state_update = StateUpdate() - state_update.set_loaded_labware( labware_id=loaded_labware.labware_id, offset_id=loaded_labware.offsetId, diff --git a/api/src/opentrons/protocol_engine/commands/load_module.py b/api/src/opentrons/protocol_engine/commands/load_module.py index 79e67182666..f8b88e08814 100644 --- a/api/src/opentrons/protocol_engine/commands/load_module.py +++ b/api/src/opentrons/protocol_engine/commands/load_module.py @@ -4,6 +4,8 @@ from typing_extensions import Literal from pydantic import BaseModel, Field +from opentrons.protocol_engine.state.update_types import StateUpdate + from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ..errors.error_occurrence import ErrorOccurrence from ..types import ( @@ -116,26 +118,35 @@ def __init__( async def execute(self, params: LoadModuleParams) -> SuccessData[LoadModuleResult]: """Check that the requested module is attached and assign its identifier.""" + state_update = StateUpdate() + module_type = params.model.as_type() self._ensure_module_location(params.location.slotName, module_type) + # todo(mm, 2024-12-03): Theoretically, we should be able to deal with + # addressable areas and deck configurations the same way between OT-2 and Flex. + # Can this be simplified? 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 ) 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, + addressable_area_provided_by_module = ( + self._state_view.modules.ensure_and_convert_module_fixture_location( + deck_slot=params.location.slotName, + model=params.model, + ) ) self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( - addressable_area + addressable_area_provided_by_module ) verified_location = self._state_view.geometry.ensure_location_not_occupied( params.location ) + state_update.set_addressable_area_used( + addressable_area_name=params.location.slotName.id + ) if params.model == ModuleModel.MAGNETIC_BLOCK_V1: loaded_module = await self._equipment.load_magnetic_block( @@ -157,11 +168,15 @@ async def execute(self, params: LoadModuleParams) -> SuccessData[LoadModuleResul model=loaded_module.definition.model, definition=loaded_module.definition, ), + state_update=state_update, ) def _ensure_module_location( self, slot: DeckSlotName, module_type: ModuleType ) -> None: + # todo(mm, 2024-12-03): Theoretically, we should be able to deal with + # addressable areas and deck configurations the same way between OT-2 and Flex. + # Can this be simplified? if self._state_view.config.robot_type == "OT-2 Standard": slot_def = self._state_view.addressable_areas.get_slot_definition(slot.id) compatible_modules = slot_def["compatibleModuleTypes"] diff --git a/api/src/opentrons/protocol_engine/commands/move_labware.py b/api/src/opentrons/protocol_engine/commands/move_labware.py index 09cdc08561c..b64491f5192 100644 --- a/api/src/opentrons/protocol_engine/commands/move_labware.py +++ b/api/src/opentrons/protocol_engine/commands/move_labware.py @@ -156,6 +156,7 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( area_name ) + state_update.set_addressable_area_used(addressable_area_name=area_name) if fixture_validation.is_gripper_waste_chute(area_name): # When dropping off labware in the waste chute, some bigger pieces @@ -201,6 +202,9 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.newLocation.slotName.id ) + state_update.set_addressable_area_used( + addressable_area_name=params.newLocation.slotName.id + ) available_new_location = self._state_view.geometry.ensure_location_not_occupied( location=params.newLocation diff --git a/api/src/opentrons/protocol_engine/commands/movement_common.py b/api/src/opentrons/protocol_engine/commands/movement_common.py index 7917daa8613..ca12d2d1ad8 100644 --- a/api/src/opentrons/protocol_engine/commands/movement_common.py +++ b/api/src/opentrons/protocol_engine/commands/movement_common.py @@ -265,17 +265,21 @@ async def move_to_addressable_area( ) ], ), - state_update=StateUpdate().clear_all_pipette_locations(), + state_update=StateUpdate() + .clear_all_pipette_locations() + .set_addressable_area_used(addressable_area_name=addressable_area_name), ) else: deck_point = DeckPoint.construct(x=x, y=y, z=z) return SuccessData( public=DestinationPositionResult(position=deck_point), - state_update=StateUpdate().set_pipette_location( + state_update=StateUpdate() + .set_pipette_location( pipette_id=pipette_id, new_addressable_area_name=addressable_area_name, new_deck_point=deck_point, - ), + ) + .set_addressable_area_used(addressable_area_name=addressable_area_name), ) diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 3479e0a295b..92d992016cd 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -30,7 +30,6 @@ HexColor, PostRunHardwareState, DeckConfigurationType, - AddressableAreaLocation, ) from .execution import ( QueueWorker, @@ -574,9 +573,8 @@ def add_liquid( def add_addressable_area(self, addressable_area_name: str) -> None: """Add an addressable area to state.""" - area = AddressableAreaLocation(addressableAreaName=addressable_area_name) self._action_dispatcher.dispatch( - AddAddressableAreaAction(addressable_area=area) + AddAddressableAreaAction(addressable_area_name) ) def reset_tips(self, labware_id: str) -> None: diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index bd7d8de0188..16898ccb4ed 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -1,7 +1,7 @@ """Basic addressable area data state and store.""" from dataclasses import dataclass from functools import cached_property -from typing import Dict, List, Optional, Set, Union +from typing import Dict, List, Optional, Set from opentrons_shared_data.robot.types import RobotType, RobotDefinition from opentrons_shared_data.deck.types import ( @@ -12,14 +12,6 @@ from opentrons.types import Point, DeckSlotName -from ..commands import ( - Command, - LoadLabwareResult, - LoadModuleResult, - MoveLabwareResult, - MoveToAddressableAreaResult, - MoveToAddressableAreaForDropTipResult, -) from ..errors import ( IncompatibleAddressableAreaError, AreaNotInDeckConfigurationError, @@ -29,19 +21,18 @@ ) from ..resources import deck_configuration_provider from ..types import ( - DeckSlotLocation, - AddressableAreaLocation, AddressableArea, PotentialCutoutFixture, DeckConfigurationType, Dimensions, ) +from ..actions.get_state_update import get_state_updates from ..actions import ( Action, - SucceedCommandAction, SetDeckConfigurationAction, AddAddressableAreaAction, ) +from . import update_types from .config import Config from ._abstract_store import HasState, HandlesActions @@ -193,10 +184,14 @@ def __init__( def handle_action(self, action: Action) -> None: """Modify state in reaction to an action.""" - if isinstance(action, SucceedCommandAction): - self._handle_command(action.command) - elif isinstance(action, AddAddressableAreaAction): - self._check_location_is_addressable_area(action.addressable_area) + for state_update in get_state_updates(action): + if state_update.addressable_area_used != update_types.NO_CHANGE: + self._add_addressable_area( + state_update.addressable_area_used.addressable_area_name + ) + + if isinstance(action, AddAddressableAreaAction): + self._add_addressable_area(action.addressable_area_name) elif isinstance(action, SetDeckConfigurationAction): current_state = self._state if ( @@ -211,28 +206,6 @@ def handle_action(self, action: Action) -> None: ) ) - def _handle_command(self, command: Command) -> None: - """Modify state in reaction to a command.""" - if isinstance(command.result, LoadLabwareResult): - location = command.params.location - if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)): - self._check_location_is_addressable_area(location) - - elif isinstance(command.result, MoveLabwareResult): - location = command.params.newLocation - if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)): - self._check_location_is_addressable_area(location) - - elif isinstance(command.result, LoadModuleResult): - self._check_location_is_addressable_area(command.params.location) - - elif isinstance( - command.result, - (MoveToAddressableAreaResult, MoveToAddressableAreaForDropTipResult), - ): - addressable_area_name = command.params.addressableAreaName - self._check_location_is_addressable_area(addressable_area_name) - @staticmethod def _get_addressable_areas_from_deck_configuration( deck_config: DeckConfigurationType, deck_definition: DeckDefinitionV5 @@ -260,16 +233,7 @@ def _get_addressable_areas_from_deck_configuration( ) return {area.area_name: area for area in addressable_areas} - def _check_location_is_addressable_area( - self, location: Union[DeckSlotLocation, AddressableAreaLocation, str] - ) -> None: - if isinstance(location, DeckSlotLocation): - addressable_area_name = location.slotName.id - elif isinstance(location, AddressableAreaLocation): - addressable_area_name = location.addressableAreaName - else: - addressable_area_name = location - + def _add_addressable_area(self, addressable_area_name: str) -> None: if addressable_area_name not in self._state.loaded_addressable_areas_by_name: cutout_id = self._validate_addressable_area_for_simulation( addressable_area_name diff --git a/api/src/opentrons/protocol_engine/state/modules.py b/api/src/opentrons/protocol_engine/state/modules.py index 0292329b8ea..ebf503c51fb 100644 --- a/api/src/opentrons/protocol_engine/state/modules.py +++ b/api/src/opentrons/protocol_engine/state/modules.py @@ -908,7 +908,7 @@ def get_nominal_offset_to_child( "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 + location, module.model ) module_addressable_area_position = ( addressable_areas.get_addressable_area_offsets_from_cutout( @@ -1281,13 +1281,14 @@ def convert_absorbance_reader_data_points( 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. """ + deck_type = self._state.deck_type + 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." diff --git a/api/src/opentrons/protocol_engine/state/update_types.py b/api/src/opentrons/protocol_engine/state/update_types.py index 1e81881a2b4..25b7802976c 100644 --- a/api/src/opentrons/protocol_engine/state/update_types.py +++ b/api/src/opentrons/protocol_engine/state/update_types.py @@ -268,6 +268,13 @@ class FilesAddedUpdate: file_ids: list[str] +@dataclasses.dataclass +class AddressableAreaUsedUpdate: + """An update that says an addressable area has been used.""" + + addressable_area_name: str + + @dataclasses.dataclass class StateUpdate: """Represents an update to perform on engine state.""" @@ -308,6 +315,8 @@ class StateUpdate: files_added: FilesAddedUpdate | NoChangeType = NO_CHANGE + addressable_area_used: AddressableAreaUsedUpdate | NoChangeType = NO_CHANGE + def append(self, other: Self) -> Self: """Apply another `StateUpdate` "on top of" this one. @@ -334,7 +343,8 @@ def reduce(cls: typing.Type[Self], *args: Self) -> Self: return accumulator # These convenience functions let the caller avoid the boilerplate of constructing a - # complicated dataclass tree. + # complicated dataclass tree, and allow chaining. + @typing.overload def set_pipette_location( self: Self, *, pipette_id: str, new_deck_point: DeckPoint @@ -567,3 +577,10 @@ def set_absorbance_reader_lid(self: Self, module_id: str, is_lid_on: bool) -> Se module_id=module_id, is_lid_on=is_lid_on ) return self + + def set_addressable_area_used(self: Self, addressable_area_name: str) -> Self: + """Mark that an addressable area has been used. See `AddressableAreaUsedUpdate`.""" + self.addressable_area_used = AddressableAreaUsedUpdate( + addressable_area_name=addressable_area_name + ) + return self diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_labware.py b/api/tests/opentrons/protocol_engine/commands/test_load_labware.py index 3873f9854b4..8229d7f4265 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_labware.py @@ -1,12 +1,9 @@ """Test load labware commands.""" import inspect from typing import Optional -from opentrons.protocol_engine.state.update_types import ( - LoadedLabwareUpdate, - StateUpdate, -) -import pytest +from unittest.mock import sentinel +import pytest from decoy import Decoy from opentrons.types import DeckSlotName @@ -18,12 +15,19 @@ ) from opentrons.protocol_engine.types import ( + AddressableAreaLocation, DeckSlotLocation, + LabwareLocation, OnLabwareLocation, ) from opentrons.protocol_engine.execution import LoadedLabwareData, EquipmentHandler from opentrons.protocol_engine.resources import labware_validation from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.update_types import ( + AddressableAreaUsedUpdate, + LoadedLabwareUpdate, + StateUpdate, +) from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.load_labware import ( @@ -42,33 +46,40 @@ def patch_mock_labware_validation( monkeypatch.setattr(labware_validation, name, decoy.mock(func=func)) -@pytest.mark.parametrize("display_name", [("My custom display name"), (None)]) -async def test_load_labware_implementation( +@pytest.mark.parametrize("display_name", ["My custom display name", None]) +@pytest.mark.parametrize( + ("location", "expected_addressable_area_name"), + [ + (DeckSlotLocation(slotName=DeckSlotName.SLOT_3), "3"), + (AddressableAreaLocation(addressableAreaName="3"), "3"), + ], +) +async def test_load_labware_on_slot_or_addressable_area( decoy: Decoy, well_plate_def: LabwareDefinition, equipment: EquipmentHandler, state_view: StateView, display_name: Optional[str], + location: LabwareLocation, + expected_addressable_area_name: str, ) -> None: """A LoadLabware command should have an execution implementation.""" subject = LoadLabwareImplementation(equipment=equipment, state_view=state_view) data = LoadLabwareParams( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3), + location=location, loadName="some-load-name", namespace="opentrons-test", version=1, displayName=display_name, ) - decoy.when( - state_view.geometry.ensure_location_not_occupied( - DeckSlotLocation(slotName=DeckSlotName.SLOT_3) - ) - ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_4)) + decoy.when(state_view.geometry.ensure_location_not_occupied(location)).then_return( + sentinel.validated_empty_location + ) decoy.when( await equipment.load_labware( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + location=sentinel.validated_empty_location, load_name="some-load-name", namespace="opentrons-test", version=1, @@ -99,9 +110,12 @@ async def test_load_labware_implementation( labware_id="labware-id", definition=well_plate_def, offset_id="labware-offset-id", - new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + new_location=sentinel.validated_empty_location, display_name=display_name, - ) + ), + addressable_area_used=AddressableAreaUsedUpdate( + addressable_area_name=expected_addressable_area_name + ), ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_module.py b/api/tests/opentrons/protocol_engine/commands/test_load_module.py index e5098b5dc49..65ee30e7a88 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_module.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_module.py @@ -1,9 +1,12 @@ """Test load module command.""" -import pytest from typing import cast +from unittest.mock import sentinel + +import pytest from decoy import Decoy from opentrons.protocol_engine.errors import LocationIsOccupiedError +from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.state.state import StateView from opentrons_shared_data.robot.types import RobotType from opentrons.types import DeckSlotName @@ -70,6 +73,13 @@ async def test_load_module_implementation( ) ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_2)) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + deck_slot=data.location.slotName, + model=data.model, + ) + ).then_return(sentinel.addressable_area_provided_by_module) + decoy.when( await equipment.load_module( model=ModuleModel.TEMPERATURE_MODULE_V2, @@ -85,6 +95,11 @@ async def test_load_module_implementation( ) result = await subject.execute(data) + decoy.verify( + state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + sentinel.addressable_area_provided_by_module + ) + ) assert result == SuccessData( public=LoadModuleResult( moduleId="module-id", @@ -92,6 +107,11 @@ async def test_load_module_implementation( model=ModuleModel.TEMPERATURE_MODULE_V2, definition=tempdeck_v2_def, ), + state_update=update_types.StateUpdate( + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=data.location.slotName.id + ) + ), ) @@ -125,6 +145,13 @@ async def test_load_module_implementation_mag_block( ) ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_2)) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + deck_slot=data.location.slotName, + model=data.model, + ) + ).then_return(sentinel.addressable_area_provided_by_module) + decoy.when( await equipment.load_magnetic_block( model=ModuleModel.MAGNETIC_BLOCK_V1, @@ -140,6 +167,11 @@ async def test_load_module_implementation_mag_block( ) result = await subject.execute(data) + decoy.verify( + state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + sentinel.addressable_area_provided_by_module + ) + ) assert result == SuccessData( public=LoadModuleResult( moduleId="module-id", @@ -147,6 +179,11 @@ async def test_load_module_implementation_mag_block( model=ModuleModel.MAGNETIC_BLOCK_V1, definition=mag_block_v1_def, ), + state_update=update_types.StateUpdate( + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=data.location.slotName.id + ) + ), ) @@ -180,6 +217,13 @@ async def test_load_module_implementation_abs_reader( ) ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_D3)) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + deck_slot=data.location.slotName, + model=data.model, + ) + ).then_return(sentinel.addressable_area_name) + decoy.when( await equipment.load_module( model=ModuleModel.ABSORBANCE_READER_V1, @@ -202,6 +246,11 @@ async def test_load_module_implementation_abs_reader( model=ModuleModel.ABSORBANCE_READER_V1, definition=abs_reader_v1_def, ), + state_update=update_types.StateUpdate( + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=data.location.slotName.id + ) + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py index a946eccf05d..49e3c4f5471 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py @@ -1,6 +1,8 @@ """Test the ``moveLabware`` command.""" from datetime import datetime import inspect +from unittest.mock import sentinel + import pytest from decoy import Decoy, matchers @@ -90,9 +92,10 @@ async def test_manual_move_labware_implementation( times_pause_called: int, ) -> None: """It should execute a pause and return the new offset.""" + new_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_4) data = MoveLabwareParams( labwareId="my-cool-labware-id", - newLocation=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + newLocation=new_location, strategy=strategy, ) @@ -131,7 +134,10 @@ async def test_manual_move_labware_implementation( labware_id="my-cool-labware-id", offset_id="wowzers-a-new-offset-id", new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_5), - ) + ), + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=new_location.slotName.id + ), ), ) @@ -211,20 +217,19 @@ async def test_gripper_move_labware_implementation( """It should delegate to the equipment handler and return the new offset.""" from_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_1) new_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_5) + pick_up_offset = LabwareOffsetVector(x=1, y=2, z=3) data = MoveLabwareParams( labwareId="my-cool-labware-id", - newLocation=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + newLocation=new_location, strategy=LabwareMovementStrategy.USING_GRIPPER, - pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3), + pickUpOffset=pick_up_offset, dropOffset=None, ) decoy.when( state_view.labware.get_definition(labware_id="my-cool-labware-id") - ).then_return( - LabwareDefinition.construct(namespace="my-cool-namespace") # type: ignore[call-arg] - ) + ).then_return(sentinel.labware_definition) decoy.when(state_view.labware.get(labware_id="my-cool-labware-id")).then_return( LoadedLabware( id="my-cool-labware-id", @@ -235,29 +240,25 @@ async def test_gripper_move_labware_implementation( ) ) decoy.when( - state_view.geometry.ensure_location_not_occupied( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), - ) - ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_5)) + state_view.geometry.ensure_location_not_occupied(location=new_location) + ).then_return(sentinel.new_location_validated_unoccupied) decoy.when( equipment.find_applicable_labware_offset_id( labware_definition_uri="opentrons-test/load-name/1", - labware_location=new_location, + labware_location=sentinel.new_location_validated_unoccupied, ) ).then_return("wowzers-a-new-offset-id") - validated_from_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_6) - validated_new_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_7) decoy.when( state_view.geometry.ensure_valid_gripper_location(from_location) - ).then_return(validated_from_location) - decoy.when( - state_view.geometry.ensure_valid_gripper_location(new_location) - ).then_return(validated_new_location) + ).then_return(sentinel.from_location_validated_for_gripper) decoy.when( - labware_validation.validate_gripper_compatible( - LabwareDefinition.construct(namespace="my-cool-namespace") # type: ignore[call-arg] + state_view.geometry.ensure_valid_gripper_location( + sentinel.new_location_validated_unoccupied ) + ).then_return(sentinel.new_location_validated_for_gripper) + decoy.when( + labware_validation.validate_gripper_compatible(sentinel.labware_definition) ).then_return(True) result = await subject.execute(data) @@ -265,10 +266,10 @@ async def test_gripper_move_labware_implementation( state_view.labware.raise_if_labware_has_labware_on_top("my-cool-labware-id"), await labware_movement.move_labware_with_gripper( labware_id="my-cool-labware-id", - current_location=validated_from_location, - new_location=validated_new_location, + current_location=sentinel.from_location_validated_for_gripper, + new_location=sentinel.new_location_validated_for_gripper, user_offset_data=LabwareMovementOffsetData( - pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3), + pickUpOffset=pick_up_offset, dropOffset=LabwareOffsetVector(x=0, y=0, z=0), ), post_drop_slide_offset=None, @@ -282,9 +283,12 @@ async def test_gripper_move_labware_implementation( pipette_location=update_types.CLEAR, labware_location=update_types.LabwareLocationUpdate( labware_id="my-cool-labware-id", - new_location=new_location, + new_location=sentinel.new_location_validated_unoccupied, offset_id="wowzers-a-new-offset-id", ), + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=new_location.slotName.id + ), ), ) @@ -380,6 +384,9 @@ async def test_gripper_error( state_update=update_types.StateUpdate( labware_location=update_types.NO_CHANGE, pipette_location=update_types.CLEAR, + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=new_location.slotName.id + ), ), ) @@ -520,6 +527,9 @@ async def test_gripper_move_to_waste_chute_implementation( new_location=new_location, offset_id="wowzers-a-new-offset-id", ), + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name=new_location.addressableAreaName + ), ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py index 9f1470b95da..9142f792252 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py @@ -93,7 +93,10 @@ async def test_move_to_addressable_area_implementation_non_gen1( pipette_id="abc", new_location=update_types.AddressableArea(addressable_area_name="123"), new_deck_point=DeckPoint(x=9, y=8, z=7), - ) + ), + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name="123" + ), ), ) @@ -154,7 +157,10 @@ async def test_move_to_addressable_area_implementation_with_gen1( pipette_id="abc", new_location=update_types.AddressableArea(addressable_area_name="123"), new_deck_point=DeckPoint(x=9, y=8, z=7), - ) + ), + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name="123" + ), ), ) @@ -206,5 +212,10 @@ async def test_move_to_addressable_area_implementation_handles_stalls( public=StallOrCollisionError.construct( id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] ), - state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name="123" + ), + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py index 019ec6bec3f..b6ee2097458 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py @@ -79,7 +79,10 @@ async def test_move_to_addressable_area_for_drop_tip_implementation( pipette_id="abc", new_location=update_types.AddressableArea(addressable_area_name="123"), new_deck_point=DeckPoint(x=9, y=8, z=7), - ) + ), + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name="123" + ), ), ) @@ -133,5 +136,10 @@ async def test_move_to_addressable_area_for_drop_tip_handles_stalls( public=StallOrCollisionError.construct( id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] ), - state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name="123" + ), + ), ) diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store_old.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store_old.py index b9e3e8f4e78..1bbccf96d42 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store_old.py @@ -8,15 +8,13 @@ import pytest from opentrons_shared_data.deck.types import DeckDefinitionV5 -from opentrons_shared_data.labware.labware_definition import Parameters -from opentrons.protocols.models import LabwareDefinition -from opentrons.types import DeckSlotName -from opentrons.protocol_engine.commands import Command +from opentrons.protocol_engine.commands import Command, Comment from opentrons.protocol_engine.actions import ( SucceedCommandAction, AddAddressableAreaAction, ) +from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.state.config import Config from opentrons.protocol_engine.state.addressable_areas import ( AddressableAreaStore, @@ -25,17 +23,6 @@ from opentrons.protocol_engine.types import ( DeckType, DeckConfigurationType, - ModuleModel, - LabwareMovementStrategy, - DeckSlotLocation, - AddressableAreaLocation, -) - -from .command_fixtures import ( - create_load_labware_command, - create_load_module_command, - create_move_labware_command, - create_move_to_addressable_area_command, ) @@ -56,6 +43,11 @@ def _make_deck_config() -> DeckConfigurationType: ] +def _dummy_command() -> Command: + """Return a placeholder command.""" + return Comment.construct() # type: ignore[call-arg] + + @pytest.fixture def simulated_subject( ot3_standard_deck_def: DeckDefinitionV5, @@ -167,140 +159,30 @@ def test_initial_state( assert len(subject.state.loaded_addressable_areas_by_name) == 16 -@pytest.mark.parametrize( - ("command", "expected_area"), - ( - ( - create_load_labware_command( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), - labware_id="test-labware-id", - definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] - namespace="bleh", - version=123, - ), - offset_id="offset-id", - display_name="display-name", - ), - "A1", - ), - ( - create_load_labware_command( - location=AddressableAreaLocation(addressableAreaName="A4"), - labware_id="test-labware-id", - definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] - namespace="bleh", - version=123, - ), - offset_id="offset-id", - display_name="display-name", - ), - "A4", - ), - ( - create_load_module_command( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), - module_id="test-module-id", - model=ModuleModel.TEMPERATURE_MODULE_V2, - ), - "A1", - ), - ( - create_move_labware_command( - new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), - strategy=LabwareMovementStrategy.USING_GRIPPER, - ), - "A1", - ), - ( - create_move_labware_command( - new_location=AddressableAreaLocation(addressableAreaName="A4"), - strategy=LabwareMovementStrategy.USING_GRIPPER, - ), - "A4", - ), - ( - create_move_to_addressable_area_command( - pipette_id="pipette-id", addressable_area_name="gripperWasteChute" - ), - "gripperWasteChute", - ), - ), -) -def test_addressable_area_referencing_commands_load_on_simulated_deck( - command: Command, - expected_area: str, +@pytest.mark.parametrize("addressable_area_name", ["A1", "A4", "gripperWasteChute"]) +def test_addressable_area_usage_in_simulation( simulated_subject: AddressableAreaStore, + addressable_area_name: str, ) -> None: - """It should check and store the addressable area when referenced in a command.""" - simulated_subject.handle_action(SucceedCommandAction(command=command)) - assert expected_area in simulated_subject.state.loaded_addressable_areas_by_name - - -@pytest.mark.parametrize( - ("command", "expected_area"), - ( - ( - create_load_labware_command( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), - labware_id="test-labware-id", - definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] - namespace="bleh", - version=123, - ), - offset_id="offset-id", - display_name="display-name", - ), - "A1", - ), - ( - create_load_labware_command( - location=AddressableAreaLocation(addressableAreaName="C4"), - labware_id="test-labware-id", - definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] - namespace="bleh", - version=123, - ), - offset_id="offset-id", - display_name="display-name", - ), - "C4", - ), - ( - create_load_module_command( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), - module_id="test-module-id", - model=ModuleModel.TEMPERATURE_MODULE_V2, - ), - "A1", - ), - ( - create_move_labware_command( - new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), - strategy=LabwareMovementStrategy.USING_GRIPPER, - ), - "A1", - ), - ( - create_move_labware_command( - new_location=AddressableAreaLocation(addressableAreaName="C4"), - strategy=LabwareMovementStrategy.USING_GRIPPER, + """Simulating stores should correctly handle `StateUpdate`s with addressable areas.""" + assert ( + addressable_area_name + not in simulated_subject.state.loaded_addressable_areas_by_name + ) + simulated_subject.handle_action( + SucceedCommandAction( + command=_dummy_command(), + state_update=update_types.StateUpdate( + addressable_area_used=update_types.AddressableAreaUsedUpdate( + addressable_area_name + ) ), - "C4", - ), - ), -) -def test_addressable_area_referencing_commands_load( - command: Command, - expected_area: str, - subject: AddressableAreaStore, -) -> None: - """It should check that the addressable area is in the deck config.""" - subject.handle_action(SucceedCommandAction(command=command)) - assert expected_area in subject.state.loaded_addressable_areas_by_name + ) + ) + assert ( + addressable_area_name + in simulated_subject.state.loaded_addressable_areas_by_name + ) def test_add_addressable_area_action( @@ -308,10 +190,6 @@ def test_add_addressable_area_action( ) -> None: """It should add the addressable area to the store.""" simulated_subject.handle_action( - AddAddressableAreaAction( - addressable_area=AddressableAreaLocation( - addressableAreaName="movableTrashA1" - ) - ) + AddAddressableAreaAction(addressable_area_name="movableTrashA1") ) assert "movableTrashA1" in simulated_subject.state.loaded_addressable_areas_by_name diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index 34dac853ef8..cd6ffeb99cb 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -32,7 +32,6 @@ ModuleModel, Liquid, PostRunHardwareState, - AddressableAreaLocation, ) from opentrons.protocol_engine.execution import ( QueueWorker, @@ -1119,11 +1118,7 @@ def test_add_addressable_area( decoy.verify( action_dispatcher.dispatch( - AddAddressableAreaAction( - addressable_area=AddressableAreaLocation( - addressableAreaName="my_funky_area" - ) - ) + AddAddressableAreaAction(addressable_area_name="my_funky_area") ) )