opentrons 8.2.0__py2.py3-none-any.whl → 8.2.0a0__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 (62) hide show
  1. opentrons/drivers/absorbance_reader/async_byonoy.py +5 -6
  2. opentrons/hardware_control/backends/ot3utils.py +0 -1
  3. opentrons/hardware_control/modules/absorbance_reader.py +0 -2
  4. opentrons/hardware_control/ot3api.py +5 -5
  5. opentrons/hardware_control/protocols/position_estimator.py +1 -3
  6. opentrons/hardware_control/types.py +0 -2
  7. opentrons/legacy_commands/helpers.py +2 -8
  8. opentrons/protocol_api/core/engine/labware.py +2 -10
  9. opentrons/protocol_api/core/engine/module_core.py +1 -38
  10. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +5 -12
  11. opentrons/protocol_api/core/engine/protocol.py +30 -5
  12. opentrons/protocol_api/core/labware.py +0 -4
  13. opentrons/protocol_api/core/legacy/legacy_labware_core.py +0 -5
  14. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +0 -1
  15. opentrons/protocol_api/core/protocol.py +0 -1
  16. opentrons/protocol_api/module_contexts.py +26 -69
  17. opentrons/protocol_api/protocol_context.py +2 -12
  18. opentrons/protocol_engine/actions/__init__.py +2 -0
  19. opentrons/protocol_engine/actions/actions.py +12 -0
  20. opentrons/protocol_engine/clients/sync_client.py +6 -0
  21. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +31 -18
  22. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +7 -19
  23. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +29 -17
  24. opentrons/protocol_engine/commands/absorbance_reader/read.py +0 -4
  25. opentrons/protocol_engine/commands/aspirate_in_place.py +3 -3
  26. opentrons/protocol_engine/commands/command.py +1 -3
  27. opentrons/protocol_engine/commands/dispense_in_place.py +1 -1
  28. opentrons/protocol_engine/commands/drop_tip.py +1 -2
  29. opentrons/protocol_engine/commands/drop_tip_in_place.py +2 -7
  30. opentrons/protocol_engine/commands/load_labware.py +0 -9
  31. opentrons/protocol_engine/commands/load_module.py +39 -0
  32. opentrons/protocol_engine/commands/move_labware.py +4 -49
  33. opentrons/protocol_engine/commands/pick_up_tip.py +1 -1
  34. opentrons/protocol_engine/commands/pipetting_common.py +1 -8
  35. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +35 -49
  36. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +1 -3
  37. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +1 -5
  38. opentrons/protocol_engine/create_protocol_engine.py +1 -18
  39. opentrons/protocol_engine/errors/__init__.py +0 -2
  40. opentrons/protocol_engine/errors/error_occurrence.py +3 -8
  41. opentrons/protocol_engine/errors/exceptions.py +0 -13
  42. opentrons/protocol_engine/execution/labware_movement.py +21 -69
  43. opentrons/protocol_engine/execution/movement.py +4 -9
  44. opentrons/protocol_engine/protocol_engine.py +7 -0
  45. opentrons/protocol_engine/resources/deck_data_provider.py +39 -0
  46. opentrons/protocol_engine/resources/file_provider.py +7 -11
  47. opentrons/protocol_engine/resources/fixture_validation.py +1 -6
  48. opentrons/protocol_engine/state/geometry.py +49 -91
  49. opentrons/protocol_engine/state/labware.py +25 -102
  50. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +1 -3
  51. opentrons/protocol_engine/state/modules.py +80 -53
  52. opentrons/protocol_engine/state/motion.py +5 -17
  53. opentrons/protocol_engine/state/update_types.py +0 -16
  54. opentrons/protocol_runner/run_orchestrator.py +0 -15
  55. opentrons/protocols/parameters/csv_parameter_interface.py +1 -3
  56. opentrons/util/logging_config.py +1 -1
  57. {opentrons-8.2.0.dist-info → opentrons-8.2.0a0.dist-info}/METADATA +4 -4
  58. {opentrons-8.2.0.dist-info → opentrons-8.2.0a0.dist-info}/RECORD +62 -62
  59. {opentrons-8.2.0.dist-info → opentrons-8.2.0a0.dist-info}/LICENSE +0 -0
  60. {opentrons-8.2.0.dist-info → opentrons-8.2.0a0.dist-info}/WHEEL +0 -0
  61. {opentrons-8.2.0.dist-info → opentrons-8.2.0a0.dist-info}/entry_points.txt +0 -0
  62. {opentrons-8.2.0.dist-info → opentrons-8.2.0a0.dist-info}/top_level.txt +0 -0
