-
Notifications
You must be signed in to change notification settings - Fork 179
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(api): location sequences for offsets
When we load and express labware offsets, we previously used an object that had explicit named members for a labware parent; a module parent; and a deck slot. This worked as long as we don't have heterogenous labware stacking (to include labware-on-labware-on-adapter) but it's inflexible, and maybe we do want that. Instead, let's have a sequence of little models that specify a parent geometry - a labware def uri, a module model, or an addressable area name - and use that both for specifying offset locations via the HTTP API, for comparing locations internally, and for downloading locations from the HTTP API. Importantly, these offset locations are currently in the database as parts of runs, so we have to keep the old version around when expressing labware data; this is annoying, but that's life. We can calculate the equivalent old data when we load a labware offset pretty easily, and this also lets us not change the legacy protocol core. We also are going to keep the old method of specifying them around, and convert them into the new format, which is also pretty easy, to preserve backwards compatibility and roundtripping older offsets from before this data was present.
- Loading branch information
Showing
31 changed files
with
1,747 additions
and
290 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
172 changes: 172 additions & 0 deletions
172
api/src/opentrons/protocol_engine/labware_offset_standardization.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
"""Convert labware offset creation requests and stored elements between legacy and new.""" | ||
|
||
from opentrons_shared_data.robot.types import RobotType | ||
from opentrons_shared_data.deck.types import DeckDefinitionV5 | ||
from .errors import ( | ||
OffsetLocationInvalidError, | ||
FixtureDoesNotExistError, | ||
) | ||
from .types import ( | ||
LabwareOffsetCreate, | ||
LegacyLabwareOffsetCreate, | ||
LabwareOffsetCreateInternal, | ||
LegacyLabwareOffsetLocation, | ||
LabwareOffsetLocationSequence, | ||
OnLabwareOffsetLocationSequenceComponent, | ||
OnAddressableAreaOffsetLocationSequenceComponent, | ||
OnModuleOffsetLocationSequenceComponent, | ||
ModuleModel, | ||
) | ||
from .resources import deck_configuration_provider | ||
|
||
|
||
def standardize_labware_offset_create( | ||
request: LabwareOffsetCreate | LegacyLabwareOffsetCreate, | ||
robot_type: RobotType, | ||
deck_definition: DeckDefinitionV5, | ||
) -> LabwareOffsetCreateInternal: | ||
"""Turn a union of old and new labware offset create requests into a new one.""" | ||
location_sequence, legacy_location = _locations_for_create( | ||
request, robot_type, deck_definition | ||
) | ||
return LabwareOffsetCreateInternal( | ||
definitionUri=request.definitionUri, | ||
locationSequence=location_sequence, | ||
legacyLocation=legacy_location, | ||
vector=request.vector, | ||
) | ||
|
||
|
||
def _legacy_offset_location_to_offset_location_sequence( | ||
location: LegacyLabwareOffsetLocation, deck_definition: DeckDefinitionV5 | ||
) -> LabwareOffsetLocationSequence: | ||
sequence: LabwareOffsetLocationSequence = [] | ||
if location.definitionUri: | ||
sequence.append( | ||
OnLabwareOffsetLocationSequenceComponent(labwareUri=location.definitionUri) | ||
) | ||
if location.moduleModel: | ||
sequence.append( | ||
OnModuleOffsetLocationSequenceComponent(moduleModel=location.moduleModel) | ||
) | ||
cutout_id = deck_configuration_provider.get_cutout_id_by_deck_slot_name( | ||
location.slotName | ||
) | ||
possible_cutout_fixture_id = location.moduleModel.value | ||
try: | ||
addressable_area = deck_configuration_provider.get_labware_hosting_addressable_area_name_for_cutout_and_cutout_fixture( | ||
cutout_id, possible_cutout_fixture_id, deck_definition | ||
) | ||
sequence.append( | ||
OnAddressableAreaOffsetLocationSequenceComponent( | ||
addressableAreaName=addressable_area | ||
) | ||
) | ||
except FixtureDoesNotExistError: | ||
# this is an OT-2 (or this module isn't supported in the deck definition) and we should use a | ||
# slot addressable area name | ||
sequence.append( | ||
OnAddressableAreaOffsetLocationSequenceComponent( | ||
addressableAreaName=location.slotName.value | ||
) | ||
) | ||
|
||
else: | ||
# Slight hack: we should have a more formal association here. However, since the slot | ||
# name is already standardized, and since the addressable areas for slots are just the | ||
# name of the slots, we can rely on this. | ||
sequence.append( | ||
OnAddressableAreaOffsetLocationSequenceComponent( | ||
addressableAreaName=location.slotName.value | ||
) | ||
) | ||
return sequence | ||
|
||
|
||
def _offset_location_sequence_head_to_labware_and_module( | ||
location_sequence: LabwareOffsetLocationSequence, deck_definition: DeckDefinitionV5 | ||
) -> tuple[ModuleModel | None, str | None]: | ||
labware_uri: str | None = None | ||
module_model: ModuleModel | None = None | ||
for location in location_sequence: | ||
if isinstance(location, OnAddressableAreaOffsetLocationSequenceComponent): | ||
raise OffsetLocationInvalidError( | ||
"Addressable areas may only be the final element of an offset location." | ||
) | ||
elif isinstance(location, OnLabwareOffsetLocationSequenceComponent): | ||
if labware_uri is not None: | ||
# We only take the first location | ||
continue | ||
if module_model is not None: | ||
# Labware can't be underneath modules | ||
raise OffsetLocationInvalidError( | ||
"Labware must not be underneath a module." | ||
) | ||
labware_uri = location.labwareUri | ||
elif isinstance(location, OnModuleOffsetLocationSequenceComponent): | ||
if module_model is not None: | ||
# Bad, somebody put more than one module in here | ||
raise OffsetLocationInvalidError( | ||
"Only one module location may exist in an offset location." | ||
) | ||
module_model = location.moduleModel | ||
else: | ||
raise OffsetLocationInvalidError( | ||
f"Invalid location component in offset location: {repr(location)}" | ||
) | ||
return module_model, labware_uri | ||
|
||
|
||
def _offset_location_sequence_to_legacy_offset_location( | ||
location_sequence: LabwareOffsetLocationSequence, deck_definition: DeckDefinitionV5 | ||
) -> LegacyLabwareOffsetLocation: | ||
if len(location_sequence) == 0: | ||
raise OffsetLocationInvalidError( | ||
"Offset locations must contain at least one component." | ||
) | ||
last_element = location_sequence[-1] | ||
if not isinstance(last_element, OnAddressableAreaOffsetLocationSequenceComponent): | ||
raise OffsetLocationInvalidError( | ||
"Offset locations must end with an addressable area." | ||
) | ||
module_model, labware_uri = _offset_location_sequence_head_to_labware_and_module( | ||
location_sequence[:-1], deck_definition | ||
) | ||
( | ||
cutout_id, | ||
cutout_fixtures, | ||
) = deck_configuration_provider.get_potential_cutout_fixtures( | ||
last_element.addressableAreaName, deck_definition | ||
) | ||
slot_name = deck_configuration_provider.get_deck_slot_for_cutout_id(cutout_id) | ||
return LegacyLabwareOffsetLocation( | ||
slotName=slot_name, moduleModel=module_model, definitionUri=labware_uri | ||
) | ||
|
||
|
||
def _locations_for_create( | ||
request: LabwareOffsetCreate | LegacyLabwareOffsetCreate, | ||
robot_type: RobotType, | ||
deck_definition: DeckDefinitionV5, | ||
) -> tuple[LabwareOffsetLocationSequence, LegacyLabwareOffsetLocation]: | ||
if isinstance(request, LabwareOffsetCreate): | ||
return ( | ||
request.locationSequence, | ||
_offset_location_sequence_to_legacy_offset_location( | ||
request.locationSequence, deck_definition | ||
), | ||
) | ||
else: | ||
normalized = request.location.model_copy( | ||
update={ | ||
"slotName": request.location.slotName.to_equivalent_for_robot_type( | ||
robot_type | ||
) | ||
} | ||
) | ||
return ( | ||
_legacy_offset_location_to_offset_location_sequence( | ||
normalized, deck_definition | ||
), | ||
normalized, | ||
) |
Oops, something went wrong.