Skip to content

Commit

Permalink
streamline engine logic include plate reader lid as fixed labware and…
Browse files Browse the repository at this point in the history
… ensure addressable areas used exclusively
  • Loading branch information
CaseyBatten committed Jul 24, 2024
1 parent 772d913 commit 6371d4d
Show file tree
Hide file tree
Showing 18 changed files with 250 additions and 75 deletions.
4 changes: 1 addition & 3 deletions api/src/opentrons/protocol_api/core/engine/module_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,6 @@ def close_lid(
self._engine_client.execute_command(
cmd.absorbance_reader.CloseLidParams(
moduleId=self.module_id,
# FIXME: using staging slot for now (wrong z), but this should be the lid dock slot
pickUpOffset=LabwareOffsetVector(x=14, y=0, z=0),
dropOffset=LabwareOffsetVector(x=14, y=0, z=0),
)
Expand All @@ -565,7 +564,6 @@ def open_lid(self) -> None:
cmd.absorbance_reader.OpenLidParams(
moduleId=self.module_id,
pickUpOffset=LabwareOffsetVector(x=14, y=0, z=0),
# FIXME: using staging slot for now (wrong z), but this should be the lid dock slot
dropOffset=LabwareOffsetVector(x=14, y=0, z=1),
dropOffset=LabwareOffsetVector(x=14, y=0, z=0),
)
)
30 changes: 17 additions & 13 deletions api/src/opentrons/protocol_api/core/engine/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,31 +440,35 @@ def load_module(
existing_labware_ids=list(self._labware_cores_by_id.keys()),
existing_module_ids=list(self._module_cores_by_id.keys()),
)
self._load_module_lid(module_core)

# When the protocol engine is created, we add Module Lids as part of the deck fixed labware
# If a valid module exists in the deck config. For analysis, we add the labware here since
# deck fixed labware is not created under the same conditions.
if self._engine_client.state.config.use_virtual_modules:
self._load_virtual_module_lid(module_core)

self._module_cores_by_id[module_core.module_id] = module_core

return module_core

def _load_module_lid(
def _load_virtual_module_lid(
self, module_core: Union[ModuleCore, NonConnectedModuleCore]
) -> None:
if isinstance(module_core, AbsorbanceReaderCore):
lid_slot = (
self._engine_client.state.modules.absorbance_reader_dock_location_name(
module_id=module_core.module_id
lid = self._engine_client.execute_command_without_recovery(
cmd.LoadLabwareParams(
loadName="opentrons_flex_lid_absorbance_plate_reader_module",
location=ModuleLocation(moduleId=module_core.module_id),
namespace="opentrons",
version=1,
displayName="Absorbance Reader Lid",
)
)
lid = self.load_labware(
load_name="opentrons_flex_lid_absorbance_plate_reader_module",
location=lid_slot,
namespace="opentrons",
version=1,
label="Absorbance Reader Lid",
)
validation.ensure_definition_is_labware(lid.definition)

self._engine_client.add_absorbance_reader_lid(
module_id=module_core.module_id,
lid_id=lid.labware_id,
lid_id=lid.labwareId,
)

def _create_non_connected_module_core(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from opentrons.protocol_engine.types import (
LabwareOffsetVector,
LabwareMovementOffsetData,
ModuleLocation,
AddressableAreaLocation,
)
from opentrons.protocol_engine.resources import labware_validation
from .types import MoveLidResult
Expand Down Expand Up @@ -85,9 +85,18 @@ async def execute(
self._state_view.geometry.ensure_valid_gripper_location(current_location)
)

# we need to move the lid onto the module
new_location = ModuleLocation(moduleId=mod_substate.module_id)
# new_location = self._state_view.modules.get_location(mod_substate.module_id)
# we need to move the lid onto the module reader
absorbance_model = self._state_view.modules.get_requested_model(params.moduleId)
assert absorbance_model is not None
new_location = AddressableAreaLocation(
addressableAreaName=self._state_view.modules.ensure_and_convert_module_fixture_location(
deck_slot=self._state_view.modules.get_location(
params.moduleId
).slotName,
deck_type=self._state_view.config.deck_type,
model=absorbance_model,
)
)
validated_new_location = (
self._state_view.geometry.ensure_valid_gripper_location(new_location)
)
Expand Down
4 changes: 1 addition & 3 deletions api/src/opentrons/protocol_engine/commands/load_labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ async def execute(
area_name = params.location.addressableAreaName
if not (
fixture_validation.is_deck_slot(params.location.addressableAreaName)
or fixture_validation.is_abs_reader_lid_dock(
params.location.addressableAreaName
)
or fixture_validation.is_abs_reader(params.location.addressableAreaName)
):
raise LabwareIsNotAllowedInLocationError(
f"Cannot load {params.loadName} onto addressable area {area_name}"
Expand Down
7 changes: 3 additions & 4 deletions api/src/opentrons/protocol_engine/create_protocol_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,10 @@ async def create_protocol_engine(
"""
deck_data = DeckDataProvider(config.deck_type)
deck_definition = await deck_data.get_deck_definition()
deck_fixed_labware = (
await deck_data.get_deck_fixed_labware(deck_definition)
if load_fixed_trash
else []
deck_fixed_labware = await deck_data.get_deck_fixed_labware(
load_fixed_trash, deck_definition, deck_configuration
)

module_calibration_offsets = ModuleDataProvider.load_module_calibrations()

state_store = StateStore(
Expand Down
63 changes: 58 additions & 5 deletions api/src/opentrons/protocol_engine/resources/deck_data_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@
from opentrons.protocols.models import LabwareDefinition
from opentrons.types import DeckSlotName

from ..types import DeckSlotLocation, DeckType
from ..types import (
DeckSlotLocation,
DeckType,
LabwareLocation,
AddressableAreaLocation,
DeckConfigurationType,
)
from .labware_data_provider import LabwareDataProvider
from ..resources import deck_configuration_provider


@final
Expand All @@ -23,7 +30,7 @@ class DeckFixedLabware:
"""A labware fixture that is always present on a deck."""

labware_id: str
location: DeckSlotLocation
location: LabwareLocation
definition: LabwareDefinition


Expand Down Expand Up @@ -51,7 +58,9 @@ def sync() -> DeckDefinitionV5:

async def get_deck_fixed_labware(
self,
load_fixed_trash: bool,
deck_definition: DeckDefinitionV5,
deck_configuration: Optional[DeckConfigurationType] = None,
) -> List[DeckFixedLabware]:
"""Get a list of all labware fixtures from a given deck definition."""
labware: List[DeckFixedLabware] = []
Expand All @@ -61,8 +70,52 @@ async def get_deck_fixed_labware(
load_name = cast(Optional[str], fixture.get("labware"))
slot = cast(Optional[str], fixture.get("slot"))

if load_name is not None and slot is not None:
location = DeckSlotLocation(slotName=DeckSlotName.from_primitive(slot))
if (
deck_configuration is not None
and load_name is not None
and slot is not None
and slot not in DeckSlotName._value2member_map_
):
# The provided slot is likely to be an addressable area for Module-required labware Eg: Plate Reader Lid
for (
cutout_id,
cutout_fixture_id,
opentrons_module_serial_number,
) in deck_configuration:
provided_addressable_areas = (
deck_configuration_provider.get_provided_addressable_area_names(
cutout_fixture_id=cutout_fixture_id,
cutout_id=cutout_id,
deck_definition=deck_definition,
)
)
if slot in provided_addressable_areas:
addressable_area_location = AddressableAreaLocation(
addressableAreaName=slot
)
definition = await self._labware_data.get_labware_definition(
load_name=load_name,
namespace="opentrons",
version=1,
)

labware.append(
DeckFixedLabware(
labware_id=labware_id,
definition=definition,
location=addressable_area_location,
)
)

elif (
load_fixed_trash
and load_name is not None
and slot is not None
and slot in DeckSlotName._value2member_map_
):
deck_slot_location = DeckSlotLocation(
slotName=DeckSlotName.from_primitive(slot)
)
definition = await self._labware_data.get_labware_definition(
load_name=load_name,
namespace="opentrons",
Expand All @@ -73,7 +126,7 @@ async def get_deck_fixed_labware(
DeckFixedLabware(
labware_id=labware_id,
definition=definition,
location=location,
location=deck_slot_location,
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@ def is_deck_slot(addressable_area_name: str) -> bool:
return True


def is_abs_reader_lid_dock(addressable_area_name: str) -> bool:
"""Check if an addressable area is an absorbance plate reader lid dock."""
return "absorbanceReaderV1LidDock" in addressable_area_name
def is_abs_reader(addressable_area_name: str) -> bool:
"""Check if an addressable area is an absorbance plate reader area."""
return "absorbanceReaderV1" in addressable_area_name
66 changes: 44 additions & 22 deletions api/src/opentrons/protocol_engine/state/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from opentrons.protocol_engine.commands.calibration.calibrate_module import (
CalibrateModuleResult,
)
from opentrons.types import DeckSlotName, MountType, StagingSlotName
from opentrons.types import DeckSlotName, MountType
from ..errors import ModuleNotConnectedError

from ..types import (
Expand All @@ -48,6 +48,8 @@
LabwareMovementOffsetData,
AddressableAreaLocation,
)

from ..resources import DeckFixedLabware
from .addressable_areas import AddressableAreaView
from .. import errors
from ..commands import (
Expand Down Expand Up @@ -181,6 +183,15 @@ class ModuleState:
deck_type: DeckType
"""Type of deck that the modules are on."""

deck_fixed_labware: Sequence[DeckFixedLabware]
"""Fixed labware from the deck which may be assigned to a module.
The Opentrons Plate Reader module makes use of an electronic Lid labware which moves
between the Reader and Dock positions, and is pre-loaded into the engine as to persist
even when not in use. For this reason, we inject it here when an appropriate match
is identified.
"""


class ModuleStore(HasState[ModuleState], HandlesActions):
"""Module state container."""
Expand All @@ -190,6 +201,7 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
def __init__(
self,
config: Config,
deck_fixed_labware: Sequence[DeckFixedLabware],
module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None,
) -> None:
"""Initialize a ModuleStore and its state."""
Expand All @@ -201,6 +213,7 @@ def __init__(
substate_by_module_id={},
module_offset_by_serial=module_calibration_offsets or {},
deck_type=config.deck_type,
deck_fixed_labware=deck_fixed_labware,
)
self._robot_type = config.robot_type

Expand Down Expand Up @@ -310,7 +323,7 @@ def _update_absorbance_reader_lid_id(
lid_id=lid_id,
)

def _add_module_substate(
def _add_module_substate( # noqa: C901
self,
module_id: str,
serial_number: Optional[str],
Expand Down Expand Up @@ -369,15 +382,29 @@ def _add_module_substate(
module_id=MagneticBlockId(module_id)
)
elif ModuleModel.is_absorbance_reader(actual_model):
self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState(
module_id=AbsorbanceReaderId(module_id),
configured=False,
measured=False,
is_lid_on=False,
data=None,
configured_wavelength=None,
lid_id=None,
)
slot = self._state.slot_by_module_id[module_id]
if slot is not None:
reader_addressable_area = f"absorbanceReaderV1{slot.value}"
lid_labware_id = None
for labware in self._state.deck_fixed_labware:
if labware.location == AddressableAreaLocation(
addressableAreaName=reader_addressable_area
):
lid_labware_id = labware.labware_id
break
self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState(
module_id=AbsorbanceReaderId(module_id),
configured=False,
measured=False,
is_lid_on=True,
data=None,
configured_wavelength=None,
lid_id=lid_labware_id,
)
else:
raise errors.ModuleNotOnDeckError(
"Opentrons Plate Reader location did not return a valid Deck Slot."
)

def _update_additional_slots_occupied_by_thermocycler(
self,
Expand Down Expand Up @@ -1292,14 +1319,9 @@ def absorbance_reader_dock_location(
) -> AddressableAreaLocation:
"""Get the addressable area for the absorbance reader dock."""
reader_slot = self.get_location(module_id)
lid_dock_slot = get_adjacent_staging_slot(reader_slot.slotName)
assert lid_dock_slot is not None

return AddressableAreaLocation(addressableAreaName=lid_dock_slot.id)

def absorbance_reader_dock_location_name(self, module_id: str) -> StagingSlotName:
"""Get the addressable area for the absorbance reader dock."""
reader_slot = self.get_location(module_id)
lid_dock_slot = get_adjacent_staging_slot(reader_slot.slotName)
assert lid_dock_slot is not None
return lid_dock_slot
lid_doc_slot = get_adjacent_staging_slot(reader_slot.slotName)
assert lid_doc_slot is not None
lid_dock_area = AddressableAreaLocation(
addressableAreaName="absorbanceReaderV1LidDock" + lid_doc_slot.value
)
return lid_dock_area
1 change: 1 addition & 0 deletions api/src/opentrons/protocol_engine/state/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ def __init__(
)
self._module_store = ModuleStore(
config=config,
deck_fixed_labware=deck_fixed_labware,
module_calibration_offsets=module_calibration_offsets,
)
self._liquid_store = LiquidStore()
Expand Down
2 changes: 1 addition & 1 deletion api/src/opentrons/protocols/api_support/definitions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .types import APIVersion

MAX_SUPPORTED_VERSION = APIVersion(2, 20)
MAX_SUPPORTED_VERSION = APIVersion(2, 21)
"""The maximum supported protocol API version in this release."""

MIN_SUPPORTED_VERSION = APIVersion(2, 0)
Expand Down
Loading

0 comments on commit 6371d4d

Please sign in to comment.