opentrons 8.1.0__py2.py3-none-any.whl → 8.2.0__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 (238) hide show
  1. opentrons/cli/analyze.py +71 -7
  2. opentrons/config/__init__.py +9 -0
  3. opentrons/config/advanced_settings.py +22 -0
  4. opentrons/config/defaults_ot3.py +14 -36
  5. opentrons/config/feature_flags.py +4 -0
  6. opentrons/config/types.py +6 -17
  7. opentrons/drivers/absorbance_reader/abstract.py +27 -3
  8. opentrons/drivers/absorbance_reader/async_byonoy.py +208 -154
  9. opentrons/drivers/absorbance_reader/driver.py +24 -15
  10. opentrons/drivers/absorbance_reader/hid_protocol.py +79 -50
  11. opentrons/drivers/absorbance_reader/simulator.py +32 -6
  12. opentrons/drivers/types.py +23 -1
  13. opentrons/execute.py +2 -2
  14. opentrons/hardware_control/api.py +18 -10
  15. opentrons/hardware_control/backends/controller.py +3 -2
  16. opentrons/hardware_control/backends/flex_protocol.py +11 -5
  17. opentrons/hardware_control/backends/ot3controller.py +18 -50
  18. opentrons/hardware_control/backends/ot3simulator.py +7 -6
  19. opentrons/hardware_control/backends/ot3utils.py +1 -0
  20. opentrons/hardware_control/instruments/ot2/pipette_handler.py +22 -82
  21. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -2
  22. opentrons/hardware_control/module_control.py +43 -2
  23. opentrons/hardware_control/modules/__init__.py +7 -1
  24. opentrons/hardware_control/modules/absorbance_reader.py +232 -83
  25. opentrons/hardware_control/modules/errors.py +7 -0
  26. opentrons/hardware_control/modules/heater_shaker.py +8 -3
  27. opentrons/hardware_control/modules/magdeck.py +12 -3
  28. opentrons/hardware_control/modules/mod_abc.py +27 -2
  29. opentrons/hardware_control/modules/tempdeck.py +15 -7
  30. opentrons/hardware_control/modules/thermocycler.py +69 -3
  31. opentrons/hardware_control/modules/types.py +11 -5
  32. opentrons/hardware_control/modules/update.py +11 -5
  33. opentrons/hardware_control/modules/utils.py +3 -1
  34. opentrons/hardware_control/ot3_calibration.py +6 -6
  35. opentrons/hardware_control/ot3api.py +131 -94
  36. opentrons/hardware_control/poller.py +15 -11
  37. opentrons/hardware_control/protocols/__init__.py +1 -7
  38. opentrons/hardware_control/protocols/instrument_configurer.py +14 -2
  39. opentrons/hardware_control/protocols/liquid_handler.py +5 -0
  40. opentrons/hardware_control/protocols/position_estimator.py +3 -1
  41. opentrons/hardware_control/types.py +2 -0
  42. opentrons/legacy_commands/helpers.py +8 -2
  43. opentrons/motion_planning/__init__.py +2 -0
  44. opentrons/motion_planning/waypoints.py +32 -0
  45. opentrons/protocol_api/__init__.py +2 -1
  46. opentrons/protocol_api/_liquid.py +87 -1
  47. opentrons/protocol_api/_parameter_context.py +10 -1
  48. opentrons/protocol_api/core/engine/deck_conflict.py +0 -297
  49. opentrons/protocol_api/core/engine/instrument.py +29 -25
  50. opentrons/protocol_api/core/engine/labware.py +20 -4
  51. opentrons/protocol_api/core/engine/module_core.py +166 -17
  52. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +362 -0
  53. opentrons/protocol_api/core/engine/protocol.py +30 -2
  54. opentrons/protocol_api/core/instrument.py +2 -0
  55. opentrons/protocol_api/core/labware.py +4 -0
  56. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -0
  57. opentrons/protocol_api/core/legacy/legacy_labware_core.py +5 -0
  58. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +6 -2
  59. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -0
  60. opentrons/protocol_api/core/module.py +22 -4
  61. opentrons/protocol_api/core/protocol.py +6 -2
  62. opentrons/protocol_api/instrument_context.py +52 -20
  63. opentrons/protocol_api/labware.py +13 -1
  64. opentrons/protocol_api/module_contexts.py +115 -17
  65. opentrons/protocol_api/protocol_context.py +49 -5
  66. opentrons/protocol_api/validation.py +5 -3
  67. opentrons/protocol_engine/__init__.py +10 -9
  68. opentrons/protocol_engine/actions/__init__.py +3 -0
  69. opentrons/protocol_engine/actions/actions.py +30 -25
  70. opentrons/protocol_engine/actions/get_state_update.py +38 -0
  71. opentrons/protocol_engine/clients/sync_client.py +1 -1
  72. opentrons/protocol_engine/clients/transports.py +1 -1
  73. opentrons/protocol_engine/commands/__init__.py +0 -4
  74. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +41 -11
  75. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +148 -0
  76. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +65 -9
  77. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +148 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/read.py +200 -0
  79. opentrons/protocol_engine/commands/aspirate.py +29 -16
  80. opentrons/protocol_engine/commands/aspirate_in_place.py +33 -16
  81. opentrons/protocol_engine/commands/blow_out.py +63 -14
  82. opentrons/protocol_engine/commands/blow_out_in_place.py +55 -13
  83. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +2 -5
  84. opentrons/protocol_engine/commands/calibration/calibrate_module.py +3 -4
  85. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +2 -5
  86. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +6 -4
  87. opentrons/protocol_engine/commands/command.py +31 -18
  88. opentrons/protocol_engine/commands/command_unions.py +37 -24
  89. opentrons/protocol_engine/commands/comment.py +5 -3
  90. opentrons/protocol_engine/commands/configure_for_volume.py +11 -14
  91. opentrons/protocol_engine/commands/configure_nozzle_layout.py +9 -15
  92. opentrons/protocol_engine/commands/custom.py +5 -3
  93. opentrons/protocol_engine/commands/dispense.py +42 -20
  94. opentrons/protocol_engine/commands/dispense_in_place.py +32 -14
  95. opentrons/protocol_engine/commands/drop_tip.py +70 -16
  96. opentrons/protocol_engine/commands/drop_tip_in_place.py +59 -13
  97. opentrons/protocol_engine/commands/get_tip_presence.py +5 -3
  98. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +6 -6
  99. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +6 -6
  100. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +6 -6
  101. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +8 -6
  102. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +8 -4
  103. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +6 -4
  104. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +6 -6
  105. opentrons/protocol_engine/commands/home.py +11 -5
  106. opentrons/protocol_engine/commands/liquid_probe.py +146 -88
  107. opentrons/protocol_engine/commands/load_labware.py +28 -5
  108. opentrons/protocol_engine/commands/load_liquid.py +18 -7
  109. opentrons/protocol_engine/commands/load_module.py +4 -6
  110. opentrons/protocol_engine/commands/load_pipette.py +18 -17
  111. opentrons/protocol_engine/commands/magnetic_module/disengage.py +6 -6
  112. opentrons/protocol_engine/commands/magnetic_module/engage.py +6 -4
  113. opentrons/protocol_engine/commands/move_labware.py +155 -23
  114. opentrons/protocol_engine/commands/move_relative.py +15 -3
  115. opentrons/protocol_engine/commands/move_to_addressable_area.py +29 -4
  116. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +13 -4
  117. opentrons/protocol_engine/commands/move_to_coordinates.py +11 -5
  118. opentrons/protocol_engine/commands/move_to_well.py +37 -10
  119. opentrons/protocol_engine/commands/pick_up_tip.py +51 -30
  120. opentrons/protocol_engine/commands/pipetting_common.py +47 -16
  121. opentrons/protocol_engine/commands/prepare_to_aspirate.py +62 -15
  122. opentrons/protocol_engine/commands/reload_labware.py +13 -4
  123. opentrons/protocol_engine/commands/retract_axis.py +6 -3
  124. opentrons/protocol_engine/commands/save_position.py +2 -3
  125. opentrons/protocol_engine/commands/set_rail_lights.py +5 -3
  126. opentrons/protocol_engine/commands/set_status_bar.py +5 -3
  127. opentrons/protocol_engine/commands/temperature_module/deactivate.py +6 -4
  128. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +3 -4
  129. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +6 -6
  130. opentrons/protocol_engine/commands/thermocycler/__init__.py +19 -0
  131. opentrons/protocol_engine/commands/thermocycler/close_lid.py +8 -8
  132. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +6 -4
  133. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +6 -4
  134. opentrons/protocol_engine/commands/thermocycler/open_lid.py +8 -4
  135. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +165 -0
  136. opentrons/protocol_engine/commands/thermocycler/run_profile.py +6 -6
  137. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +3 -4
  138. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +3 -4
  139. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +6 -4
  140. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +6 -4
  141. opentrons/protocol_engine/commands/touch_tip.py +19 -7
  142. opentrons/protocol_engine/commands/unsafe/__init__.py +30 -0
  143. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +6 -4
  144. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -4
  145. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +5 -3
  146. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +208 -0
  147. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +77 -0
  148. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +10 -4
  149. opentrons/protocol_engine/commands/verify_tip_presence.py +5 -5
  150. opentrons/protocol_engine/commands/wait_for_duration.py +5 -3
  151. opentrons/protocol_engine/commands/wait_for_resume.py +5 -3
  152. opentrons/protocol_engine/create_protocol_engine.py +60 -10
  153. opentrons/protocol_engine/engine_support.py +2 -1
  154. opentrons/protocol_engine/error_recovery_policy.py +14 -3
  155. opentrons/protocol_engine/errors/__init__.py +20 -0
  156. opentrons/protocol_engine/errors/error_occurrence.py +8 -3
  157. opentrons/protocol_engine/errors/exceptions.py +127 -2
  158. opentrons/protocol_engine/execution/__init__.py +2 -0
  159. opentrons/protocol_engine/execution/command_executor.py +22 -13
  160. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  161. opentrons/protocol_engine/execution/door_watcher.py +1 -1
  162. opentrons/protocol_engine/execution/equipment.py +2 -1
  163. opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
  164. opentrons/protocol_engine/execution/gantry_mover.py +4 -2
  165. opentrons/protocol_engine/execution/hardware_stopper.py +3 -3
  166. opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +1 -4
  167. opentrons/protocol_engine/execution/labware_movement.py +73 -22
  168. opentrons/protocol_engine/execution/movement.py +17 -7
  169. opentrons/protocol_engine/execution/pipetting.py +7 -4
  170. opentrons/protocol_engine/execution/queue_worker.py +6 -2
  171. opentrons/protocol_engine/execution/run_control.py +1 -1
  172. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +1 -1
  173. opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +2 -1
  174. opentrons/protocol_engine/execution/tip_handler.py +77 -43
  175. opentrons/protocol_engine/notes/__init__.py +14 -2
  176. opentrons/protocol_engine/notes/notes.py +18 -1
  177. opentrons/protocol_engine/plugins.py +1 -1
  178. opentrons/protocol_engine/protocol_engine.py +47 -31
  179. opentrons/protocol_engine/resources/__init__.py +2 -0
  180. opentrons/protocol_engine/resources/deck_data_provider.py +19 -5
  181. opentrons/protocol_engine/resources/file_provider.py +161 -0
  182. opentrons/protocol_engine/resources/fixture_validation.py +11 -1
  183. opentrons/protocol_engine/resources/labware_validation.py +10 -0
  184. opentrons/protocol_engine/state/__init__.py +0 -70
  185. opentrons/protocol_engine/state/addressable_areas.py +1 -1
  186. opentrons/protocol_engine/state/command_history.py +21 -2
  187. opentrons/protocol_engine/state/commands.py +110 -31
  188. opentrons/protocol_engine/state/files.py +59 -0
  189. opentrons/protocol_engine/state/frustum_helpers.py +440 -0
  190. opentrons/protocol_engine/state/geometry.py +445 -59
  191. opentrons/protocol_engine/state/labware.py +264 -84
  192. opentrons/protocol_engine/state/liquids.py +1 -1
  193. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +21 -3
  194. opentrons/protocol_engine/state/modules.py +145 -90
  195. opentrons/protocol_engine/state/motion.py +33 -14
  196. opentrons/protocol_engine/state/pipettes.py +157 -317
  197. opentrons/protocol_engine/state/state.py +30 -1
  198. opentrons/protocol_engine/state/state_summary.py +3 -0
  199. opentrons/protocol_engine/state/tips.py +69 -114
  200. opentrons/protocol_engine/state/update_types.py +424 -0
  201. opentrons/protocol_engine/state/wells.py +236 -0
  202. opentrons/protocol_engine/types.py +90 -0
  203. opentrons/protocol_reader/file_format_validator.py +83 -15
  204. opentrons/protocol_runner/json_translator.py +21 -5
  205. opentrons/protocol_runner/legacy_command_mapper.py +27 -6
  206. opentrons/protocol_runner/legacy_context_plugin.py +27 -71
  207. opentrons/protocol_runner/protocol_runner.py +6 -3
  208. opentrons/protocol_runner/run_orchestrator.py +41 -6
  209. opentrons/protocols/advanced_control/mix.py +3 -5
  210. opentrons/protocols/advanced_control/transfers.py +125 -56
  211. opentrons/protocols/api_support/constants.py +1 -1
  212. opentrons/protocols/api_support/definitions.py +1 -1
  213. opentrons/protocols/api_support/labware_like.py +4 -4
  214. opentrons/protocols/api_support/tip_tracker.py +2 -2
  215. opentrons/protocols/api_support/types.py +15 -2
  216. opentrons/protocols/api_support/util.py +30 -42
  217. opentrons/protocols/duration/errors.py +1 -1
  218. opentrons/protocols/duration/estimator.py +50 -29
  219. opentrons/protocols/execution/dev_types.py +2 -2
  220. opentrons/protocols/execution/execute_json_v4.py +15 -10
  221. opentrons/protocols/execution/execute_python.py +8 -3
  222. opentrons/protocols/geometry/planning.py +12 -12
  223. opentrons/protocols/labware.py +17 -33
  224. opentrons/protocols/parameters/csv_parameter_interface.py +3 -1
  225. opentrons/simulate.py +3 -3
  226. opentrons/types.py +30 -3
  227. opentrons/util/logging_config.py +34 -0
  228. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/METADATA +5 -4
  229. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/RECORD +235 -223
  230. opentrons/protocol_engine/commands/absorbance_reader/measure.py +0 -94
  231. opentrons/protocol_engine/commands/configuring_common.py +0 -26
  232. opentrons/protocol_runner/thread_async_queue.py +0 -174
  233. /opentrons/protocol_engine/state/{abstract_store.py → _abstract_store.py} +0 -0
  234. /opentrons/protocol_engine/state/{move_types.py → _move_types.py} +0 -0
  235. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/LICENSE +0 -0
  236. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/WHEEL +0 -0
  237. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/entry_points.txt +0 -0
  238. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,148 @@
1
+ """Command models to close the lid on an Absorbance Reader."""
2
+ from __future__ import annotations
3
+ from typing import Optional, Literal, TYPE_CHECKING
4
+ from typing_extensions import Type
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
9
+ from ...errors.error_occurrence import ErrorOccurrence
10
+ from ...errors import CannotPerformModuleAction
11
+ from opentrons.protocol_engine.types import AddressableAreaLocation
12
+
13
+ from ...state.update_types import StateUpdate
14
+
15
+
16
+ from opentrons.drivers.types import AbsorbanceReaderLidStatus
17
+
18
+ if TYPE_CHECKING:
19
+ from opentrons.protocol_engine.state.state import StateView
20
+ from opentrons.protocol_engine.execution import (
21
+ EquipmentHandler,
22
+ LabwareMovementHandler,
23
+ )
24
+
25
+
26
+ CloseLidCommandType = Literal["absorbanceReader/closeLid"]
27
+
28
+
29
+ class CloseLidParams(BaseModel):
30
+ """Input parameters to close the lid on an absorbance reading."""
31
+
32
+ moduleId: str = Field(..., description="Unique ID of the absorbance reader.")
33
+
34
+
35
+ class CloseLidResult(BaseModel):
36
+ """Result data from closing the lid on an aborbance reading."""
37
+
38
+
39
+ class CloseLidImpl(AbstractCommandImpl[CloseLidParams, SuccessData[CloseLidResult]]):
40
+ """Execution implementation of closing the lid on an Absorbance Reader."""
41
+
42
+ def __init__(
43
+ self,
44
+ state_view: StateView,
45
+ equipment: EquipmentHandler,
46
+ labware_movement: LabwareMovementHandler,
47
+ **unused_dependencies: object,
48
+ ) -> None:
49
+ self._state_view = state_view
50
+ self._equipment = equipment
51
+ self._labware_movement = labware_movement
52
+
53
+ async def execute(self, params: CloseLidParams) -> SuccessData[CloseLidResult]:
54
+ """Execute the close lid command."""
55
+ state_update = StateUpdate()
56
+ mod_substate = self._state_view.modules.get_absorbance_reader_substate(
57
+ module_id=params.moduleId
58
+ )
59
+
60
+ hardware_lid_status = AbsorbanceReaderLidStatus.OFF
61
+ if not self._state_view.config.use_virtual_modules:
62
+ abs_reader = self._equipment.get_module_hardware_api(mod_substate.module_id)
63
+
64
+ if abs_reader is not None:
65
+ hardware_lid_status = await abs_reader.get_current_lid_status()
66
+ else:
67
+ raise CannotPerformModuleAction(
68
+ "Could not reach the Hardware API for Opentrons Plate Reader Module."
69
+ )
70
+
71
+ if hardware_lid_status is AbsorbanceReaderLidStatus.ON:
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
75
+ )
76
+ else:
77
+ # Allow propagation of ModuleNotAttachedError.
78
+ _ = self._equipment.get_module_hardware_api(mod_substate.module_id)
79
+
80
+ lid_definition = (
81
+ self._state_view.labware.get_absorbance_reader_lid_definition()
82
+ )
83
+
84
+ current_location = self._state_view.modules.absorbance_reader_dock_location(
85
+ params.moduleId
86
+ )
87
+
88
+ # we need to move the lid onto the module reader
89
+ absorbance_model = self._state_view.modules.get_requested_model(
90
+ params.moduleId
91
+ )
92
+ assert absorbance_model is not None
93
+ new_location = AddressableAreaLocation(
94
+ addressableAreaName=self._state_view.modules.ensure_and_convert_module_fixture_location(
95
+ deck_slot=self._state_view.modules.get_location(
96
+ params.moduleId
97
+ ).slotName,
98
+ deck_type=self._state_view.config.deck_type,
99
+ model=absorbance_model,
100
+ )
101
+ )
102
+
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,
108
+ )
109
+ if lid_gripper_offsets is None:
110
+ raise ValueError(
111
+ "Gripper Offset values for Absorbance Reader Lid labware must not be None."
112
+ )
113
+
114
+ await self._labware_movement.move_labware_with_gripper(
115
+ labware_definition=lid_definition,
116
+ current_location=current_location,
117
+ new_location=new_location,
118
+ user_offset_data=lid_gripper_offsets,
119
+ post_drop_slide_offset=None,
120
+ )
121
+ state_update.set_absorbance_reader_lid(
122
+ module_id=mod_substate.module_id,
123
+ is_lid_on=True,
124
+ )
125
+
126
+ return SuccessData(
127
+ public=CloseLidResult(),
128
+ state_update=state_update,
129
+ )
130
+
131
+
132
+ class CloseLid(BaseCommand[CloseLidParams, CloseLidResult, ErrorOccurrence]):
133
+ """A command to close the lid on an Absorbance Reader."""
134
+
135
+ commandType: CloseLidCommandType = "absorbanceReader/closeLid"
136
+ params: CloseLidParams
137
+ result: Optional[CloseLidResult]
138
+
139
+ _ImplementationCls: Type[CloseLidImpl] = CloseLidImpl
140
+
141
+
142
+ class CloseLidCreate(BaseCommandCreate[CloseLidParams]):
143
+ """A request to execute an Absorbance Reader close lid command."""
144
+
145
+ commandType: CloseLidCommandType = "absorbanceReader/closeLid"
146
+ params: CloseLidParams
147
+
148
+ _CommandCls: Type[CloseLid] = CloseLid
@@ -1,15 +1,19 @@
1
1
  """Command models to initialize an Absorbance Reader."""
2
2
  from __future__ import annotations
3
- from typing import Optional, Literal, TYPE_CHECKING
3
+ from typing import List, Optional, Literal, TYPE_CHECKING
4
4
  from typing_extensions import Type
5
5
 
6
6
  from pydantic import BaseModel, Field
7
7
 
8
+ from opentrons.drivers.types import ABSMeasurementMode
9
+ from opentrons.protocol_engine.types import ABSMeasureMode
10
+
8
11
  from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
9
12
  from ...errors.error_occurrence import ErrorOccurrence
13
+ from ...errors import InvalidWavelengthError
10
14
 
11
15
  if TYPE_CHECKING:
12
- from opentrons.protocol_engine.state import StateView
16
+ from opentrons.protocol_engine.state.state import StateView
13
17
  from opentrons.protocol_engine.execution import EquipmentHandler
14
18
 
15
19
 
@@ -20,7 +24,13 @@ class InitializeParams(BaseModel):
20
24
  """Input parameters to initialize an absorbance reading."""
21
25
 
22
26
  moduleId: str = Field(..., description="Unique ID of the absorbance reader.")
23
- sampleWavelength: int = Field(..., description="Sample wavelength in nm.")
27
+ measureMode: ABSMeasureMode = Field(
28
+ ..., description="Initialize single or multi measurement mode."
29
+ )
30
+ sampleWavelengths: List[int] = Field(..., description="Sample wavelengths in nm.")
31
+ referenceWavelength: Optional[int] = Field(
32
+ None, description="Optional reference wavelength in nm."
33
+ )
24
34
 
25
35
 
26
36
  class InitializeResult(BaseModel):
@@ -28,7 +38,7 @@ class InitializeResult(BaseModel):
28
38
 
29
39
 
30
40
  class InitializeImpl(
31
- AbstractCommandImpl[InitializeParams, SuccessData[InitializeResult, None]]
41
+ AbstractCommandImpl[InitializeParams, SuccessData[InitializeResult]]
32
42
  ):
33
43
  """Execution implementation of initializing an Absorbance Reader."""
34
44
 
@@ -41,9 +51,7 @@ class InitializeImpl(
41
51
  self._state_view = state_view
42
52
  self._equipment = equipment
43
53
 
44
- async def execute(
45
- self, params: InitializeParams
46
- ) -> SuccessData[InitializeResult, None]:
54
+ async def execute(self, params: InitializeParams) -> SuccessData[InitializeResult]:
47
55
  """Initiate a single absorbance measurement."""
48
56
  abs_reader_substate = self._state_view.modules.get_absorbance_reader_substate(
49
57
  module_id=params.moduleId
@@ -54,11 +62,59 @@ class InitializeImpl(
54
62
  )
55
63
 
56
64
  if abs_reader is not None:
57
- await abs_reader.set_sample_wavelength(wavelength=params.sampleWavelength)
65
+ # Validate the parameters before initializing.
66
+ sample_wavelengths = set(params.sampleWavelengths)
67
+ sample_wavelengths_len = len(params.sampleWavelengths)
68
+ reference_wavelength = params.referenceWavelength
69
+ supported_wavelengths = set(abs_reader.supported_wavelengths)
70
+ unsupported_wavelengths = sample_wavelengths.difference(
71
+ supported_wavelengths
72
+ )
73
+ sample_wl_str = ", ".join([str(w) + "nm" for w in sample_wavelengths])
74
+ supported_wl_str = ", ".join([str(w) + "nm" for w in supported_wavelengths])
75
+ unsupported_wl_str = ", ".join(
76
+ [str(w) + "nm" for w in unsupported_wavelengths]
77
+ )
78
+ if unsupported_wavelengths:
79
+ raise InvalidWavelengthError(
80
+ f"Unsupported wavelengths: {unsupported_wl_str}. "
81
+ f" Use one of {supported_wl_str} instead."
82
+ )
83
+
84
+ if params.measureMode == "single":
85
+ if sample_wavelengths_len != 1:
86
+ raise ValueError(
87
+ f"Measure mode `single` requires one sample wavelength,"
88
+ f" {sample_wl_str} provided instead."
89
+ )
90
+ if (
91
+ reference_wavelength is not None
92
+ and reference_wavelength not in supported_wavelengths
93
+ ):
94
+ raise InvalidWavelengthError(
95
+ f"Reference wavelength {reference_wavelength}nm is not supported."
96
+ f" Use one of {supported_wl_str} instead."
97
+ )
98
+
99
+ if params.measureMode == "multi":
100
+ if sample_wavelengths_len < 1 or sample_wavelengths_len > 6:
101
+ raise ValueError(
102
+ f"Measure mode `multi` requires 1-6 sample wavelengths,"
103
+ f" {sample_wl_str} provided instead."
104
+ )
105
+ if reference_wavelength is not None:
106
+ raise ValueError(
107
+ "Reference wavelength cannot be used with Measure mode `multi`."
108
+ )
109
+
110
+ await abs_reader.set_sample_wavelength(
111
+ ABSMeasurementMode(params.measureMode),
112
+ params.sampleWavelengths,
113
+ reference_wavelength=params.referenceWavelength,
114
+ )
58
115
 
59
116
  return SuccessData(
60
117
  public=InitializeResult(),
61
- private=None,
62
118
  )
63
119
 
64
120
 
@@ -0,0 +1,148 @@
1
+ """Command models to close the lid on an Absorbance Reader."""
2
+ from __future__ import annotations
3
+ from typing import Optional, Literal, TYPE_CHECKING
4
+ from typing_extensions import Type
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
9
+ from ...errors.error_occurrence import ErrorOccurrence
10
+ from ...errors import CannotPerformModuleAction
11
+
12
+ from opentrons.protocol_engine.types import AddressableAreaLocation
13
+
14
+ from opentrons.drivers.types import AbsorbanceReaderLidStatus
15
+
16
+ from ...state.update_types import StateUpdate
17
+
18
+
19
+ if TYPE_CHECKING:
20
+ from opentrons.protocol_engine.state.state import StateView
21
+ from opentrons.protocol_engine.execution import (
22
+ EquipmentHandler,
23
+ LabwareMovementHandler,
24
+ )
25
+
26
+
27
+ OpenLidCommandType = Literal["absorbanceReader/openLid"]
28
+
29
+
30
+ class OpenLidParams(BaseModel):
31
+ """Input parameters to open the lid on an absorbance reading."""
32
+
33
+ moduleId: str = Field(..., description="Unique ID of the absorbance reader.")
34
+
35
+
36
+ class OpenLidResult(BaseModel):
37
+ """Result data from opening the lid on an aborbance reading."""
38
+
39
+
40
+ class OpenLidImpl(AbstractCommandImpl[OpenLidParams, SuccessData[OpenLidResult]]):
41
+ """Execution implementation of opening the lid on an Absorbance Reader."""
42
+
43
+ def __init__(
44
+ self,
45
+ state_view: StateView,
46
+ equipment: EquipmentHandler,
47
+ labware_movement: LabwareMovementHandler,
48
+ **unused_dependencies: object,
49
+ ) -> None:
50
+ self._state_view = state_view
51
+ self._equipment = equipment
52
+ self._labware_movement = labware_movement
53
+
54
+ async def execute(self, params: OpenLidParams) -> SuccessData[OpenLidResult]:
55
+ """Move the absorbance reader lid from the module to the lid dock."""
56
+ state_update = StateUpdate()
57
+ mod_substate = self._state_view.modules.get_absorbance_reader_substate(
58
+ module_id=params.moduleId
59
+ )
60
+
61
+ hardware_lid_status = AbsorbanceReaderLidStatus.ON
62
+ if not self._state_view.config.use_virtual_modules:
63
+ abs_reader = self._equipment.get_module_hardware_api(mod_substate.module_id)
64
+
65
+ if abs_reader is not None:
66
+ hardware_lid_status = await abs_reader.get_current_lid_status()
67
+ else:
68
+ raise CannotPerformModuleAction(
69
+ "Could not reach the Hardware API for Opentrons Plate Reader Module."
70
+ )
71
+
72
+ if hardware_lid_status is AbsorbanceReaderLidStatus.OFF:
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
76
+ )
77
+ else:
78
+ # Allow propagation of ModuleNotAttachedError.
79
+ _ = self._equipment.get_module_hardware_api(mod_substate.module_id)
80
+
81
+ lid_definition = (
82
+ self._state_view.labware.get_absorbance_reader_lid_definition()
83
+ )
84
+
85
+ absorbance_model = self._state_view.modules.get_requested_model(
86
+ params.moduleId
87
+ )
88
+ assert absorbance_model is not None
89
+ current_location = AddressableAreaLocation(
90
+ addressableAreaName=self._state_view.modules.ensure_and_convert_module_fixture_location(
91
+ deck_slot=self._state_view.modules.get_location(
92
+ params.moduleId
93
+ ).slotName,
94
+ deck_type=self._state_view.config.deck_type,
95
+ model=absorbance_model,
96
+ )
97
+ )
98
+
99
+ # we need to move the lid to the lid dock
100
+ new_location = self._state_view.modules.absorbance_reader_dock_location(
101
+ mod_substate.module_id
102
+ )
103
+
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,
109
+ )
110
+ if lid_gripper_offsets is None:
111
+ raise ValueError(
112
+ "Gripper Offset values for Absorbance Reader Lid labware must not be None."
113
+ )
114
+
115
+ await self._labware_movement.move_labware_with_gripper(
116
+ labware_definition=lid_definition,
117
+ current_location=current_location,
118
+ new_location=new_location,
119
+ user_offset_data=lid_gripper_offsets,
120
+ post_drop_slide_offset=None,
121
+ )
122
+ state_update.set_absorbance_reader_lid(
123
+ module_id=mod_substate.module_id, is_lid_on=False
124
+ )
125
+
126
+ return SuccessData(
127
+ public=OpenLidResult(),
128
+ state_update=state_update,
129
+ )
130
+
131
+
132
+ class OpenLid(BaseCommand[OpenLidParams, OpenLidResult, ErrorOccurrence]):
133
+ """A command to open the lid on an Absorbance Reader."""
134
+
135
+ commandType: OpenLidCommandType = "absorbanceReader/openLid"
136
+ params: OpenLidParams
137
+ result: Optional[OpenLidResult]
138
+
139
+ _ImplementationCls: Type[OpenLidImpl] = OpenLidImpl
140
+
141
+
142
+ class OpenLidCreate(BaseCommandCreate[OpenLidParams]):
143
+ """A request to execute an Absorbance Reader open lid command."""
144
+
145
+ commandType: OpenLidCommandType = "absorbanceReader/openLid"
146
+ params: OpenLidParams
147
+
148
+ _CommandCls: Type[OpenLid] = OpenLid
@@ -0,0 +1,200 @@
1
+ """Command models to read absorbance."""
2
+ from __future__ import annotations
3
+ from datetime import datetime
4
+ from typing import Optional, Dict, TYPE_CHECKING, List
5
+ from typing_extensions import Literal, Type
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
10
+ from ...errors import CannotPerformModuleAction, StorageLimitReachedError
11
+ from ...errors.error_occurrence import ErrorOccurrence
12
+
13
+ from ...resources.file_provider import (
14
+ PlateReaderData,
15
+ ReadData,
16
+ MAXIMUM_CSV_FILE_LIMIT,
17
+ )
18
+ from ...resources import FileProvider
19
+
20
+ if TYPE_CHECKING:
21
+ from opentrons.protocol_engine.state.state import StateView
22
+ from opentrons.protocol_engine.execution import EquipmentHandler
23
+
24
+
25
+ ReadAbsorbanceCommandType = Literal["absorbanceReader/read"]
26
+
27
+
28
+ class ReadAbsorbanceParams(BaseModel):
29
+ """Input parameters for an absorbance reading."""
30
+
31
+ moduleId: str = Field(..., description="Unique ID of the Absorbance Reader.")
32
+ fileName: Optional[str] = Field(
33
+ None,
34
+ description="Optional file name to use when storing the results of a measurement.",
35
+ )
36
+
37
+
38
+ class ReadAbsorbanceResult(BaseModel):
39
+ """Result data from running an aborbance reading, returned as a dictionary map of wavelengths containing a map of values by well name (eg. {450: {"A1": 0.0, ...}})."""
40
+
41
+ data: Optional[Dict[int, Dict[str, float]]] = Field(
42
+ ..., description="Absorbance data points per wavelength."
43
+ )
44
+ fileIds: Optional[List[str]] = Field(
45
+ ...,
46
+ description="List of file IDs for files output as a result of a Read action.",
47
+ )
48
+
49
+
50
+ class ReadAbsorbanceImpl(
51
+ AbstractCommandImpl[ReadAbsorbanceParams, SuccessData[ReadAbsorbanceResult]]
52
+ ):
53
+ """Execution implementation of an Absorbance Reader measurement."""
54
+
55
+ def __init__(
56
+ self,
57
+ state_view: StateView,
58
+ equipment: EquipmentHandler,
59
+ file_provider: FileProvider,
60
+ **unused_dependencies: object,
61
+ ) -> None:
62
+ self._state_view = state_view
63
+ self._equipment = equipment
64
+ self._file_provider = file_provider
65
+
66
+ async def execute( # noqa: C901
67
+ self, params: ReadAbsorbanceParams
68
+ ) -> SuccessData[ReadAbsorbanceResult]:
69
+ """Initiate an absorbance measurement."""
70
+ abs_reader_substate = self._state_view.modules.get_absorbance_reader_substate(
71
+ module_id=params.moduleId
72
+ )
73
+
74
+ # Allow propagation of ModuleNotAttachedError.
75
+ abs_reader = self._equipment.get_module_hardware_api(
76
+ abs_reader_substate.module_id
77
+ )
78
+
79
+ if abs_reader_substate.configured is False:
80
+ raise CannotPerformModuleAction(
81
+ "Cannot perform Read action on Absorbance Reader without calling `.initialize(...)` first."
82
+ )
83
+ if abs_reader_substate.is_lid_on is False:
84
+ raise CannotPerformModuleAction(
85
+ "Absorbance Plate Reader can't read a plate with the lid open. Call `close_lid()` first."
86
+ )
87
+
88
+ # TODO: we need to return a file ID and increase the file count even when a moduel is not attached
89
+ if (
90
+ params.fileName is not None
91
+ and abs_reader_substate.configured_wavelengths is not None
92
+ ):
93
+ # Validate that the amount of files we are about to generate does not put us higher than the limit
94
+ if (
95
+ self._state_view.files.get_filecount()
96
+ + len(abs_reader_substate.configured_wavelengths)
97
+ > MAXIMUM_CSV_FILE_LIMIT
98
+ ):
99
+ raise StorageLimitReachedError(
100
+ message=f"Attempt to write file {params.fileName} exceeds file creation limit of {MAXIMUM_CSV_FILE_LIMIT} files."
101
+ )
102
+
103
+ asbsorbance_result: Dict[int, Dict[str, float]] = {}
104
+ transform_results = []
105
+ # Handle the measurement and begin building data for return
106
+ if abs_reader is not None:
107
+ start_time = datetime.now()
108
+ results = await abs_reader.start_measure()
109
+ finish_time = datetime.now()
110
+ if abs_reader._measurement_config is not None:
111
+ sample_wavelengths = abs_reader._measurement_config.sample_wavelengths
112
+ for wavelength, result in zip(sample_wavelengths, results):
113
+ converted_values = (
114
+ self._state_view.modules.convert_absorbance_reader_data_points(
115
+ data=result
116
+ )
117
+ )
118
+ asbsorbance_result[wavelength] = converted_values
119
+ transform_results.append(
120
+ ReadData.construct(wavelength=wavelength, data=converted_values)
121
+ )
122
+ # Handle the virtual module case for data creation (all zeroes)
123
+ elif self._state_view.config.use_virtual_modules:
124
+ start_time = finish_time = datetime.now()
125
+ if abs_reader_substate.configured_wavelengths is not None:
126
+ for wavelength in abs_reader_substate.configured_wavelengths:
127
+ converted_values = (
128
+ self._state_view.modules.convert_absorbance_reader_data_points(
129
+ data=[0] * 96
130
+ )
131
+ )
132
+ asbsorbance_result[wavelength] = converted_values
133
+ transform_results.append(
134
+ ReadData.construct(wavelength=wavelength, data=converted_values)
135
+ )
136
+ else:
137
+ raise CannotPerformModuleAction(
138
+ "Plate Reader data cannot be requested with a module that has not been initialized."
139
+ )
140
+
141
+ # TODO (cb, 10-17-2024): FILE PROVIDER - Some day we may want to break the file provider behavior into a seperate API function.
142
+ # When this happens, we probably will to have the change the command results handler we utilize to track file IDs in engine.
143
+ # Today, the action handler for the FileStore looks for a ReadAbsorbanceResult command action, this will need to be delinked.
144
+
145
+ # Begin interfacing with the file provider if the user provided a filename
146
+ file_ids = []
147
+ if params.fileName is not None:
148
+ # Create the Plate Reader Transform
149
+ plate_read_result = PlateReaderData.construct(
150
+ read_results=transform_results,
151
+ reference_wavelength=abs_reader_substate.reference_wavelength,
152
+ start_time=start_time,
153
+ finish_time=finish_time,
154
+ serial_number=abs_reader.serial_number
155
+ if (abs_reader is not None and abs_reader.serial_number is not None)
156
+ else "VIRTUAL_SERIAL",
157
+ )
158
+
159
+ if isinstance(plate_read_result, PlateReaderData):
160
+ # Write a CSV file for each of the measurements taken
161
+ for measurement in plate_read_result.read_results:
162
+ file_id = await self._file_provider.write_csv(
163
+ write_data=plate_read_result.build_generic_csv(
164
+ filename=params.fileName,
165
+ measurement=measurement,
166
+ )
167
+ )
168
+ file_ids.append(file_id)
169
+
170
+ # Return success data to api
171
+ return SuccessData(
172
+ public=ReadAbsorbanceResult(
173
+ data=asbsorbance_result, fileIds=file_ids
174
+ ),
175
+ )
176
+
177
+ return SuccessData(
178
+ public=ReadAbsorbanceResult(data=asbsorbance_result, fileIds=file_ids),
179
+ )
180
+
181
+
182
+ class ReadAbsorbance(
183
+ BaseCommand[ReadAbsorbanceParams, ReadAbsorbanceResult, ErrorOccurrence]
184
+ ):
185
+ """A command to execute an Absorbance Reader measurement."""
186
+
187
+ commandType: ReadAbsorbanceCommandType = "absorbanceReader/read"
188
+ params: ReadAbsorbanceParams
189
+ result: Optional[ReadAbsorbanceResult]
190
+
191
+ _ImplementationCls: Type[ReadAbsorbanceImpl] = ReadAbsorbanceImpl
192
+
193
+
194
+ class ReadAbsorbanceCreate(BaseCommandCreate[ReadAbsorbanceParams]):
195
+ """A request to execute an Absorbance Reader measurement."""
196
+
197
+ commandType: ReadAbsorbanceCommandType = "absorbanceReader/read"
198
+ params: ReadAbsorbanceParams
199
+
200
+ _CommandCls: Type[ReadAbsorbance] = ReadAbsorbance