opentrons 8.2.0a0__py2.py3-none-any.whl → 8.2.0a1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. opentrons/drivers/absorbance_reader/async_byonoy.py +3 -3
  2. opentrons/legacy_commands/helpers.py +8 -2
  3. opentrons/protocol_api/core/engine/labware.py +10 -2
  4. opentrons/protocol_api/core/engine/module_core.py +38 -1
  5. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +12 -5
  6. opentrons/protocol_api/core/engine/protocol.py +5 -30
  7. opentrons/protocol_api/core/labware.py +4 -0
  8. opentrons/protocol_api/core/legacy/legacy_labware_core.py +5 -0
  9. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -0
  10. opentrons/protocol_api/core/protocol.py +1 -0
  11. opentrons/protocol_api/module_contexts.py +13 -0
  12. opentrons/protocol_api/protocol_context.py +12 -2
  13. opentrons/protocol_engine/actions/__init__.py +0 -2
  14. opentrons/protocol_engine/actions/actions.py +0 -12
  15. opentrons/protocol_engine/clients/sync_client.py +0 -6
  16. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +18 -31
  17. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +17 -29
  18. opentrons/protocol_engine/commands/load_labware.py +9 -0
  19. opentrons/protocol_engine/commands/load_module.py +0 -39
  20. opentrons/protocol_engine/commands/move_labware.py +49 -4
  21. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +41 -32
  22. opentrons/protocol_engine/create_protocol_engine.py +18 -1
  23. opentrons/protocol_engine/execution/labware_movement.py +69 -21
  24. opentrons/protocol_engine/execution/movement.py +9 -4
  25. opentrons/protocol_engine/protocol_engine.py +0 -7
  26. opentrons/protocol_engine/resources/deck_data_provider.py +0 -39
  27. opentrons/protocol_engine/resources/file_provider.py +11 -7
  28. opentrons/protocol_engine/resources/fixture_validation.py +6 -1
  29. opentrons/protocol_engine/state/geometry.py +91 -49
  30. opentrons/protocol_engine/state/labware.py +102 -25
  31. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +3 -1
  32. opentrons/protocol_engine/state/modules.py +49 -79
  33. opentrons/protocol_engine/state/motion.py +17 -5
  34. opentrons/protocol_engine/state/update_types.py +16 -0
  35. opentrons/util/logging_config.py +1 -1
  36. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a1.dist-info}/METADATA +4 -4
  37. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a1.dist-info}/RECORD +41 -41
  38. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a1.dist-info}/LICENSE +0 -0
  39. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a1.dist-info}/WHEEL +0 -0
  40. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a1.dist-info}/entry_points.txt +0 -0
  41. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a1.dist-info}/top_level.txt +0 -0
@@ -24,7 +24,7 @@ from opentrons.hardware_control.modules.errors import AbsorbanceReaderDisconnect
24
24
 
25
25
  SN_PARSER = re.compile(r'ATTRS{serial}=="(?P<serial>.+?)"')
26
26
  VERSION_PARSER = re.compile(r"Absorbance (?P<version>V\d+\.\d+\.\d+)")
27
- SERIAL_PARSER = re.compile(r"(?P<serial>BYO[A-Z]{3}[0-9]{5})")
27
+ SERIAL_PARSER = re.compile(r"(?P<serial>(OPT|BYO)[A-Z]{3}[0-9]+)")
28
28
 
29
29
 
30
30
  class AsyncByonoy:
@@ -156,9 +156,9 @@ class AsyncByonoy:
156
156
  func=partial(self._interface.get_device_information, handle),
157
157
  )
158
158
  self._raise_if_error(err.name, f"Error getting device information: {err}")
159
- serial_match = SERIAL_PARSER.match(device_info.sn)
159
+ serial_match = SERIAL_PARSER.fullmatch(device_info.sn)
160
160
  version_match = VERSION_PARSER.match(device_info.version)
161
- serial = serial_match["serial"] if serial_match else "BYOMAA00000"
161
+ serial = serial_match["serial"].strip() if serial_match else "OPTMAA00000"
162
162
  version = version_match["version"].lower() if version_match else "v0.0.0"
163
163
  info = {
164
164
  "serial": serial,
@@ -49,7 +49,9 @@ def stringify_disposal_location(location: Union[TrashBin, WasteChute]) -> str:
49
49
 
50
50
 
51
51
  def _stringify_labware_movement_location(
52
- location: Union[DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute]
52
+ location: Union[
53
+ DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute, TrashBin
54
+ ]
53
55
  ) -> str:
54
56
  if isinstance(location, (int, str)):
55
57
  return f"slot {location}"
@@ -61,11 +63,15 @@ def _stringify_labware_movement_location(
61
63
  return str(location)
62
64
  elif isinstance(location, WasteChute):
63
65
  return "Waste Chute"
66
+ elif isinstance(location, TrashBin):
67
+ return "Trash Bin " + location.location.name
64
68
 
65
69
 
66
70
  def stringify_labware_movement_command(
67
71
  source_labware: Labware,
68
- destination: Union[DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute],
72
+ destination: Union[
73
+ DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute, TrashBin
74
+ ],
69
75
  use_gripper: bool,
70
76
  ) -> str:
71
77
  source_labware_text = _stringify_labware_movement_location(source_labware)
@@ -19,7 +19,7 @@ from opentrons.protocol_engine.types import (
19
19
  LabwareOffsetCreate,
20
20
  LabwareOffsetVector,
21
21
  )
22
- from opentrons.types import DeckSlotName, Point
22
+ from opentrons.types import DeckSlotName, Point, StagingSlotName
23
23
  from opentrons.hardware_control.nozzle_manager import NozzleMap
24
24
 
25
25
 
@@ -139,6 +139,10 @@ class LabwareCore(AbstractLabware[WellCore]):
139
139
  """Whether the labware is an adapter."""
140
140
  return LabwareRole.adapter in self._definition.allowedRoles
141
141
 
142
+ def is_lid(self) -> bool:
143
+ """Whether the labware is a lid."""
144
+ return LabwareRole.lid in self._definition.allowedRoles
145
+
142
146
  def is_fixed_trash(self) -> bool:
143
147
  """Whether the labware is a fixed trash."""
144
148
  return self._engine_client.state.labware.is_fixed_trash(
@@ -186,9 +190,13 @@ class LabwareCore(AbstractLabware[WellCore]):
186
190
  def get_deck_slot(self) -> Optional[DeckSlotName]:
187
191
  """Get the deck slot the labware is in, if on deck."""
188
192
  try:
189
- return self._engine_client.state.geometry.get_ancestor_slot_name(
193
+ ancestor = self._engine_client.state.geometry.get_ancestor_slot_name(
190
194
  self.labware_id
191
195
  )
196
+ if isinstance(ancestor, StagingSlotName):
197
+ # The only use case for get_deck_slot is with a legacy OT-2 function which resolves to a numerical deck slot, so we can ignore staging area slots for now
198
+ return None
199
+ return ancestor
192
200
  except (
193
201
  LabwareNotOnDeckError,
194
202
  ModuleNotOnDeckError,
@@ -41,6 +41,11 @@ from ..module import (
41
41
  from .exceptions import InvalidMagnetEngageHeightError
42
42
 
43
43
 
44
+ # Valid wavelength range for absorbance reader
45
+ ABS_WAVELENGTH_MIN = 350
46
+ ABS_WAVELENGTH_MAX = 1000
47
+
48
+
44
49
  class ModuleCore(AbstractModuleCore):
45
50
  """Module core logic implementation for Python protocols.
46
51
  Args:
@@ -581,7 +586,39 @@ class AbsorbanceReaderCore(ModuleCore, AbstractAbsorbanceReaderCore):
581
586
  "Cannot perform Initialize action on Absorbance Reader without calling `.close_lid()` first."
582
587
  )
583
588
 
584
- # TODO: check that the wavelengths are within the supported wavelengths
589
+ wavelength_len = len(wavelengths)
590
+ if mode == "single" and wavelength_len != 1:
591
+ raise ValueError(
592
+ f"Single mode can only be initialized with 1 wavelength"
593
+ f" {wavelength_len} wavelengths provided instead."
594
+ )
595
+
596
+ if mode == "multi" and (wavelength_len < 1 or wavelength_len > 6):
597
+ raise ValueError(
598
+ f"Multi mode can only be initialized with 1 - 6 wavelengths."
599
+ f" {wavelength_len} wavelengths provided instead."
600
+ )
601
+
602
+ if reference_wavelength is not None and (
603
+ reference_wavelength < ABS_WAVELENGTH_MIN
604
+ or reference_wavelength > ABS_WAVELENGTH_MAX
605
+ ):
606
+ raise ValueError(
607
+ f"Unsupported reference wavelength: ({reference_wavelength}) needs"
608
+ f" to between {ABS_WAVELENGTH_MIN} and {ABS_WAVELENGTH_MAX} nm."
609
+ )
610
+
611
+ for wavelength in wavelengths:
612
+ if (
613
+ not isinstance(wavelength, int)
614
+ or wavelength < ABS_WAVELENGTH_MIN
615
+ or wavelength > ABS_WAVELENGTH_MAX
616
+ ):
617
+ raise ValueError(
618
+ f"Unsupported sample wavelength: ({wavelength}) needs"
619
+ f" to between {ABS_WAVELENGTH_MIN} and {ABS_WAVELENGTH_MAX} nm."
620
+ )
621
+
585
622
  self._engine_client.execute_command(
586
623
  cmd.absorbance_reader.InitializeParams(
587
624
  moduleId=self.module_id,
@@ -9,6 +9,7 @@ from typing import (
9
9
  )
10
10
 
11
11
  from opentrons_shared_data.errors.exceptions import MotionPlanningFailureError
12
+ from opentrons.protocol_engine.errors import LocationIsStagingSlotError
12
13
  from opentrons_shared_data.module import FLEX_TC_LID_COLLISION_ZONE
13
14
 
14
15
  from opentrons.hardware_control import CriticalPoint
@@ -63,7 +64,7 @@ _FLEX_TC_LID_FRONT_RIGHT_PT = Point(
63
64
  )
64
65
 
65
66
 
66
- def check_safe_for_pipette_movement(
67
+ def check_safe_for_pipette_movement( # noqa: C901
67
68
  engine_state: StateView,
68
69
  pipette_id: str,
69
70
  labware_id: str,
@@ -121,8 +122,12 @@ def check_safe_for_pipette_movement(
121
122
  f"Requested motion with the {primary_nozzle} nozzle partial configuration"
122
123
  f" is outside of robot bounds for the pipette."
123
124
  )
124
-
125
- labware_slot = engine_state.geometry.get_ancestor_slot_name(labware_id)
125
+ ancestor = engine_state.geometry.get_ancestor_slot_name(labware_id)
126
+ if isinstance(ancestor, StagingSlotName):
127
+ raise LocationIsStagingSlotError(
128
+ "Cannot perform pipette actions on labware in Staging Area Slot."
129
+ )
130
+ labware_slot = ancestor
126
131
 
127
132
  surrounding_slots = adjacent_slots_getters.get_surrounding_slots(
128
133
  slot=labware_slot.as_int(), robot_type=engine_state.config.robot_type
@@ -282,8 +287,10 @@ def check_safe_for_tip_pickup_and_return(
282
287
  is_96_ch_tiprack_adapter = engine_state.labware.get_has_quirk(
283
288
  labware_id=tiprack_parent.labwareId, quirk="tiprackAdapterFor96Channel"
284
289
  )
285
- tiprack_height = engine_state.labware.get_dimensions(labware_id).z
286
- adapter_height = engine_state.labware.get_dimensions(tiprack_parent.labwareId).z
290
+ tiprack_height = engine_state.labware.get_dimensions(labware_id=labware_id).z
291
+ adapter_height = engine_state.labware.get_dimensions(
292
+ labware_id=tiprack_parent.labwareId
293
+ ).z
287
294
  if is_partial_config and tiprack_height < adapter_height:
288
295
  raise PartialTipMovementNotAllowedError(
289
296
  f"{tiprack_name} cannot be on an adapter taller than the tip rack"
@@ -329,6 +329,7 @@ class ProtocolCore(
329
329
  NonConnectedModuleCore,
330
330
  OffDeckType,
331
331
  WasteChute,
332
+ TrashBin,
332
333
  ],
333
334
  use_gripper: bool,
334
335
  pause_for_manual_move: bool,
@@ -447,40 +448,10 @@ class ProtocolCore(
447
448
  existing_module_ids=list(self._module_cores_by_id.keys()),
448
449
  )
449
450
 
450
- # When the protocol engine is created, we add Module Lids as part of the deck fixed labware
451
- # If a valid module exists in the deck config. For analysis, we add the labware here since
452
- # deck fixed labware is not created under the same conditions. We also need to inject the Module
453
- # lids when the module isnt already on the deck config, like when adding a new
454
- # module during a protocol setup.
455
- self._load_virtual_module_lid(module_core)
456
-
457
451
  self._module_cores_by_id[module_core.module_id] = module_core
458
452
 
459
453
  return module_core
460
454
 
461
- def _load_virtual_module_lid(
462
- self, module_core: Union[ModuleCore, NonConnectedModuleCore]
463
- ) -> None:
464
- if isinstance(module_core, AbsorbanceReaderCore):
465
- substate = self._engine_client.state.modules.get_absorbance_reader_substate(
466
- module_core.module_id
467
- )
468
- if substate.lid_id is None:
469
- lid = self._engine_client.execute_command_without_recovery(
470
- cmd.LoadLabwareParams(
471
- loadName="opentrons_flex_lid_absorbance_plate_reader_module",
472
- location=ModuleLocation(moduleId=module_core.module_id),
473
- namespace="opentrons",
474
- version=1,
475
- displayName="Absorbance Reader Lid",
476
- )
477
- )
478
-
479
- self._engine_client.add_absorbance_reader_lid(
480
- module_id=module_core.module_id,
481
- lid_id=lid.labwareId,
482
- )
483
-
484
455
  def _create_non_connected_module_core(
485
456
  self, load_module_result: LoadModuleResult
486
457
  ) -> NonConnectedModuleCore:
@@ -807,6 +778,7 @@ class ProtocolCore(
807
778
  NonConnectedModuleCore,
808
779
  OffDeckType,
809
780
  WasteChute,
781
+ TrashBin,
810
782
  ],
811
783
  ) -> LabwareLocation:
812
784
  if isinstance(location, LabwareCore):
@@ -823,6 +795,7 @@ class ProtocolCore(
823
795
  NonConnectedModuleCore,
824
796
  OffDeckType,
825
797
  WasteChute,
798
+ TrashBin,
826
799
  ]
827
800
  ) -> NonStackedLocation:
828
801
  if isinstance(location, (ModuleCore, NonConnectedModuleCore)):
@@ -836,3 +809,5 @@ class ProtocolCore(
836
809
  elif isinstance(location, WasteChute):
837
810
  # TODO(mm, 2023-12-06) This will need to determine the appropriate Waste Chute to return, but only move_labware uses this for now
838
811
  return AddressableAreaLocation(addressableAreaName="gripperWasteChute")
812
+ elif isinstance(location, TrashBin):
813
+ return AddressableAreaLocation(addressableAreaName=location.area_name)
@@ -97,6 +97,10 @@ class AbstractLabware(ABC, Generic[WellCoreType]):
97
97
  def is_adapter(self) -> bool:
98
98
  """Whether the labware is an adapter."""
99
99
 
100
+ @abstractmethod
101
+ def is_lid(self) -> bool:
102
+ """Whether the labware is a lid."""
103
+
100
104
  @abstractmethod
101
105
  def is_fixed_trash(self) -> bool:
102
106
  """Whether the labware is a fixed trash."""
@@ -138,6 +138,11 @@ class LegacyLabwareCore(AbstractLabware[LegacyWellCore]):
138
138
  def is_adapter(self) -> bool:
139
139
  return False # Adapters were introduced in v2.15 and not supported in legacy protocols
140
140
 
141
+ def is_lid(self) -> bool:
142
+ return (
143
+ False # Lids were introduced in v2.21 and not supported in legacy protocols
144
+ )
145
+
141
146
  def is_fixed_trash(self) -> bool:
142
147
  """Whether the labware is fixed trash."""
143
148
  return "fixedTrash" in self.get_quirks()
@@ -277,6 +277,7 @@ class LegacyProtocolCore(
277
277
  legacy_module_core.LegacyModuleCore,
278
278
  OffDeckType,
279
279
  WasteChute,
280
+ TrashBin,
280
281
  ],
281
282
  use_gripper: bool,
282
283
  pause_for_manual_move: bool,
@@ -104,6 +104,7 @@ class AbstractProtocol(
104
104
  ModuleCoreType,
105
105
  OffDeckType,
106
106
  WasteChute,
107
+ TrashBin,
107
108
  ],
108
109
  use_gripper: bool,
109
110
  pause_for_manual_move: bool,
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  import logging
4
4
  from typing import List, Dict, Optional, Union, cast
5
5
 
6
+ from opentrons_shared_data.errors.exceptions import CommandPreconditionViolated
7
+
6
8
  from opentrons.protocol_engine.types import ABSMeasureMode
7
9
  from opentrons_shared_data.labware.types import LabwareDefinition
8
10
  from opentrons_shared_data.module.types import ModuleModel, ModuleType
@@ -159,7 +161,18 @@ class ModuleContext(CommandPublisher):
159
161
  load_location = loaded_adapter._core
160
162
  else:
161
163
  load_location = self._core
164
+
162
165
  name = validation.ensure_lowercase_name(name)
166
+
167
+ # todo(mm, 2024-11-08): This check belongs in opentrons.protocol_api.core.engine.deck_conflict.
168
+ # We're currently doing it here, at the ModuleContext level, for consistency with what
169
+ # ProtocolContext.load_labware() does. (It should also be moved to the deck_conflict module.)
170
+ if isinstance(self._core, AbsorbanceReaderCore):
171
+ if self._core.is_lid_on():
172
+ raise CommandPreconditionViolated(
173
+ f"Cannot load {name} onto the Absorbance Reader Module when its lid is closed."
174
+ )
175
+
163
176
  labware_core = self._protocol_core.load_labware(
164
177
  load_name=name,
165
178
  label=label,
@@ -45,6 +45,7 @@ from opentrons.protocols.api_support.util import (
45
45
  UnsupportedAPIError,
46
46
  )
47
47
  from opentrons_shared_data.errors.exceptions import CommandPreconditionViolated
48
+ from opentrons.protocol_engine.errors import LabwareMovementNotAllowedError
48
49
 
49
50
  from ._types import OffDeckType
50
51
  from .core.common import ModuleCore, LabwareCore, ProtocolCore
@@ -668,7 +669,7 @@ class ProtocolContext(CommandPublisher):
668
669
  self,
669
670
  labware: Labware,
670
671
  new_location: Union[
671
- DeckLocation, Labware, ModuleTypes, OffDeckType, WasteChute
672
+ DeckLocation, Labware, ModuleTypes, OffDeckType, WasteChute, TrashBin
672
673
  ],
673
674
  use_gripper: bool = False,
674
675
  pick_up_offset: Optional[Mapping[str, float]] = None,
@@ -713,7 +714,8 @@ class ProtocolContext(CommandPublisher):
713
714
  f"Expected labware of type 'Labware' but got {type(labware)}."
714
715
  )
715
716
 
716
- # Ensure that when moving to an absorbance reader than the lid is open
717
+ # Ensure that when moving to an absorbance reader that the lid is open
718
+ # todo(mm, 2024-11-08): Unify this with opentrons.protocol_api.core.engine.deck_conflict.
717
719
  if isinstance(new_location, AbsorbanceReaderContext):
718
720
  if new_location.is_lid_on():
719
721
  raise CommandPreconditionViolated(
@@ -727,11 +729,19 @@ class ProtocolContext(CommandPublisher):
727
729
  OffDeckType,
728
730
  DeckSlotName,
729
731
  StagingSlotName,
732
+ TrashBin,
730
733
  ]
731
734
  if isinstance(new_location, (Labware, ModuleContext)):
732
735
  location = new_location._core
733
736
  elif isinstance(new_location, (OffDeckType, WasteChute)):
734
737
  location = new_location
738
+ elif isinstance(new_location, TrashBin):
739
+ if labware._core.is_lid():
740
+ location = new_location
741
+ else:
742
+ raise LabwareMovementNotAllowedError(
743
+ "Can only dispose of tips and Lid-type labware in a Trash Bin. Did you mean to use a Waste Chute?"
744
+ )
735
745
  else:
736
746
  location = validation.ensure_and_convert_deck_slot(
737
747
  new_location, self._api_version, self._core.robot_type
@@ -28,7 +28,6 @@ from .actions import (
28
28
  DoorChangeAction,
29
29
  ResetTipsAction,
30
30
  SetPipetteMovementSpeedAction,
31
- AddAbsorbanceReaderLidAction,
32
31
  )
33
32
  from .get_state_update import get_state_updates
34
33
 
@@ -58,7 +57,6 @@ __all__ = [
58
57
  "DoorChangeAction",
59
58
  "ResetTipsAction",
60
59
  "SetPipetteMovementSpeedAction",
61
- "AddAbsorbanceReaderLidAction",
62
60
  # action payload values
63
61
  "PauseSource",
64
62
  "FinishErrorDetails",
@@ -271,17 +271,6 @@ class SetPipetteMovementSpeedAction:
271
271
  speed: Optional[float]
272
272
 
273
273
 
274
- @dataclasses.dataclass(frozen=True)
275
- class AddAbsorbanceReaderLidAction:
276
- """Add the absorbance reader lid id to the absorbance reader module substate.
277
-
278
- This action is dispatched the absorbance reader module is first loaded.
279
- """
280
-
281
- module_id: str
282
- lid_id: str
283
-
284
-
285
274
  @dataclasses.dataclass(frozen=True)
286
275
  class SetErrorRecoveryPolicyAction:
287
276
  """See `ProtocolEngine.set_error_recovery_policy()`."""
@@ -309,6 +298,5 @@ Action = Union[
309
298
  AddLiquidAction,
310
299
  ResetTipsAction,
311
300
  SetPipetteMovementSpeedAction,
312
- AddAbsorbanceReaderLidAction,
313
301
  SetErrorRecoveryPolicyAction,
314
302
  ]
@@ -119,12 +119,6 @@ class SyncClient:
119
119
  "add_addressable_area", addressable_area_name=addressable_area_name
120
120
  )
121
121
 
122
- def add_absorbance_reader_lid(self, module_id: str, lid_id: str) -> None:
123
- """Add an absorbance reader lid to the module state."""
124
- self._transport.call_method(
125
- "add_absorbance_reader_lid", module_id=module_id, lid_id=lid_id
126
- )
127
-
128
122
  def add_liquid(
129
123
  self, name: str, color: Optional[str], description: Optional[str]
130
124
  ) -> Liquid:
@@ -10,7 +10,6 @@ from ...errors.error_occurrence import ErrorOccurrence
10
10
  from ...errors import CannotPerformModuleAction
11
11
  from opentrons.protocol_engine.types import AddressableAreaLocation
12
12
 
13
- from opentrons.protocol_engine.resources import labware_validation
14
13
  from ...state.update_types import StateUpdate
15
14
 
16
15
 
@@ -53,41 +52,35 @@ class CloseLidImpl(AbstractCommandImpl[CloseLidParams, SuccessData[CloseLidResul
53
52
 
54
53
  async def execute(self, params: CloseLidParams) -> SuccessData[CloseLidResult]:
55
54
  """Execute the close lid command."""
55
+ state_update = StateUpdate()
56
56
  mod_substate = self._state_view.modules.get_absorbance_reader_substate(
57
57
  module_id=params.moduleId
58
58
  )
59
59
 
60
- # lid should currently be on the module
61
- assert mod_substate.lid_id is not None
62
- loaded_lid = self._state_view.labware.get(mod_substate.lid_id)
63
- assert labware_validation.is_absorbance_reader_lid(loaded_lid.loadName)
64
-
65
60
  hardware_lid_status = AbsorbanceReaderLidStatus.OFF
66
- # If the lid is closed, if the lid is open No-op out
67
61
  if not self._state_view.config.use_virtual_modules:
68
62
  abs_reader = self._equipment.get_module_hardware_api(mod_substate.module_id)
69
63
 
70
64
  if abs_reader is not None:
71
- result = await abs_reader.get_current_lid_status()
72
- hardware_lid_status = result
65
+ hardware_lid_status = await abs_reader.get_current_lid_status()
73
66
  else:
74
67
  raise CannotPerformModuleAction(
75
68
  "Could not reach the Hardware API for Opentrons Plate Reader Module."
76
69
  )
77
70
 
78
- # If the lid is already ON, no-op losing lid
79
71
  if hardware_lid_status is AbsorbanceReaderLidStatus.ON:
80
- # The lid is already On, so we can no-op and return the lids current location data
81
- assert isinstance(loaded_lid.location, AddressableAreaLocation)
82
- new_location = loaded_lid.location
83
- new_offset_id = self._equipment.find_applicable_labware_offset_id(
84
- labware_definition_uri=loaded_lid.definitionUri,
85
- labware_location=loaded_lid.location,
72
+ # The lid is already physically ON, so we can no-op physically closing it
73
+ state_update.set_absorbance_reader_lid(
74
+ module_id=mod_substate.module_id, is_lid_on=True
86
75
  )
87
76
  else:
88
77
  # Allow propagation of ModuleNotAttachedError.
89
78
  _ = self._equipment.get_module_hardware_api(mod_substate.module_id)
90
79
 
80
+ lid_definition = (
81
+ self._state_view.labware.get_absorbance_reader_lid_definition()
82
+ )
83
+
91
84
  current_location = self._state_view.modules.absorbance_reader_dock_location(
92
85
  params.moduleId
93
86
  )
@@ -107,35 +100,29 @@ class CloseLidImpl(AbstractCommandImpl[CloseLidParams, SuccessData[CloseLidResul
107
100
  )
108
101
  )
109
102
 
110
- lid_gripper_offsets = self._state_view.labware.get_labware_gripper_offsets(
111
- loaded_lid.id, None
103
+ # The lid's labware definition stores gripper offsets for itself in the
104
+ # space normally meant for offsets for labware stacked atop it.
105
+ lid_gripper_offsets = self._state_view.labware.get_child_gripper_offsets(
106
+ labware_definition=lid_definition,
107
+ slot_name=None,
112
108
  )
113
109
  if lid_gripper_offsets is None:
114
110
  raise ValueError(
115
111
  "Gripper Offset values for Absorbance Reader Lid labware must not be None."
116
112
  )
117
113
 
118
- # Skips gripper moves when using virtual gripper
119
114
  await self._labware_movement.move_labware_with_gripper(
120
- labware_id=loaded_lid.id,
115
+ labware_definition=lid_definition,
121
116
  current_location=current_location,
122
117
  new_location=new_location,
123
118
  user_offset_data=lid_gripper_offsets,
124
119
  post_drop_slide_offset=None,
125
120
  )
126
-
127
- new_offset_id = self._equipment.find_applicable_labware_offset_id(
128
- labware_definition_uri=loaded_lid.definitionUri,
129
- labware_location=new_location,
121
+ state_update.set_absorbance_reader_lid(
122
+ module_id=mod_substate.module_id,
123
+ is_lid_on=True,
130
124
  )
131
125
 
132
- state_update = StateUpdate()
133
- state_update.set_labware_location(
134
- labware_id=loaded_lid.id,
135
- new_location=new_location,
136
- new_offset_id=new_offset_id,
137
- )
138
-
139
126
  return SuccessData(
140
127
  public=CloseLidResult(),
141
128
  state_update=state_update,
@@ -9,7 +9,6 @@ from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, Succe
9
9
  from ...errors.error_occurrence import ErrorOccurrence
10
10
  from ...errors import CannotPerformModuleAction
11
11
 
12
- from opentrons.protocol_engine.resources import labware_validation
13
12
  from opentrons.protocol_engine.types import AddressableAreaLocation
14
13
 
15
14
  from opentrons.drivers.types import AbsorbanceReaderLidStatus
@@ -54,39 +53,35 @@ class OpenLidImpl(AbstractCommandImpl[OpenLidParams, SuccessData[OpenLidResult]]
54
53
 
55
54
  async def execute(self, params: OpenLidParams) -> SuccessData[OpenLidResult]:
56
55
  """Move the absorbance reader lid from the module to the lid dock."""
56
+ state_update = StateUpdate()
57
57
  mod_substate = self._state_view.modules.get_absorbance_reader_substate(
58
58
  module_id=params.moduleId
59
59
  )
60
- # lid should currently be on the module
61
- assert mod_substate.lid_id is not None
62
- loaded_lid = self._state_view.labware.get(mod_substate.lid_id)
63
- assert labware_validation.is_absorbance_reader_lid(loaded_lid.loadName)
64
60
 
65
61
  hardware_lid_status = AbsorbanceReaderLidStatus.ON
66
- # If the lid is closed, if the lid is open No-op out
67
62
  if not self._state_view.config.use_virtual_modules:
68
63
  abs_reader = self._equipment.get_module_hardware_api(mod_substate.module_id)
69
64
 
70
65
  if abs_reader is not None:
71
- result = await abs_reader.get_current_lid_status()
72
- hardware_lid_status = result
66
+ hardware_lid_status = await abs_reader.get_current_lid_status()
73
67
  else:
74
68
  raise CannotPerformModuleAction(
75
69
  "Could not reach the Hardware API for Opentrons Plate Reader Module."
76
70
  )
77
71
 
78
- # If the lid is already OFF, no-op the lid removal
79
72
  if hardware_lid_status is AbsorbanceReaderLidStatus.OFF:
80
- assert isinstance(loaded_lid.location, AddressableAreaLocation)
81
- new_location = loaded_lid.location
82
- new_offset_id = self._equipment.find_applicable_labware_offset_id(
83
- labware_definition_uri=loaded_lid.definitionUri,
84
- labware_location=loaded_lid.location,
73
+ # The lid is already physically OFF, so we can no-op physically closing it
74
+ state_update.set_absorbance_reader_lid(
75
+ module_id=mod_substate.module_id, is_lid_on=False
85
76
  )
86
77
  else:
87
78
  # Allow propagation of ModuleNotAttachedError.
88
79
  _ = self._equipment.get_module_hardware_api(mod_substate.module_id)
89
80
 
81
+ lid_definition = (
82
+ self._state_view.labware.get_absorbance_reader_lid_definition()
83
+ )
84
+
90
85
  absorbance_model = self._state_view.modules.get_requested_model(
91
86
  params.moduleId
92
87
  )
@@ -106,35 +101,28 @@ class OpenLidImpl(AbstractCommandImpl[OpenLidParams, SuccessData[OpenLidResult]]
106
101
  mod_substate.module_id
107
102
  )
108
103
 
109
- lid_gripper_offsets = self._state_view.labware.get_labware_gripper_offsets(
110
- loaded_lid.id, None
104
+ # The lid's labware definition stores gripper offsets for itself in the
105
+ # space normally meant for offsets for labware stacked atop it.
106
+ lid_gripper_offsets = self._state_view.labware.get_child_gripper_offsets(
107
+ labware_definition=lid_definition,
108
+ slot_name=None,
111
109
  )
112
110
  if lid_gripper_offsets is None:
113
111
  raise ValueError(
114
112
  "Gripper Offset values for Absorbance Reader Lid labware must not be None."
115
113
  )
116
114
 
117
- # Skips gripper moves when using virtual gripper
118
115
  await self._labware_movement.move_labware_with_gripper(
119
- labware_id=loaded_lid.id,
116
+ labware_definition=lid_definition,
120
117
  current_location=current_location,
121
118
  new_location=new_location,
122
119
  user_offset_data=lid_gripper_offsets,
123
120
  post_drop_slide_offset=None,
124
121
  )
125
- new_offset_id = self._equipment.find_applicable_labware_offset_id(
126
- labware_definition_uri=loaded_lid.definitionUri,
127
- labware_location=new_location,
122
+ state_update.set_absorbance_reader_lid(
123
+ module_id=mod_substate.module_id, is_lid_on=False
128
124
  )
129
125
 
130
- state_update = StateUpdate()
131
-
132
- state_update.set_labware_location(
133
- labware_id=loaded_lid.id,
134
- new_location=new_location,
135
- new_offset_id=new_offset_id,
136
- )
137
-
138
126
  return SuccessData(
139
127
  public=OpenLidResult(),
140
128
  state_update=state_update,
@@ -10,6 +10,8 @@ from ..errors import LabwareIsNotAllowedInLocationError
10
10
  from ..resources import labware_validation, fixture_validation
11
11
  from ..types import (
12
12
  LabwareLocation,
13
+ ModuleLocation,
14
+ ModuleModel,
13
15
  OnLabwareLocation,
14
16
  DeckSlotLocation,
15
17
  AddressableAreaLocation,
@@ -160,6 +162,13 @@ class LoadLabwareImplementation(
160
162
  top_labware_definition=loaded_labware.definition,
161
163
  bottom_labware_id=verified_location.labwareId,
162
164
  )
165
+ # Validate labware for the absorbance reader
166
+ elif isinstance(params.location, ModuleLocation):
167
+ module = self._state_view.modules.get(params.location.moduleId)
168
+ if module is not None and module.model == ModuleModel.ABSORBANCE_READER_V1:
169
+ self._state_view.labware.raise_if_labware_incompatible_with_plate_reader(
170
+ loaded_labware.definition
171
+ )
163
172
 
164
173
  return SuccessData(
165
174
  public=LoadLabwareResult(