Skip to content

Commit 5e9955b

Browse files
authored
feat(api, shared-data): Expand Labware architecture to accommodate Lids (#17072)
Covers EXEC-1000, EXEC-1001, EXEC-1002, EXEC-1004 For API 2.23 remove Lids as a handled labware concept in PAPI and treat them more as an attribute with new load commands with new Engine commands
1 parent f04b221 commit 5e9955b

File tree

31 files changed

+1555
-34
lines changed

31 files changed

+1555
-34
lines changed

api/src/opentrons/protocol_api/core/engine/protocol.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ def load_labware(
233233
)
234234
# FIXME(jbl, 2023-08-14) validating after loading the object issue
235235
validation.ensure_definition_is_labware(load_result.definition)
236+
validation.ensure_definition_is_not_lid_after_api_version(
237+
self.api_version, load_result.definition
238+
)
236239

237240
# FIXME(mm, 2023-02-21):
238241
#
@@ -322,6 +325,52 @@ def load_adapter(
322325

323326
return labware_core
324327

328+
def load_lid(
329+
self,
330+
load_name: str,
331+
location: LabwareCore,
332+
namespace: Optional[str],
333+
version: Optional[int],
334+
) -> LabwareCore:
335+
"""Load an individual lid using its identifying parameters. Must be loaded on an existing Labware."""
336+
load_location = self._convert_labware_location(location=location)
337+
custom_labware_params = (
338+
self._engine_client.state.labware.find_custom_labware_load_params()
339+
)
340+
namespace, version = load_labware_params.resolve(
341+
load_name, namespace, version, custom_labware_params
342+
)
343+
load_result = self._engine_client.execute_command_without_recovery(
344+
cmd.LoadLidParams(
345+
loadName=load_name,
346+
location=load_location,
347+
namespace=namespace,
348+
version=version,
349+
)
350+
)
351+
# FIXME(chb, 2024-12-06) validating after loading the object issue
352+
validation.ensure_definition_is_lid(load_result.definition)
353+
354+
deck_conflict.check(
355+
engine_state=self._engine_client.state,
356+
new_labware_id=load_result.labwareId,
357+
existing_disposal_locations=self._disposal_locations,
358+
# TODO: We can now fetch these IDs from engine too.
359+
# See comment in self.load_labware().
360+
#
361+
# Wrapping .keys() in list() is just to make Decoy verification easier.
362+
existing_labware_ids=list(self._labware_cores_by_id.keys()),
363+
existing_module_ids=list(self._module_cores_by_id.keys()),
364+
)
365+
366+
labware_core = LabwareCore(
367+
labware_id=load_result.labwareId,
368+
engine_client=self._engine_client,
369+
)
370+
371+
self._labware_cores_by_id[labware_core.labware_id] = labware_core
372+
return labware_core
373+
325374
def move_labware(
326375
self,
327376
labware_core: LabwareCore,
@@ -644,6 +693,72 @@ def set_last_location(
644693
self._last_location = location
645694
self._last_mount = mount
646695

696+
def load_lid_stack(
697+
self,
698+
load_name: str,
699+
location: Union[DeckSlotName, StagingSlotName, LabwareCore],
700+
quantity: int,
701+
namespace: Optional[str],
702+
version: Optional[int],
703+
) -> LabwareCore:
704+
"""Load a Stack of Lids to a given location, creating a Lid Stack."""
705+
if quantity < 1:
706+
raise ValueError(
707+
"When loading a lid stack quantity cannot be less than one."
708+
)
709+
if isinstance(location, DeckSlotName) or isinstance(location, StagingSlotName):
710+
load_location = self._convert_labware_location(location=location)
711+
else:
712+
if isinstance(location, LabwareCore):
713+
load_location = self._convert_labware_location(location=location)
714+
else:
715+
raise ValueError(
716+
"Expected type of Labware Location for lid stack must be Labware, not Legacy Labware or Well."
717+
)
718+
719+
custom_labware_params = (
720+
self._engine_client.state.labware.find_custom_labware_load_params()
721+
)
722+
namespace, version = load_labware_params.resolve(
723+
load_name, namespace, version, custom_labware_params
724+
)
725+
726+
load_result = self._engine_client.execute_command_without_recovery(
727+
cmd.LoadLidStackParams(
728+
loadName=load_name,
729+
location=load_location,
730+
namespace=namespace,
731+
version=version,
732+
quantity=quantity,
733+
)
734+
)
735+
736+
# FIXME(CHB, 2024-12-04) just like load labware and load adapter we have a validating after loading the object issue
737+
validation.ensure_definition_is_lid(load_result.definition)
738+
739+
deck_conflict.check(
740+
engine_state=self._engine_client.state,
741+
new_labware_id=load_result.stackLabwareId,
742+
existing_disposal_locations=self._disposal_locations,
743+
# TODO (spp, 2023-11-27): We've been using IDs from _labware_cores_by_id
744+
# and _module_cores_by_id instead of getting the lists directly from engine
745+
# because of the chance of engine carrying labware IDs from LPC too.
746+
# But with https://github.com/Opentrons/opentrons/pull/13943,
747+
# & LPC in maintenance runs, we can now rely on engine state for these IDs too.
748+
# Wrapping .keys() in list() is just to make Decoy verification easier.
749+
existing_labware_ids=list(self._labware_cores_by_id.keys()),
750+
existing_module_ids=list(self._module_cores_by_id.keys()),
751+
)
752+
753+
labware_core = LabwareCore(
754+
labware_id=load_result.stackLabwareId,
755+
engine_client=self._engine_client,
756+
)
757+
758+
self._labware_cores_by_id[labware_core.labware_id] = labware_core
759+
760+
return labware_core
761+
647762
def get_deck_definition(self) -> DeckDefinitionV5:
648763
"""Get the geometry definition of the robot's deck."""
649764
return self._engine_client.state.labware.get_deck_definition()

api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
from opentrons_shared_data.pipette.types import PipetteNameType
77
from opentrons_shared_data.robot.types import RobotType
88

9-
from opentrons.types import DeckSlotName, StagingSlotName, Location, Mount, Point
9+
from opentrons.types import (
10+
DeckSlotName,
11+
StagingSlotName,
12+
Location,
13+
Mount,
14+
Point,
15+
)
1016
from opentrons.util.broker import Broker
1117
from opentrons.hardware_control import SyncHardwareAPI
1218
from opentrons.hardware_control.modules import AbstractModule, ModuleModel, ModuleType
@@ -267,6 +273,16 @@ def load_adapter(
267273
"""Load an adapter using its identifying parameters"""
268274
raise APIVersionError(api_element="Loading adapter")
269275

276+
def load_lid(
277+
self,
278+
load_name: str,
279+
location: LegacyLabwareCore,
280+
namespace: Optional[str],
281+
version: Optional[int],
282+
) -> LegacyLabwareCore:
283+
"""Load an individual lid labware using its identifying parameters. Must be loaded on a labware."""
284+
raise APIVersionError(api_element="Loading lid")
285+
270286
def load_robot(self) -> None: # type: ignore
271287
"""Load an adapter using its identifying parameters"""
272288
raise APIVersionError(api_element="Loading robot")
@@ -478,6 +494,17 @@ def set_last_location(
478494
self._last_location = location
479495
self._last_mount = mount
480496

497+
def load_lid_stack(
498+
self,
499+
load_name: str,
500+
location: Union[DeckSlotName, StagingSlotName, LegacyLabwareCore],
501+
quantity: int,
502+
namespace: Optional[str],
503+
version: Optional[int],
504+
) -> LegacyLabwareCore:
505+
"""Load a Stack of Lids to a given location, creating a Lid Stack."""
506+
raise APIVersionError(api_element="Lid stack")
507+
481508
def get_module_cores(self) -> List[legacy_module_core.LegacyModuleCore]:
482509
"""Get loaded module cores."""
483510
return self._module_cores

api/src/opentrons/protocol_api/core/protocol.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010
from opentrons_shared_data.labware.types import LabwareDefinition
1111
from opentrons_shared_data.robot.types import RobotType
1212

13-
from opentrons.types import DeckSlotName, StagingSlotName, Location, Mount, Point
13+
from opentrons.types import (
14+
DeckSlotName,
15+
StagingSlotName,
16+
Location,
17+
Mount,
18+
Point,
19+
)
1420
from opentrons.hardware_control import SyncHardwareAPI
1521
from opentrons.hardware_control.modules.types import ModuleModel
1622
from opentrons.protocols.api_support.util import AxisMaxSpeeds
@@ -94,6 +100,17 @@ def load_adapter(
94100
"""Load an adapter using its identifying parameters"""
95101
...
96102

103+
@abstractmethod
104+
def load_lid(
105+
self,
106+
load_name: str,
107+
location: LabwareCoreType,
108+
namespace: Optional[str],
109+
version: Optional[int],
110+
) -> LabwareCoreType:
111+
"""Load an individual lid labware using its identifying parameters. Must be loaded on a labware."""
112+
...
113+
97114
@abstractmethod
98115
def move_labware(
99116
self,
@@ -191,6 +208,17 @@ def set_last_location(
191208
) -> None:
192209
...
193210

211+
@abstractmethod
212+
def load_lid_stack(
213+
self,
214+
load_name: str,
215+
location: Union[DeckSlotName, StagingSlotName, LabwareCoreType],
216+
quantity: int,
217+
namespace: Optional[str],
218+
version: Optional[int],
219+
) -> LabwareCoreType:
220+
...
221+
194222
@abstractmethod
195223
def get_deck_definition(self) -> DeckDefinitionV5:
196224
"""Get the geometry definition of the robot's deck."""

api/src/opentrons/protocol_api/labware.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@ def load_labware(
544544
self,
545545
name: str,
546546
label: Optional[str] = None,
547+
lid: Optional[str] = None,
547548
namespace: Optional[str] = None,
548549
version: Optional[int] = None,
549550
) -> Labware:
@@ -573,6 +574,20 @@ def load_labware(
573574

574575
self._core_map.add(labware_core, labware)
575576

577+
if lid is not None:
578+
if self._api_version < validation.LID_STACK_VERSION_GATE:
579+
raise APIVersionError(
580+
api_element="Loading a Lid on a Labware",
581+
until_version="2.23",
582+
current_version=f"{self._api_version}",
583+
)
584+
self._protocol_core.load_lid(
585+
load_name=lid,
586+
location=labware_core,
587+
namespace=namespace,
588+
version=version,
589+
)
590+
576591
return labware
577592

578593
@requires_version(2, 15)
@@ -597,6 +612,65 @@ def load_labware_from_definition(
597612
label=label,
598613
)
599614

615+
@requires_version(2, 23)
616+
def load_lid_stack(
617+
self,
618+
load_name: str,
619+
quantity: int,
620+
namespace: Optional[str] = None,
621+
version: Optional[int] = None,
622+
) -> Labware:
623+
"""
624+
Load a stack of Lids onto a valid Deck Location or Adapter.
625+
626+
:param str load_name: A string to use for looking up a lid definition.
627+
You can find the ``load_name`` for any standard lid on the Opentrons
628+
`Labware Library <https://labware.opentrons.com>`_.
629+
:param int quantity: The quantity of lids to be loaded in the stack.
630+
:param str namespace: The namespace that the lid labware definition belongs to.
631+
If unspecified, the API will automatically search two namespaces:
632+
633+
- ``"opentrons"``, to load standard Opentrons labware definitions.
634+
- ``"custom_beta"``, to load custom labware definitions created with the
635+
`Custom Labware Creator <https://labware.opentrons.com/create>`__.
636+
637+
You might need to specify an explicit ``namespace`` if you have a custom
638+
definition whose ``load_name`` is the same as an Opentrons-verified
639+
definition, and you want to explicitly choose one or the other.
640+
641+
:param version: The version of the labware definition. You should normally
642+
leave this unspecified to let ``load_lid_stack()`` choose a version
643+
automatically.
644+
645+
:return: The initialized and loaded labware object representing the Lid Stack.
646+
"""
647+
if self._api_version < validation.LID_STACK_VERSION_GATE:
648+
raise APIVersionError(
649+
api_element="Loading a Lid Stack",
650+
until_version="2.23",
651+
current_version=f"{self._api_version}",
652+
)
653+
654+
load_location = self._core
655+
656+
load_name = validation.ensure_lowercase_name(load_name)
657+
658+
result = self._protocol_core.load_lid_stack(
659+
load_name=load_name,
660+
location=load_location,
661+
quantity=quantity,
662+
namespace=namespace,
663+
version=version,
664+
)
665+
666+
labware = Labware(
667+
core=result,
668+
api_version=self._api_version,
669+
protocol_core=self._protocol_core,
670+
core_map=self._core_map,
671+
)
672+
return labware
673+
600674
def set_calibration(self, delta: Point) -> None:
601675
"""
602676
An internal, deprecated method used for updating the labware offset.

api/src/opentrons/protocol_api/module_contexts.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ def load_labware(
125125
namespace: Optional[str] = None,
126126
version: Optional[int] = None,
127127
adapter: Optional[str] = None,
128+
lid: Optional[str] = None,
128129
) -> Labware:
129130
"""Load a labware onto the module using its load parameters.
130131
@@ -180,6 +181,19 @@ def load_labware(
180181
version=version,
181182
location=load_location,
182183
)
184+
if lid is not None:
185+
if self._api_version < validation.LID_STACK_VERSION_GATE:
186+
raise APIVersionError(
187+
api_element="Loading a lid on a Labware",
188+
until_version="2.23",
189+
current_version=f"{self._api_version}",
190+
)
191+
self._protocol_core.load_lid(
192+
load_name=lid,
193+
location=labware_core,
194+
namespace=namespace,
195+
version=version,
196+
)
183197

184198
if isinstance(self._core, LegacyModuleCore):
185199
labware = self._core.add_labware_core(cast(LegacyLabwareCore, labware_core))

0 commit comments

Comments
 (0)