@@ -23,9 +23,8 @@ from opentrons.hardware_control.modules.errors import AbsorbanceReaderDisconnect
23
23
 
24
24
 
25
25
  SN_PARSER = re.compile(r'ATTRS{serial}=="(?P<serial>.+?)"')
26
- # match semver V0.0.0 (old format) or one integer (latest format)
27
- VERSION_PARSER = re.compile(r"(?P<version>(V\d+\.\d+\.\d+|^\d+$))")
28
- SERIAL_PARSER = re.compile(r"(?P<serial>(OPT|BYO)[A-Z]{3}[0-9]+)")
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})")
29
28
 
30
29
 
31
30
  class AsyncByonoy:
@@ -158,9 +157,9 @@ class AsyncByonoy:
158
157
  )
159
158
  self._raise_if_error(err.name, f"Error getting device information: {err}")
160
159
  serial_match = SERIAL_PARSER.match(device_info.sn)
161
- version_match = VERSION_PARSER.search(device_info.version)
162
- serial = serial_match["serial"].strip() if serial_match else "OPTMAA00000"
163
- version = version_match["version"].lower() if version_match else "v0"
160
+ version_match = VERSION_PARSER.match(device_info.version)
161
+ serial = serial_match["serial"] if serial_match else "BYOMAA00000"
162
+ version = version_match["version"].lower() if version_match else "v0.0.0"
164
163
  info = {
165
164
  "serial": serial,
166
165
  "version": version,
@@ -652,7 +652,6 @@ _gripper_jaw_state_lookup: Dict[FirmwareGripperjawState, GripperJawState] = {
652
652
  FirmwareGripperjawState.force_controlling_home: GripperJawState.HOMED_READY,
653
653
  FirmwareGripperjawState.force_controlling: GripperJawState.GRIPPING,
654
654
  FirmwareGripperjawState.position_controlling: GripperJawState.HOLDING,
655
- FirmwareGripperjawState.stopped: GripperJawState.STOPPED,
656
655
  }
657
656
 
658
657
 
@@ -312,8 +312,6 @@ class AbsorbanceReader(mod_abc.AbstractModule):
312
312
  log.debug(f"Updating {self.name}: {self.port} with {firmware_file_path}")
313
313
  self._updating = True
314
314
  success, res = await self._driver.update_firmware(firmware_file_path)
315
- # it takes time for the plate reader to re-init after an update.
316
- await asyncio.sleep(10)
317
315
  self._device_info = await self._driver.get_device_info()
318
316
  await self._poller.start()
319
317
  self._updating = False
@@ -775,12 +775,12 @@ class OT3API(
775
775
  """
776
776
  Function to update motor estimation for a set of axes
777
777
  """
778
- if axes is None:
779
- axes = [ax for ax in Axis]
780
778
 
781
- axes = [ax for ax in axes if self._backend.axis_is_present(ax)]
782
-
783
- await self._backend.update_motor_estimation(axes)
779
+ if axes:
780
+ checked_axes = [ax for ax in axes if ax in Axis]
781
+ else:
782
+ checked_axes = [ax for ax in Axis]
783
+ await self._backend.update_motor_estimation(checked_axes)
784
784
 
785
785
  # Global actions API
786
786
  def pause(self, pause_type: PauseType) -> None:
@@ -10,7 +10,7 @@ class PositionEstimator(Protocol):
10
10
  """Update the specified axes' position estimators from their encoders.
11
11
 
12
12
  This will allow these axes to make a non-home move even if they do not currently have
13
- a position estimation (unless there is no tracked position from the encoders, as would be
13
+ a position estimation (unless there is no tracked poition from the encoders, as would be
14
14
  true immediately after boot).
15
15
 
16
16
  Axis encoders have less precision than their position estimators. Calling this function will
@@ -19,8 +19,6 @@ class PositionEstimator(Protocol):
19
19
 
20
20
  This function updates only the requested axes. If other axes have bad position estimation,
21
21
  moves that require those axes or attempts to get the position of those axes will still fail.
22
- Axes that are not currently available (like a plunger for a pipette that is not connected)
23
- will be ignored.
24
22
  """
25
23
  ...
26
24
 
@@ -625,8 +625,6 @@ class GripperJawState(enum.Enum):
625
625
  #: the gripper is actively force-control gripping something
626
626
  HOLDING = enum.auto()
627
627
  #: the gripper is in position-control mode
628
- STOPPED = enum.auto()
629
- #: the gripper has been homed before but is stopped now
630
628
 
631
629
 
632
630
  class InstrumentProbeType(enum.Enum):
@@ -49,9 +49,7 @@ def stringify_disposal_location(location: Union[TrashBin, WasteChute]) -> str:
49
49
 
50
50
 
51
51
  def _stringify_labware_movement_location(
52
- location: Union[
53
- DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute, TrashBin
54
- ]
52
+ location: Union[DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute]
55
53
  ) -> str:
56
54
  if isinstance(location, (int, str)):
57
55
  return f"slot {location}"
@@ -63,15 +61,11 @@ def _stringify_labware_movement_location(
63
61
  return str(location)
64
62
  elif isinstance(location, WasteChute):
65
63
  return "Waste Chute"
66
- elif isinstance(location, TrashBin):
67
- return "Trash Bin " + location.location.name
68
64
 
69
65
 
70
66
  def stringify_labware_movement_command(
71
67
  source_labware: Labware,
72
- destination: Union[
73
- DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute, TrashBin
74
- ],
68
+ destination: Union[DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute],
75
69
  use_gripper: bool,
76
70
  ) -> str:
77
71
  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, StagingSlotName
22
+ from opentrons.types import DeckSlotName, Point
23
23
  from opentrons.hardware_control.nozzle_manager import NozzleMap
24
24
 
25
25
 
@@ -139,10 +139,6 @@ 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
-
146
142
  def is_fixed_trash(self) -> bool:
147
143
  """Whether the labware is a fixed trash."""
148
144
  return self._engine_client.state.labware.is_fixed_trash(
@@ -190,13 +186,9 @@ class LabwareCore(AbstractLabware[WellCore]):
190
186
  def get_deck_slot(self) -> Optional[DeckSlotName]:
191
187
  """Get the deck slot the labware is in, if on deck."""
192
188
  try:
193
- ancestor = self._engine_client.state.geometry.get_ancestor_slot_name(
189
+ return self._engine_client.state.geometry.get_ancestor_slot_name(
194
190
  self.labware_id
195
191
  )
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
200
192
  except (
201
193
  LabwareNotOnDeckError,
202
194
  ModuleNotOnDeckError,
@@ -41,11 +41,6 @@ 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
-
49
44
  class ModuleCore(AbstractModuleCore):
50
45
  """Module core logic implementation for Python protocols.
51
46
  Args:
@@ -586,39 +581,7 @@ class AbsorbanceReaderCore(ModuleCore, AbstractAbsorbanceReaderCore):
586
581
  "Cannot perform Initialize action on Absorbance Reader without calling `.close_lid()` first."
587
582
  )
588
583
 
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
-
584
+ # TODO: check that the wavelengths are within the supported wavelengths
622
585
  self._engine_client.execute_command(
623
586
  cmd.absorbance_reader.InitializeParams(
624
587
  moduleId=self.module_id,
@@ -9,7 +9,6 @@ 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
13
12
  from opentrons_shared_data.module import FLEX_TC_LID_COLLISION_ZONE
14
13
 
15
14
  from opentrons.hardware_control import CriticalPoint
@@ -64,7 +63,7 @@ _FLEX_TC_LID_FRONT_RIGHT_PT = Point(
64
63
  )
65
64
 
66
65
 
67
- def check_safe_for_pipette_movement( # noqa: C901
66
+ def check_safe_for_pipette_movement(
68
67
  engine_state: StateView,
69
68
  pipette_id: str,
70
69
  labware_id: str,
@@ -122,12 +121,8 @@ def check_safe_for_pipette_movement( # noqa: C901
122
121
  f"Requested motion with the {primary_nozzle} nozzle partial configuration"
123
122
  f" is outside of robot bounds for the pipette."
124
123
  )
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
124
+
125
+ labware_slot = engine_state.geometry.get_ancestor_slot_name(labware_id)
131
126
 
132
127
  surrounding_slots = adjacent_slots_getters.get_surrounding_slots(
133
128
  slot=labware_slot.as_int(), robot_type=engine_state.config.robot_type
@@ -287,10 +282,8 @@ def check_safe_for_tip_pickup_and_return(
287
282
  is_96_ch_tiprack_adapter = engine_state.labware.get_has_quirk(
288
283
  labware_id=tiprack_parent.labwareId, quirk="tiprackAdapterFor96Channel"
289
284
  )
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
285
+ tiprack_height = engine_state.labware.get_dimensions(labware_id).z
286
+ adapter_height = engine_state.labware.get_dimensions(tiprack_parent.labwareId).z
294
287
  if is_partial_config and tiprack_height < adapter_height:
295
288
  raise PartialTipMovementNotAllowedError(
296
289
  f"{tiprack_name} cannot be on an adapter taller than the tip rack"
@@ -329,7 +329,6 @@ class ProtocolCore(
329
329
  NonConnectedModuleCore,
330
330
  OffDeckType,
331
331
  WasteChute,
332
- TrashBin,
333
332
  ],
334
333
  use_gripper: bool,
335
334
  pause_for_manual_move: bool,
@@ -448,10 +447,40 @@ class ProtocolCore(
448
447
  existing_module_ids=list(self._module_cores_by_id.keys()),
449
448
  )
450
449
 
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
+
451
457
  self._module_cores_by_id[module_core.module_id] = module_core
452
458
 
453
459
  return module_core
454
460
 
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
+
455
484
  def _create_non_connected_module_core(
456
485
  self, load_module_result: LoadModuleResult
457
486
  ) -> NonConnectedModuleCore:
@@ -778,7 +807,6 @@ class ProtocolCore(
778
807
  NonConnectedModuleCore,
779
808
  OffDeckType,
780
809
  WasteChute,
781
- TrashBin,
782
810
  ],
783
811
  ) -> LabwareLocation:
784
812
  if isinstance(location, LabwareCore):
@@ -795,7 +823,6 @@ class ProtocolCore(
795
823
  NonConnectedModuleCore,
796
824
  OffDeckType,
797
825
  WasteChute,
798
- TrashBin,
799
826
  ]
800
827
  ) -> NonStackedLocation:
801
828
  if isinstance(location, (ModuleCore, NonConnectedModuleCore)):
@@ -809,5 +836,3 @@ class ProtocolCore(
809
836
  elif isinstance(location, WasteChute):
810
837
  # TODO(mm, 2023-12-06) This will need to determine the appropriate Waste Chute to return, but only move_labware uses this for now
811
838
  return AddressableAreaLocation(addressableAreaName="gripperWasteChute")
812
- elif isinstance(location, TrashBin):
813
- return AddressableAreaLocation(addressableAreaName=location.area_name)
@@ -97,10 +97,6 @@ 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
-
104
100
  @abstractmethod
105
101
  def is_fixed_trash(self) -> bool:
106
102
  """Whether the labware is a fixed trash."""
@@ -138,11 +138,6 @@ 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
-
146
141
  def is_fixed_trash(self) -> bool:
147
142
  """Whether the labware is fixed trash."""
148
143
  return "fixedTrash" in self.get_quirks()
@@ -277,7 +277,6 @@ class LegacyProtocolCore(
277
277
  legacy_module_core.LegacyModuleCore,
278
278
  OffDeckType,
279
279
  WasteChute,
280
- TrashBin,
281
280
  ],
282
281
  use_gripper: bool,
283
282
  pause_for_manual_move: bool,
@@ -104,7 +104,6 @@ class AbstractProtocol(
104
104
  ModuleCoreType,
105
105
  OffDeckType,
106
106
  WasteChute,
107
- TrashBin,
108
107
  ],
109
108
  use_gripper: bool,
110
109
  pause_for_manual_move: bool,
@@ -3,8 +3,6 @@ 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
-
8
6
  from opentrons.protocol_engine.types import ABSMeasureMode
9
7
  from opentrons_shared_data.labware.types import LabwareDefinition
10
8
  from opentrons_shared_data.module.types import ModuleModel, ModuleType
@@ -161,18 +159,7 @@ class ModuleContext(CommandPublisher):
161
159
  load_location = loaded_adapter._core
162
160
  else:
163
161
  load_location = self._core
164
-
165
162
  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
-
176
163
  labware_core = self._protocol_core.load_labware(
177
164
  load_name=name,
178
165
  label=label,
@@ -581,7 +568,7 @@ class ThermocyclerContext(ModuleContext):
581
568
  individual well of the loaded labware, in µL.
582
569
  If not specified, the default is 25 µL.
583
570
 
584
- .. note::
571
+ .. note:
585
572
 
586
573
  If ``hold_time_minutes`` and ``hold_time_seconds`` are not
587
574
  specified, the Thermocycler will proceed to the next command
@@ -605,7 +592,7 @@ class ThermocyclerContext(ModuleContext):
605
592
  :param temperature: A value between 37 and 110, representing the target
606
593
  temperature in °C.
607
594
 
608
- .. note::
595
+ .. note:
609
596
 
610
597
  The Thermocycler will proceed to the next command immediately after
611
598
  ``temperature`` has been reached.
@@ -635,13 +622,13 @@ class ThermocyclerContext(ModuleContext):
635
622
  individual well of the loaded labware, in µL.
636
623
  If not specified, the default is 25 µL.
637
624
 
638
- .. note::
625
+ .. note:
639
626
 
640
627
  Unlike with :py:meth:`set_block_temperature`, either or both of
641
628
  ``hold_time_minutes`` and ``hold_time_seconds`` must be defined
642
629
  and for each step.
643
630
 
644
- .. note::
631
+ .. note:
645
632
 
646
633
  Before API Version 2.21, Thermocycler profiles run with this command
647
634
  would be listed in the app as having a number of repetitions equal to
@@ -991,7 +978,7 @@ class MagneticBlockContext(ModuleContext):
991
978
 
992
979
 
993
980
  class AbsorbanceReaderContext(ModuleContext):
994
- """An object representing a connected Absorbance Plate Reader Module.
981
+ """An object representing a connected Absorbance Reader Module.
995
982
 
996
983
  It should not be instantiated directly; instead, it should be
997
984
  created through :py:meth:`.ProtocolContext.load_module`.
@@ -1009,21 +996,17 @@ class AbsorbanceReaderContext(ModuleContext):
1009
996
 
1010
997
  @requires_version(2, 21)
1011
998
  def close_lid(self) -> None:
1012
- """Use the Flex Gripper to close the lid of the Absorbance Plate Reader.
1013
-
1014
- You must call this method before initializing the reader, even if the reader was
1015
- in the closed position at the start of the protocol.
1016
- """
999
+ """Close the lid of the Absorbance Reader."""
1017
1000
  self._core.close_lid()
1018
1001
 
1019
1002
  @requires_version(2, 21)
1020
1003
  def open_lid(self) -> None:
1021
- """Use the Flex Gripper to open the lid of the Absorbance Plate Reader."""
1004
+ """Open the lid of the Absorbance Reader."""
1022
1005
  self._core.open_lid()
1023
1006
 
1024
1007
  @requires_version(2, 21)
1025
1008
  def is_lid_on(self) -> bool:
1026
- """Return ``True`` if the Absorbance Plate Reader's lid is currently closed."""
1009
+ """Return ``True`` if the Absorbance Reader's lid is currently closed."""
1027
1010
  return self._core.is_lid_on()
1028
1011
 
1029
1012
  @requires_version(2, 21)
@@ -1033,28 +1016,19 @@ class AbsorbanceReaderContext(ModuleContext):
1033
1016
  wavelengths: List[int],
1034
1017
  reference_wavelength: Optional[int] = None,
1035
1018
  ) -> None:
1036
- """Prepare the Absorbance Plate Reader to read a plate.
1037
-
1038
- See :ref:`absorbance-initialization` for examples.
1019
+ """Take a zero reading on the Absorbance Plate Reader Module.
1039
1020
 
1040
1021
  :param mode: Either ``"single"`` or ``"multi"``.
1041
1022
 
1042
- - In single measurement mode, :py:meth:`.AbsorbanceReaderContext.read` uses
1043
- one sample wavelength and an optional reference wavelength.
1044
- - In multiple measurement mode, :py:meth:`.AbsorbanceReaderContext.read` uses
1045
- a list of up to six sample wavelengths.
1046
- :param wavelengths: A list of wavelengths, in nm, to measure.
1047
-
1048
- - In the default hardware configuration, each wavelength must be one of
1049
- ``450`` (blue), ``562`` (green), ``600`` (orange), or ``650`` (red). In
1050
- custom hardware configurations, the module may accept other integers
1051
- between 350 and 1000.
1052
- - The list must contain only one item when initializing a single measurement.
1053
- - The list can contain one to six items when initializing a multiple measurement.
1054
- :param reference_wavelength: An optional reference wavelength, in nm. If provided,
1055
- :py:meth:`.AbsorbanceReaderContext.read` will read at the reference
1056
- wavelength and then subtract the reference wavelength values from the
1057
- measurement wavelength values. Can only be used with single measurements.
1023
+ - In single measurement mode, :py:meth:`.AbsorbanceReaderContext.read` uses
1024
+ one sample wavelength and an optional reference wavelength.
1025
+ - In multiple measurement mode, :py:meth:`.AbsorbanceReaderContext.read` uses
1026
+ a list of up to six sample wavelengths.
1027
+ :param wavelengths: A list of wavelengths, in mm, to measure.
1028
+ - Must contain only one item when initializing a single measurement.
1029
+ - Must contain one to six items when initializing a multiple measurement.
1030
+ :param reference_wavelength: An optional reference wavelength, in mm. Cannot be
1031
+ used with multiple measurements.
1058
1032
  """
1059
1033
  self._core.initialize(
1060
1034
  mode, wavelengths, reference_wavelength=reference_wavelength
@@ -1064,33 +1038,16 @@ class AbsorbanceReaderContext(ModuleContext):
1064
1038
  def read(
1065
1039
  self, export_filename: Optional[str] = None
1066
1040
  ) -> Dict[int, Dict[str, float]]:
1067
- """Read a plate on the Absorbance Plate Reader.
1068
-
1069
- This method always returns a dictionary of measurement data. It optionally will
1070
- save a CSV file of the results to the Flex filesystem, which you can access from
1071
- the Recent Protocol Runs screen in the Opentrons App. These files are `only` saved
1072
- if you specify ``export_filename``.
1073
-
1074
- In simulation, the values for each well key in the dictionary are set to zero, and
1075
- no files are written.
1076
-
1077
- .. note::
1078
-
1079
- Avoid divide-by-zero errors when simulating and using the results of this
1080
- method later in the protocol. If you divide by any of the measurement
1081
- values, use :py:meth:`.ProtocolContext.is_simulating` to use alternate dummy
1082
- data or skip the division step.
1041
+ """Initiate read on the Absorbance Reader.
1083
1042
 
1084
- :param export_filename: An optional file basename. If provided, this method
1085
- will write a CSV file for each measurement in the read operation. File
1086
- names will use the value of this parameter, the measurement wavelength
1087
- supplied in :py:meth:`~.AbsorbanceReaderContext.initialize`, and a
1088
- ``.csv`` extension. For example, when reading at wavelengths 450 and 562
1089
- with ``export_filename="my_data"``, there will be two output files:
1090
- ``my_data_450.csv`` and ``my_data_562.csv``.
1043
+ Returns a dictionary of wavelengths to dictionary of values ordered by well name.
1091
1044
 
1092
- See :ref:`absorbance-csv` for information on working with these CSV files.
1045
+ :param export_filename: Optional, if a filename is provided a CSV file will be saved
1046
+ as a result of the read action containing measurement data. The filename will
1047
+ be modified to include the wavelength used during measurement. If multiple
1048
+ measurements are taken, then a file will be generated for each wavelength provided.
1093
1049
 
1094
- :returns: A dictionary of wavelengths to dictionary of values ordered by well name.
1050
+ Example: If `export_filename="my_data"` and wavelengths 450 and 531 are used during
1051
+ measurement, the output files will be "my_data_450.csv" and "my_data_531.csv".
1095
1052
  """
1096
1053
  return self._core.read(filename=export_filename)
@@ -45,7 +45,6 @@ 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
49
48
 
50
49
  from ._types import OffDeckType
51
50
  from .core.common import ModuleCore, LabwareCore, ProtocolCore
@@ -669,7 +668,7 @@ class ProtocolContext(CommandPublisher):
669
668
  self,
670
669
  labware: Labware,
671
670
  new_location: Union[
672
- DeckLocation, Labware, ModuleTypes, OffDeckType, WasteChute, TrashBin
671
+ DeckLocation, Labware, ModuleTypes, OffDeckType, WasteChute
673
672
  ],
674
673
  use_gripper: bool = False,
675
674
  pick_up_offset: Optional[Mapping[str, float]] = None,
@@ -714,8 +713,7 @@ class ProtocolContext(CommandPublisher):
714
713
  f"Expected labware of type 'Labware' but got {type(labware)}."
715
714
  )
716
715
 
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.
716
+ # Ensure that when moving to an absorbance reader than the lid is open
719
717
  if isinstance(new_location, AbsorbanceReaderContext):
720
718
  if new_location.is_lid_on():
721
719
  raise CommandPreconditionViolated(
@@ -729,19 +727,11 @@ class ProtocolContext(CommandPublisher):
729
727
  OffDeckType,
730
728
  DeckSlotName,
731
729
  StagingSlotName,
732
- TrashBin,
733
730
  ]
734
731
  if isinstance(new_location, (Labware, ModuleContext)):
735
732
  location = new_location._core
736
733
  elif isinstance(new_location, (OffDeckType, WasteChute)):
737
734
  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
- )
745
735
  else:
746
736
  location = validation.ensure_and_convert_deck_slot(
747
737
  new_location, self._api_version, self._core.robot_type
@@ -28,6 +28,7 @@ from .actions import (
28
28
  DoorChangeAction,
29
29
  ResetTipsAction,
30
30
  SetPipetteMovementSpeedAction,
31
+ AddAbsorbanceReaderLidAction,
31
32
  )
32
33
  from .get_state_update import get_state_updates
33
34
 
@@ -57,6 +58,7 @@ __all__ = [
57
58
  "DoorChangeAction",
58
59
  "ResetTipsAction",
59
60
  "SetPipetteMovementSpeedAction",
61
+ "AddAbsorbanceReaderLidAction",
60
62
  # action payload values
61
63
  "PauseSource",
62
64
  "FinishErrorDetails",
@@ -271,6 +271,17 @@ 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
+
274
285
  @dataclasses.dataclass(frozen=True)
275
286
  class SetErrorRecoveryPolicyAction:
276
287
  """See `ProtocolEngine.set_error_recovery_policy()`."""
@@ -298,5 +309,6 @@ Action = Union[
298
309
  AddLiquidAction,
299
310
  ResetTipsAction,
300
311
  SetPipetteMovementSpeedAction,
312
+ AddAbsorbanceReaderLidAction,
301
313
  SetErrorRecoveryPolicyAction,
302
314
  ]
@@ -119,6 +119,12 @@ 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
+
122
128
  def add_liquid(
123
129
  self, name: str, color: Optional[str], description: Optional[str]
124
130
  ) -> Liquid: