opentrons 8.3.1a1__py2.py3-none-any.whl → 8.4.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.

Potentially problematic release.


This version of opentrons might be problematic. Click here for more details.

Files changed (191) hide show
  1. opentrons/calibration_storage/ot2/mark_bad_calibration.py +2 -0
  2. opentrons/calibration_storage/ot2/tip_length.py +6 -6
  3. opentrons/config/advanced_settings.py +9 -11
  4. opentrons/config/feature_flags.py +0 -4
  5. opentrons/config/reset.py +7 -2
  6. opentrons/drivers/asyncio/communication/__init__.py +2 -0
  7. opentrons/drivers/asyncio/communication/async_serial.py +4 -0
  8. opentrons/drivers/asyncio/communication/errors.py +41 -8
  9. opentrons/drivers/asyncio/communication/serial_connection.py +36 -10
  10. opentrons/drivers/flex_stacker/__init__.py +9 -3
  11. opentrons/drivers/flex_stacker/abstract.py +140 -15
  12. opentrons/drivers/flex_stacker/driver.py +593 -47
  13. opentrons/drivers/flex_stacker/errors.py +64 -0
  14. opentrons/drivers/flex_stacker/simulator.py +222 -24
  15. opentrons/drivers/flex_stacker/types.py +211 -15
  16. opentrons/drivers/flex_stacker/utils.py +19 -0
  17. opentrons/execute.py +4 -2
  18. opentrons/hardware_control/api.py +5 -0
  19. opentrons/hardware_control/backends/flex_protocol.py +4 -0
  20. opentrons/hardware_control/backends/ot3controller.py +12 -1
  21. opentrons/hardware_control/backends/ot3simulator.py +3 -0
  22. opentrons/hardware_control/backends/subsystem_manager.py +7 -2
  23. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +10 -6
  24. opentrons/hardware_control/instruments/ot3/pipette_handler.py +59 -6
  25. opentrons/hardware_control/modules/__init__.py +12 -1
  26. opentrons/hardware_control/modules/absorbance_reader.py +11 -9
  27. opentrons/hardware_control/modules/flex_stacker.py +498 -0
  28. opentrons/hardware_control/modules/heater_shaker.py +12 -10
  29. opentrons/hardware_control/modules/magdeck.py +5 -1
  30. opentrons/hardware_control/modules/tempdeck.py +5 -1
  31. opentrons/hardware_control/modules/thermocycler.py +15 -14
  32. opentrons/hardware_control/modules/types.py +191 -1
  33. opentrons/hardware_control/modules/utils.py +3 -0
  34. opentrons/hardware_control/motion_utilities.py +20 -0
  35. opentrons/hardware_control/ot3api.py +145 -15
  36. opentrons/hardware_control/protocols/liquid_handler.py +47 -1
  37. opentrons/hardware_control/types.py +6 -0
  38. opentrons/legacy_commands/commands.py +19 -3
  39. opentrons/legacy_commands/helpers.py +15 -0
  40. opentrons/legacy_commands/types.py +3 -2
  41. opentrons/protocol_api/__init__.py +2 -0
  42. opentrons/protocol_api/_liquid.py +39 -8
  43. opentrons/protocol_api/_liquid_properties.py +20 -19
  44. opentrons/protocol_api/_transfer_liquid_validation.py +91 -0
  45. opentrons/protocol_api/core/common.py +3 -1
  46. opentrons/protocol_api/core/engine/deck_conflict.py +11 -1
  47. opentrons/protocol_api/core/engine/instrument.py +1233 -65
  48. opentrons/protocol_api/core/engine/labware.py +8 -4
  49. opentrons/protocol_api/core/engine/load_labware_params.py +68 -10
  50. opentrons/protocol_api/core/engine/module_core.py +118 -2
  51. opentrons/protocol_api/core/engine/protocol.py +253 -11
  52. opentrons/protocol_api/core/engine/stringify.py +19 -8
  53. opentrons/protocol_api/core/engine/transfer_components_executor.py +853 -0
  54. opentrons/protocol_api/core/engine/well.py +60 -5
  55. opentrons/protocol_api/core/instrument.py +65 -19
  56. opentrons/protocol_api/core/labware.py +6 -2
  57. opentrons/protocol_api/core/legacy/labware_offset_provider.py +7 -3
  58. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +69 -21
  59. opentrons/protocol_api/core/legacy/legacy_labware_core.py +8 -4
  60. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +36 -0
  61. opentrons/protocol_api/core/legacy/legacy_well_core.py +25 -1
  62. opentrons/protocol_api/core/legacy/load_info.py +4 -12
  63. opentrons/protocol_api/core/legacy/module_geometry.py +6 -1
  64. opentrons/protocol_api/core/legacy/well_geometry.py +3 -3
  65. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +67 -21
  66. opentrons/protocol_api/core/module.py +43 -0
  67. opentrons/protocol_api/core/protocol.py +33 -0
  68. opentrons/protocol_api/core/well.py +21 -1
  69. opentrons/protocol_api/instrument_context.py +245 -123
  70. opentrons/protocol_api/labware.py +75 -11
  71. opentrons/protocol_api/module_contexts.py +140 -0
  72. opentrons/protocol_api/protocol_context.py +156 -16
  73. opentrons/protocol_api/validation.py +51 -41
  74. opentrons/protocol_engine/__init__.py +21 -2
  75. opentrons/protocol_engine/actions/actions.py +5 -5
  76. opentrons/protocol_engine/clients/sync_client.py +6 -0
  77. opentrons/protocol_engine/commands/__init__.py +30 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +0 -1
  79. opentrons/protocol_engine/commands/air_gap_in_place.py +3 -2
  80. opentrons/protocol_engine/commands/aspirate.py +6 -2
  81. opentrons/protocol_engine/commands/aspirate_in_place.py +3 -1
  82. opentrons/protocol_engine/commands/aspirate_while_tracking.py +237 -0
  83. opentrons/protocol_engine/commands/blow_out.py +2 -0
  84. opentrons/protocol_engine/commands/blow_out_in_place.py +4 -1
  85. opentrons/protocol_engine/commands/command_unions.py +69 -0
  86. opentrons/protocol_engine/commands/configure_for_volume.py +3 -0
  87. opentrons/protocol_engine/commands/dispense.py +3 -1
  88. opentrons/protocol_engine/commands/dispense_in_place.py +3 -0
  89. opentrons/protocol_engine/commands/dispense_while_tracking.py +240 -0
  90. opentrons/protocol_engine/commands/drop_tip.py +23 -1
  91. opentrons/protocol_engine/commands/evotip_dispense.py +6 -7
  92. opentrons/protocol_engine/commands/evotip_seal_pipette.py +2 -9
  93. opentrons/protocol_engine/commands/evotip_unseal_pipette.py +1 -7
  94. opentrons/protocol_engine/commands/flex_stacker/__init__.py +106 -0
  95. opentrons/protocol_engine/commands/flex_stacker/close_latch.py +72 -0
  96. opentrons/protocol_engine/commands/flex_stacker/common.py +15 -0
  97. opentrons/protocol_engine/commands/flex_stacker/empty.py +161 -0
  98. opentrons/protocol_engine/commands/flex_stacker/fill.py +164 -0
  99. opentrons/protocol_engine/commands/flex_stacker/open_latch.py +70 -0
  100. opentrons/protocol_engine/commands/flex_stacker/prepare_shuttle.py +112 -0
  101. opentrons/protocol_engine/commands/flex_stacker/retrieve.py +394 -0
  102. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +190 -0
  103. opentrons/protocol_engine/commands/flex_stacker/store.py +288 -0
  104. opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
  105. opentrons/protocol_engine/commands/labware_handling_common.py +24 -0
  106. opentrons/protocol_engine/commands/liquid_probe.py +21 -12
  107. opentrons/protocol_engine/commands/load_labware.py +42 -39
  108. opentrons/protocol_engine/commands/load_lid.py +21 -13
  109. opentrons/protocol_engine/commands/load_lid_stack.py +130 -47
  110. opentrons/protocol_engine/commands/load_module.py +18 -17
  111. opentrons/protocol_engine/commands/load_pipette.py +3 -0
  112. opentrons/protocol_engine/commands/move_labware.py +139 -20
  113. opentrons/protocol_engine/commands/pick_up_tip.py +5 -2
  114. opentrons/protocol_engine/commands/pipetting_common.py +154 -7
  115. opentrons/protocol_engine/commands/prepare_to_aspirate.py +3 -1
  116. opentrons/protocol_engine/commands/reload_labware.py +6 -19
  117. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +3 -1
  118. opentrons/protocol_engine/errors/__init__.py +8 -0
  119. opentrons/protocol_engine/errors/exceptions.py +50 -0
  120. opentrons/protocol_engine/execution/equipment.py +123 -106
  121. opentrons/protocol_engine/execution/labware_movement.py +8 -6
  122. opentrons/protocol_engine/execution/pipetting.py +233 -26
  123. opentrons/protocol_engine/execution/tip_handler.py +14 -5
  124. opentrons/protocol_engine/labware_offset_standardization.py +173 -0
  125. opentrons/protocol_engine/protocol_engine.py +22 -13
  126. opentrons/protocol_engine/resources/deck_configuration_provider.py +94 -2
  127. opentrons/protocol_engine/resources/deck_data_provider.py +1 -1
  128. opentrons/protocol_engine/resources/labware_data_provider.py +32 -12
  129. opentrons/protocol_engine/resources/labware_validation.py +7 -5
  130. opentrons/protocol_engine/slot_standardization.py +11 -23
  131. opentrons/protocol_engine/state/addressable_areas.py +84 -46
  132. opentrons/protocol_engine/state/frustum_helpers.py +26 -10
  133. opentrons/protocol_engine/state/geometry.py +683 -100
  134. opentrons/protocol_engine/state/labware.py +252 -55
  135. opentrons/protocol_engine/state/module_substates/__init__.py +4 -0
  136. opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +68 -0
  137. opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +22 -0
  138. opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +13 -0
  139. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +20 -0
  140. opentrons/protocol_engine/state/modules.py +178 -52
  141. opentrons/protocol_engine/state/pipettes.py +54 -0
  142. opentrons/protocol_engine/state/state.py +1 -1
  143. opentrons/protocol_engine/state/tips.py +14 -0
  144. opentrons/protocol_engine/state/update_types.py +180 -25
  145. opentrons/protocol_engine/state/wells.py +54 -8
  146. opentrons/protocol_engine/types/__init__.py +292 -0
  147. opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
  148. opentrons/protocol_engine/types/command_annotations.py +53 -0
  149. opentrons/protocol_engine/types/deck_configuration.py +72 -0
  150. opentrons/protocol_engine/types/execution.py +96 -0
  151. opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
  152. opentrons/protocol_engine/types/instrument.py +47 -0
  153. opentrons/protocol_engine/types/instrument_sensors.py +47 -0
  154. opentrons/protocol_engine/types/labware.py +110 -0
  155. opentrons/protocol_engine/types/labware_movement.py +22 -0
  156. opentrons/protocol_engine/types/labware_offset_location.py +108 -0
  157. opentrons/protocol_engine/types/labware_offset_vector.py +33 -0
  158. opentrons/protocol_engine/types/liquid.py +40 -0
  159. opentrons/protocol_engine/types/liquid_class.py +59 -0
  160. opentrons/protocol_engine/types/liquid_handling.py +13 -0
  161. opentrons/protocol_engine/types/liquid_level_detection.py +137 -0
  162. opentrons/protocol_engine/types/location.py +193 -0
  163. opentrons/protocol_engine/types/module.py +269 -0
  164. opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
  165. opentrons/protocol_engine/types/run_time_parameters.py +133 -0
  166. opentrons/protocol_engine/types/tip.py +18 -0
  167. opentrons/protocol_engine/types/util.py +21 -0
  168. opentrons/protocol_engine/types/well_position.py +107 -0
  169. opentrons/protocol_reader/extract_labware_definitions.py +7 -3
  170. opentrons/protocol_reader/file_format_validator.py +5 -3
  171. opentrons/protocol_runner/json_translator.py +4 -2
  172. opentrons/protocol_runner/legacy_command_mapper.py +6 -2
  173. opentrons/protocol_runner/run_orchestrator.py +4 -1
  174. opentrons/protocols/advanced_control/transfers/common.py +48 -1
  175. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +204 -0
  176. opentrons/protocols/api_support/definitions.py +1 -1
  177. opentrons/protocols/api_support/instrument.py +16 -3
  178. opentrons/protocols/labware.py +5 -6
  179. opentrons/protocols/models/__init__.py +0 -21
  180. opentrons/simulate.py +4 -2
  181. opentrons/types.py +15 -6
  182. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/METADATA +4 -4
  183. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/RECORD +187 -147
  184. opentrons/calibration_storage/ot2/models/defaults.py +0 -0
  185. opentrons/calibration_storage/ot3/models/defaults.py +0 -0
  186. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  187. opentrons/protocol_engine/types.py +0 -1311
  188. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/LICENSE +0 -0
  189. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/WHEEL +0 -0
  190. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/entry_points.txt +0 -0
  191. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,288 @@
1
+ """Command models to retrieve a labware from a Flex Stacker."""
2
+
3
+ from __future__ import annotations
4
+ from typing import Optional, Literal, TYPE_CHECKING, Type, Union
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from ..command import (
9
+ AbstractCommandImpl,
10
+ BaseCommand,
11
+ BaseCommandCreate,
12
+ SuccessData,
13
+ DefinedErrorData,
14
+ )
15
+ from ..flex_stacker.common import FlexStackerStallOrCollisionError
16
+ from ...errors import (
17
+ ErrorOccurrence,
18
+ CannotPerformModuleAction,
19
+ LabwareNotLoadedOnModuleError,
20
+ FlexStackerLabwarePoolNotYetDefinedError,
21
+ )
22
+ from ...resources import ModelUtils
23
+ from ...state import update_types
24
+ from ...types import (
25
+ LabwareLocationSequence,
26
+ InStackerHopperLocation,
27
+ )
28
+
29
+ from opentrons_shared_data.errors.exceptions import FlexStackerStallError
30
+ from opentrons.calibration_storage.helpers import uri_from_details
31
+
32
+
33
+ if TYPE_CHECKING:
34
+ from opentrons.protocol_engine.state.state import StateView
35
+ from opentrons.protocol_engine.state.module_substates import FlexStackerSubState
36
+ from opentrons.protocol_engine.execution import EquipmentHandler
37
+
38
+
39
+ StoreCommandType = Literal["flexStacker/store"]
40
+
41
+
42
+ class StoreParams(BaseModel):
43
+ """Input parameters for a labware storage command."""
44
+
45
+ moduleId: str = Field(
46
+ ...,
47
+ description="Unique ID of the flex stacker.",
48
+ )
49
+
50
+
51
+ class StoreResult(BaseModel):
52
+ """Result data from a labware storage command."""
53
+
54
+ eventualDestinationLocationSequence: LabwareLocationSequence | None = Field(
55
+ None,
56
+ description=(
57
+ "The full location in which all labware moved by this command will eventually reside."
58
+ ),
59
+ )
60
+ primaryOriginLocationSequence: LabwareLocationSequence | None = Field(
61
+ None, description=("The origin location of the primary labware.")
62
+ )
63
+ primaryLabwareId: str | None = Field(
64
+ None, description="The primary labware in the stack that was stored."
65
+ )
66
+ adapterOriginLocationSequence: LabwareLocationSequence | None = Field(
67
+ None, description=("The origin location of the adapter labware, if any.")
68
+ )
69
+ adapterLabwareId: str | None = Field(
70
+ None, description="The adapter in the stack that was stored, if any."
71
+ )
72
+ lidOriginLocationSequence: LabwareLocationSequence | None = Field(
73
+ None, description=("The origin location of the lid labware, if any.")
74
+ )
75
+ lidLabwareId: str | None = Field(
76
+ None, description="The lid in the stack that was stored, if any."
77
+ )
78
+ primaryLabwareURI: str = Field(
79
+ ...,
80
+ description="The labware definition URI of the primary labware.",
81
+ )
82
+ adapterLabwareURI: str | None = Field(
83
+ None,
84
+ description="The labware definition URI of the adapter labware.",
85
+ )
86
+ lidLabwareURI: str | None = Field(
87
+ None,
88
+ description="The labware definition URI of the lid labware.",
89
+ )
90
+
91
+
92
+ _ExecuteReturn = Union[
93
+ SuccessData[StoreResult],
94
+ DefinedErrorData[FlexStackerStallOrCollisionError],
95
+ ]
96
+
97
+
98
+ class StoreImpl(AbstractCommandImpl[StoreParams, _ExecuteReturn]):
99
+ """Implementation of a labware storage command."""
100
+
101
+ def __init__(
102
+ self,
103
+ state_view: StateView,
104
+ equipment: EquipmentHandler,
105
+ model_utils: ModelUtils,
106
+ **kwargs: object,
107
+ ) -> None:
108
+ self._state_view = state_view
109
+ self._equipment = equipment
110
+ self._model_utils = model_utils
111
+
112
+ def _verify_labware_to_store(
113
+ self, params: StoreParams, stacker_state: FlexStackerSubState
114
+ ) -> tuple[str, str | None, str | None]:
115
+ try:
116
+ bottom_id = self._state_view.labware.get_id_by_module(params.moduleId)
117
+ except LabwareNotLoadedOnModuleError:
118
+ raise CannotPerformModuleAction(
119
+ "Cannot store labware if Flex Stacker carriage is empty"
120
+ )
121
+ labware_ids = self._state_view.labware.get_labware_stack_from_parent(bottom_id)
122
+ labware_defs = [
123
+ self._state_view.labware.get_definition(id) for id in labware_ids
124
+ ]
125
+
126
+ lid_id: str | None = None
127
+
128
+ pool_list = stacker_state.get_pool_definition_ordered_list()
129
+ assert pool_list is not None
130
+ if len(labware_ids) != len(pool_list):
131
+ raise CannotPerformModuleAction(
132
+ "Cannot store labware stack that does not correspond with Flex Stacker configuration"
133
+ )
134
+ if stacker_state.pool_lid_definition is not None:
135
+ if labware_defs[-1] != stacker_state.pool_lid_definition:
136
+ raise CannotPerformModuleAction(
137
+ "Cannot store labware stack that does not correspond with Flex Stacker configuration"
138
+ )
139
+ lid_id = labware_ids[-1]
140
+
141
+ if stacker_state.pool_adapter_definition is not None:
142
+ if (
143
+ labware_defs[0] != stacker_state.pool_adapter_definition
144
+ or labware_defs[1] != stacker_state.pool_primary_definition
145
+ ):
146
+ raise CannotPerformModuleAction(
147
+ "Cannot store labware stack that does not correspond with Flex Stacker configuration"
148
+ )
149
+ else:
150
+ return labware_ids[1], labware_ids[0], lid_id
151
+ else:
152
+ if labware_defs[0] != stacker_state.pool_primary_definition:
153
+ raise CannotPerformModuleAction(
154
+ "Cannot store labware stack that does not correspond with Flex Stacker configuration"
155
+ )
156
+ return labware_ids[0], None, lid_id
157
+
158
+ async def execute(self, params: StoreParams) -> _ExecuteReturn:
159
+ """Execute the labware storage command."""
160
+ stacker_state = self._state_view.modules.get_flex_stacker_substate(
161
+ params.moduleId
162
+ )
163
+
164
+ if stacker_state.pool_count == stacker_state.max_pool_count:
165
+ raise CannotPerformModuleAction(
166
+ "Cannot store labware in Flex Stacker while it is full"
167
+ )
168
+
169
+ pool_definitions = stacker_state.get_pool_definition_ordered_list()
170
+ if pool_definitions is None:
171
+ raise FlexStackerLabwarePoolNotYetDefinedError(
172
+ message="The Flex Stacker has not been configured yet and cannot be filled."
173
+ )
174
+
175
+ primary_id, maybe_adapter_id, maybe_lid_id = self._verify_labware_to_store(
176
+ params, stacker_state
177
+ )
178
+
179
+ # Allow propagation of ModuleNotAttachedError.
180
+ stacker_hw = self._equipment.get_module_hardware_api(stacker_state.module_id)
181
+
182
+ eventual_target_location_sequence = (
183
+ self._state_view.geometry.get_predicted_location_sequence(
184
+ InStackerHopperLocation(moduleId=params.moduleId)
185
+ )
186
+ )
187
+ stack_height = self._state_view.geometry.get_height_of_labware_stack(
188
+ pool_definitions
189
+ )
190
+
191
+ state_update = update_types.StateUpdate()
192
+ try:
193
+ if stacker_hw is not None:
194
+ await stacker_hw.store_labware(labware_height=stack_height)
195
+ except FlexStackerStallError as e:
196
+ return DefinedErrorData(
197
+ public=FlexStackerStallOrCollisionError(
198
+ id=self._model_utils.generate_id(),
199
+ createdAt=self._model_utils.get_timestamp(),
200
+ wrappedErrors=[
201
+ ErrorOccurrence.from_failed(
202
+ id=self._model_utils.generate_id(),
203
+ createdAt=self._model_utils.get_timestamp(),
204
+ error=e,
205
+ )
206
+ ],
207
+ ),
208
+ )
209
+
210
+ id_list = [
211
+ id for id in (primary_id, maybe_adapter_id, maybe_lid_id) if id is not None
212
+ ]
213
+
214
+ state_update.set_batch_labware_location(
215
+ new_locations_by_id={
216
+ id: InStackerHopperLocation(moduleId=params.moduleId) for id in id_list
217
+ },
218
+ new_offset_ids_by_id={id: None for id in id_list},
219
+ )
220
+
221
+ state_update.update_flex_stacker_labware_pool_count(
222
+ module_id=params.moduleId, count=stacker_state.pool_count + 1
223
+ )
224
+ if stacker_state.pool_primary_definition is None:
225
+ raise FlexStackerLabwarePoolNotYetDefinedError(
226
+ "The Primary Labware must be defined in the stacker pool."
227
+ )
228
+
229
+ return SuccessData(
230
+ public=StoreResult(
231
+ eventualDestinationLocationSequence=eventual_target_location_sequence,
232
+ primaryOriginLocationSequence=self._state_view.geometry.get_location_sequence(
233
+ primary_id
234
+ ),
235
+ primaryLabwareId=primary_id,
236
+ adapterOriginLocationSequence=(
237
+ self._state_view.geometry.get_location_sequence(maybe_adapter_id)
238
+ if maybe_adapter_id is not None
239
+ else None
240
+ ),
241
+ adapterLabwareId=maybe_adapter_id,
242
+ lidOriginLocationSequence=(
243
+ self._state_view.geometry.get_location_sequence(maybe_lid_id)
244
+ if maybe_lid_id is not None
245
+ else None
246
+ ),
247
+ lidLabwareId=maybe_lid_id,
248
+ primaryLabwareURI=uri_from_details(
249
+ stacker_state.pool_primary_definition.namespace,
250
+ stacker_state.pool_primary_definition.parameters.loadName,
251
+ stacker_state.pool_primary_definition.version,
252
+ ),
253
+ adapterLabwareURI=uri_from_details(
254
+ stacker_state.pool_adapter_definition.namespace,
255
+ stacker_state.pool_adapter_definition.parameters.loadName,
256
+ stacker_state.pool_adapter_definition.version,
257
+ )
258
+ if stacker_state.pool_adapter_definition is not None
259
+ else None,
260
+ lidLabwareURI=uri_from_details(
261
+ stacker_state.pool_lid_definition.namespace,
262
+ stacker_state.pool_lid_definition.parameters.loadName,
263
+ stacker_state.pool_lid_definition.version,
264
+ )
265
+ if stacker_state.pool_lid_definition is not None
266
+ else None,
267
+ ),
268
+ state_update=state_update,
269
+ )
270
+
271
+
272
+ class Store(BaseCommand[StoreParams, StoreResult, ErrorOccurrence]):
273
+ """A command to store a labware in a Flex Stacker."""
274
+
275
+ commandType: StoreCommandType = "flexStacker/store"
276
+ params: StoreParams
277
+ result: Optional[StoreResult] = None
278
+
279
+ _ImplementationCls: Type[StoreImpl] = StoreImpl
280
+
281
+
282
+ class StoreCreate(BaseCommandCreate[StoreParams]):
283
+ """A request to execute a Flex Stacker store command."""
284
+
285
+ commandType: StoreCommandType = "flexStacker/store"
286
+ params: StoreParams
287
+
288
+ _CommandCls: Type[Store] = Store
@@ -5,6 +5,9 @@ import argparse
5
5
  import sys
6
6
  from opentrons.protocol_engine.commands.command_unions import CommandCreateAdapter
7
7
 
8
+ from opentrons_shared_data.command import get_newest_schema_version
9
+ from opentrons_shared_data.load import get_shared_data_root
10
+
8
11
 
9
12
  def generate_command_schema(version: str) -> str:
10
13
  """Generate a JSON Schema that all valid create commands can validate against."""
@@ -14,6 +17,13 @@ def generate_command_schema(version: str) -> str:
14
17
  return json.dumps(schema_as_dict, indent=2, sort_keys=True)
15
18
 
16
19
 
20
+ def write_command_schema(json_string: str, version: str) -> None:
21
+ """Write a JSON command schema to the shared-data command schema directory."""
22
+ path = get_shared_data_root() / "command" / "schemas" / f"{version}.json"
23
+ with open(path, "w") as schema_file:
24
+ schema_file.write(json_string)
25
+
26
+
17
27
  if __name__ == "__main__":
18
28
  parser = argparse.ArgumentParser(
19
29
  prog="generate_command_schema",
@@ -22,10 +32,29 @@ if __name__ == "__main__":
22
32
  parser.add_argument(
23
33
  "version",
24
34
  type=str,
25
- help="The command schema version. This is a single integer (e.g. 7) that will be used to name the generated schema file",
35
+ nargs="?",
36
+ help="The command schema version. This is a single integer (e.g. 7) that will be used to name the generated"
37
+ " schema file. If not included, it will automatically use the latest version in shared-data.",
38
+ )
39
+ parser.add_argument(
40
+ "--overwrite-shared-data",
41
+ action="store_true",
42
+ help="If used, overwrites the specified or automatically chosen command schema version in shared-data."
43
+ " If not included, the generated schema will be printed to stdout.",
26
44
  )
27
45
  args = parser.parse_args()
28
- print(generate_command_schema(args.version))
46
+
47
+ if args.version is None:
48
+ version_string = get_newest_schema_version()
49
+ else:
50
+ version_string = args.version
51
+
52
+ command_schema = generate_command_schema(version_string)
53
+
54
+ if args.overwrite_shared_data:
55
+ write_command_schema(command_schema, version_string)
56
+ else:
57
+ print(command_schema)
29
58
 
30
59
  sys.exit()
31
60
 
@@ -0,0 +1,24 @@
1
+ """Helpers for commands that alter the position of labware."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from ..types import LabwareLocationSequence
6
+
7
+
8
+ class LabwareHandlingResultMixin(BaseModel):
9
+ """A result for commands that create a labware entity."""
10
+
11
+ labwareId: str = Field(..., description="The id of the labware.")
12
+ locationSequence: LabwareLocationSequence | None = Field(
13
+ None,
14
+ description="The full location down to the deck on which this labware exists.",
15
+ )
16
+
17
+
18
+ class LabwarePositionResultMixin(LabwareHandlingResultMixin):
19
+ """A result for commands that create an offsetable labware entity."""
20
+
21
+ offsetId: str | None = Field(
22
+ None,
23
+ description="An ID referencing the labware offset that will apply to this labware in this location.",
24
+ )
@@ -23,6 +23,7 @@ from opentrons_shared_data.errors.exceptions import (
23
23
  )
24
24
 
25
25
  from ..types import DeckPoint
26
+ from ..types.liquid_level_detection import LiquidTrackingType
26
27
  from .pipetting_common import (
27
28
  LiquidNotFoundError,
28
29
  PipetteIdMixin,
@@ -80,7 +81,7 @@ class TryLiquidProbeParams(_CommonParams):
80
81
  class LiquidProbeResult(DestinationPositionResult):
81
82
  """Result data from the execution of a `liquidProbe` command."""
82
83
 
83
- z_position: float = Field(
84
+ z_position: LiquidTrackingType = Field(
84
85
  ..., description="The Z coordinate, in mm, of the found liquid in deck space."
85
86
  )
86
87
  # New fields should use camelCase. z_position is snake_case for historical reasons.
@@ -89,7 +90,7 @@ class LiquidProbeResult(DestinationPositionResult):
89
90
  class TryLiquidProbeResult(DestinationPositionResult):
90
91
  """Result data from the execution of a `tryLiquidProbe` command."""
91
92
 
92
- z_position: float | SkipJsonSchema[None] = Field(
93
+ z_position: Union[LiquidTrackingType, SkipJsonSchema[None]] = Field(
93
94
  ...,
94
95
  description=(
95
96
  "The Z coordinate, in mm, of the found liquid in deck space."
@@ -116,8 +117,7 @@ class _ExecuteCommonResult(NamedTuple):
116
117
  # If the probe succeeded, the z_pos that it returned.
117
118
  # Or, if the probe found no liquid, the error representing that,
118
119
  # so calling code can propagate those details up.
119
- z_pos_or_error: float | PipetteLiquidNotFoundError | PipetteOverpressureError
120
-
120
+ z_pos_or_error: LiquidTrackingType | PipetteLiquidNotFoundError | PipetteOverpressureError
121
121
  state_update: update_types.StateUpdate
122
122
  deck_point: DeckPoint
123
123
 
@@ -190,6 +190,9 @@ async def _execute_common( # noqa: C901
190
190
  well_location=params.wellLocation,
191
191
  )
192
192
  except PipetteLiquidNotFoundError as exception:
193
+ move_result.state_update.set_pipette_ready_to_aspirate(
194
+ pipette_id=pipette_id, ready_to_aspirate=True
195
+ )
193
196
  return _ExecuteCommonResult(
194
197
  z_pos_or_error=exception,
195
198
  state_update=move_result.state_update,
@@ -223,6 +226,9 @@ async def _execute_common( # noqa: C901
223
226
  ),
224
227
  )
225
228
  else:
229
+ move_result.state_update.set_pipette_ready_to_aspirate(
230
+ pipette_id=pipette_id, ready_to_aspirate=True
231
+ )
226
232
  return _ExecuteCommonResult(
227
233
  z_pos_or_error=z_pos,
228
234
  state_update=move_result.state_update,
@@ -303,12 +309,13 @@ class LiquidProbeImplementation(
303
309
  )
304
310
  else:
305
311
  try:
306
- well_volume: float | update_types.ClearType = (
307
- self._state_view.geometry.get_well_volume_at_height(
308
- labware_id=params.labwareId,
309
- well_name=params.wellName,
310
- height=z_pos_or_error,
311
- )
312
+ well_volume: Union[
313
+ LiquidTrackingType,
314
+ update_types.ClearType,
315
+ ] = self._state_view.geometry.get_well_volume_at_height(
316
+ labware_id=params.labwareId,
317
+ well_name=params.wellName,
318
+ height=z_pos_or_error,
312
319
  )
313
320
  except IncompleteLabwareDefinitionError:
314
321
  well_volume = update_types.CLEAR
@@ -370,7 +377,10 @@ class TryLiquidProbeImplementation(
370
377
  z_pos_or_error, (PipetteLiquidNotFoundError, PipetteOverpressureError)
371
378
  ):
372
379
  z_pos = None
373
- well_volume: float | update_types.ClearType = update_types.CLEAR
380
+ well_volume: Union[
381
+ LiquidTrackingType,
382
+ update_types.ClearType,
383
+ ] = update_types.CLEAR
374
384
  else:
375
385
  z_pos = z_pos_or_error
376
386
  try:
@@ -387,7 +397,6 @@ class TryLiquidProbeImplementation(
387
397
  volume=well_volume,
388
398
  last_probed=self._model_utils.get_timestamp(),
389
399
  )
390
-
391
400
  return SuccessData(
392
401
  public=TryLiquidProbeResult(
393
402
  z_position=z_pos,
@@ -1,25 +1,28 @@
1
1
  """Load labware command request, result, and implementation models."""
2
+
2
3
  from __future__ import annotations
3
4
  from typing import TYPE_CHECKING, Optional, Type, Any
4
5
 
5
6
  from pydantic import BaseModel, Field
6
7
  from pydantic.json_schema import SkipJsonSchema
7
- from typing_extensions import Literal
8
+ from typing_extensions import Literal, TypeGuard
8
9
 
9
10
  from opentrons_shared_data.labware.labware_definition import LabwareDefinition
10
11
 
11
12
  from ..errors import LabwareIsNotAllowedInLocationError
12
13
  from ..resources import labware_validation, fixture_validation
13
14
  from ..types import (
14
- LabwareLocation,
15
+ LoadableLabwareLocation,
15
16
  ModuleLocation,
16
17
  ModuleModel,
17
18
  OnLabwareLocation,
18
19
  DeckSlotLocation,
19
20
  AddressableAreaLocation,
21
+ LoadedModule,
20
22
  )
21
23
 
22
24
  from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
25
+ from .labware_handling_common import LabwarePositionResultMixin
23
26
  from ..errors.error_occurrence import ErrorOccurrence
24
27
  from ..state.update_types import StateUpdate
25
28
 
@@ -38,7 +41,7 @@ def _remove_default(s: dict[str, Any]) -> None:
38
41
  class LoadLabwareParams(BaseModel):
39
42
  """Payload required to load a labware into a slot."""
40
43
 
41
- location: LabwareLocation = Field(
44
+ location: LoadableLabwareLocation = Field(
42
45
  ...,
43
46
  description="Location the labware should be loaded into.",
44
47
  )
@@ -71,30 +74,13 @@ class LoadLabwareParams(BaseModel):
71
74
  )
72
75
 
73
76
 
74
- class LoadLabwareResult(BaseModel):
77
+ class LoadLabwareResult(LabwarePositionResultMixin):
75
78
  """Result data from the execution of a LoadLabware command."""
76
79
 
77
- labwareId: str = Field(
78
- ...,
79
- description="An ID to reference this labware in subsequent commands.",
80
- )
81
80
  definition: LabwareDefinition = Field(
82
81
  ...,
83
82
  description="The full definition data for this labware.",
84
83
  )
85
- offsetId: Optional[str] = Field(
86
- # Default `None` instead of `...` so this field shows up as non-required in
87
- # OpenAPI. The server is allowed to omit it or make it null.
88
- None,
89
- description=(
90
- "An ID referencing the labware offset that will apply"
91
- " to the newly-placed labware."
92
- " This offset will be in effect until the labware is moved"
93
- " with a `moveLabware` command."
94
- " Null or undefined means no offset applies,"
95
- " so the default of (0, 0, 0) will be used."
96
- ),
97
- )
98
84
 
99
85
 
100
86
  class LoadLabwareImplementation(
@@ -108,6 +94,15 @@ class LoadLabwareImplementation(
108
94
  self._equipment = equipment
109
95
  self._state_view = state_view
110
96
 
97
+ def _is_loading_to_module(
98
+ self, location: LoadableLabwareLocation, module_model: ModuleModel
99
+ ) -> TypeGuard[ModuleLocation]:
100
+ if not isinstance(location, ModuleLocation):
101
+ return False
102
+
103
+ module: LoadedModule = self._state_view.modules.get(location.moduleId)
104
+ return module.model == module_model
105
+
111
106
  async def execute(
112
107
  self, params: LoadLabwareParams
113
108
  ) -> SuccessData[LoadLabwareResult]:
@@ -148,6 +143,7 @@ class LoadLabwareImplementation(
148
143
  verified_location = self._state_view.geometry.ensure_location_not_occupied(
149
144
  params.location
150
145
  )
146
+
151
147
  loaded_labware = await self._equipment.load_labware(
152
148
  load_name=params.loadName,
153
149
  namespace=params.namespace,
@@ -164,34 +160,38 @@ class LoadLabwareImplementation(
164
160
  display_name=params.displayName,
165
161
  )
166
162
 
167
- # TODO(jbl 2023-06-23) these validation checks happen after the labware is loaded, because they rely on
168
- # on the definition. In practice this will not cause any issues since they will raise protocol ending
169
- # exception, but for correctness should be refactored to do this check beforehand.
170
163
  if isinstance(verified_location, OnLabwareLocation):
171
164
  self._state_view.labware.raise_if_labware_cannot_be_stacked(
172
165
  top_labware_definition=loaded_labware.definition,
173
166
  bottom_labware_id=verified_location.labwareId,
174
167
  )
175
168
  # Validate load location is valid for lids
176
- if (
177
- labware_validation.validate_definition_is_lid(
178
- definition=loaded_labware.definition
179
- )
180
- and loaded_labware.definition.compatibleParentLabware is not None
181
- and self._state_view.labware.get_load_name(verified_location.labwareId)
182
- not in loaded_labware.definition.compatibleParentLabware
169
+ if labware_validation.validate_definition_is_lid(
170
+ definition=loaded_labware.definition
183
171
  ):
184
- raise ValueError(
185
- f"Labware Lid {params.loadName} may not be loaded on parent labware {self._state_view.labware.get_display_name(verified_location.labwareId)}."
172
+ # This parent is assumed to be compatible, unless the lid enumerates
173
+ # all its compatible parents and this parent is missing from the list.
174
+ parent_is_incompatible = (
175
+ loaded_labware.definition.compatibleParentLabware is not None
176
+ and self._state_view.labware.get_load_name(
177
+ verified_location.labwareId
178
+ )
179
+ not in loaded_labware.definition.compatibleParentLabware
186
180
  )
187
181
 
182
+ if parent_is_incompatible:
183
+ raise ValueError(
184
+ f"Labware Lid {params.loadName} may not be loaded on parent labware"
185
+ f" {self._state_view.labware.get_display_name(verified_location.labwareId)}."
186
+ )
187
+
188
188
  # Validate labware for the absorbance reader
189
- elif isinstance(params.location, ModuleLocation):
190
- module = self._state_view.modules.get(params.location.moduleId)
191
- if module is not None and module.model == ModuleModel.ABSORBANCE_READER_V1:
192
- self._state_view.labware.raise_if_labware_incompatible_with_plate_reader(
193
- loaded_labware.definition
194
- )
189
+ if self._is_loading_to_module(
190
+ params.location, ModuleModel.ABSORBANCE_READER_V1
191
+ ):
192
+ self._state_view.labware.raise_if_labware_incompatible_with_plate_reader(
193
+ loaded_labware.definition
194
+ )
195
195
 
196
196
  self._state_view.labware.raise_if_labware_cannot_be_ondeck(
197
197
  location=params.location, labware_definition=loaded_labware.definition
@@ -202,6 +202,9 @@ class LoadLabwareImplementation(
202
202
  labwareId=loaded_labware.labware_id,
203
203
  definition=loaded_labware.definition,
204
204
  offsetId=loaded_labware.offsetId,
205
+ locationSequence=self._state_view.geometry.get_predicted_location_sequence(
206
+ verified_location,
207
+ ),
205
208
  ),
206
209
  state_update=state_update,
207
210
  )