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

Potentially problematic release.


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

Files changed (196) 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 +8 -4
  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 +102 -5
  39. opentrons/legacy_commands/helpers.py +74 -1
  40. opentrons/legacy_commands/types.py +33 -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 +1356 -107
  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/pipette_movement_conflict.py +6 -14
  52. opentrons/protocol_api/core/engine/protocol.py +253 -11
  53. opentrons/protocol_api/core/engine/stringify.py +19 -8
  54. opentrons/protocol_api/core/engine/transfer_components_executor.py +858 -0
  55. opentrons/protocol_api/core/engine/well.py +73 -5
  56. opentrons/protocol_api/core/instrument.py +71 -21
  57. opentrons/protocol_api/core/labware.py +6 -2
  58. opentrons/protocol_api/core/legacy/labware_offset_provider.py +7 -3
  59. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +76 -49
  60. opentrons/protocol_api/core/legacy/legacy_labware_core.py +8 -4
  61. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +36 -0
  62. opentrons/protocol_api/core/legacy/legacy_well_core.py +27 -2
  63. opentrons/protocol_api/core/legacy/load_info.py +4 -12
  64. opentrons/protocol_api/core/legacy/module_geometry.py +6 -1
  65. opentrons/protocol_api/core/legacy/well_geometry.py +3 -3
  66. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +73 -23
  67. opentrons/protocol_api/core/module.py +43 -0
  68. opentrons/protocol_api/core/protocol.py +33 -0
  69. opentrons/protocol_api/core/well.py +23 -2
  70. opentrons/protocol_api/instrument_context.py +454 -150
  71. opentrons/protocol_api/labware.py +98 -50
  72. opentrons/protocol_api/module_contexts.py +140 -0
  73. opentrons/protocol_api/protocol_context.py +163 -19
  74. opentrons/protocol_api/validation.py +51 -41
  75. opentrons/protocol_engine/__init__.py +21 -2
  76. opentrons/protocol_engine/actions/actions.py +5 -5
  77. opentrons/protocol_engine/clients/sync_client.py +6 -0
  78. opentrons/protocol_engine/commands/__init__.py +66 -36
  79. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +0 -1
  80. opentrons/protocol_engine/commands/air_gap_in_place.py +3 -2
  81. opentrons/protocol_engine/commands/aspirate.py +6 -2
  82. opentrons/protocol_engine/commands/aspirate_in_place.py +3 -1
  83. opentrons/protocol_engine/commands/aspirate_while_tracking.py +210 -0
  84. opentrons/protocol_engine/commands/blow_out.py +2 -0
  85. opentrons/protocol_engine/commands/blow_out_in_place.py +4 -1
  86. opentrons/protocol_engine/commands/command_unions.py +102 -33
  87. opentrons/protocol_engine/commands/configure_for_volume.py +3 -0
  88. opentrons/protocol_engine/commands/dispense.py +3 -1
  89. opentrons/protocol_engine/commands/dispense_in_place.py +3 -0
  90. opentrons/protocol_engine/commands/dispense_while_tracking.py +204 -0
  91. opentrons/protocol_engine/commands/drop_tip.py +23 -1
  92. opentrons/protocol_engine/commands/flex_stacker/__init__.py +106 -0
  93. opentrons/protocol_engine/commands/flex_stacker/close_latch.py +72 -0
  94. opentrons/protocol_engine/commands/flex_stacker/common.py +15 -0
  95. opentrons/protocol_engine/commands/flex_stacker/empty.py +161 -0
  96. opentrons/protocol_engine/commands/flex_stacker/fill.py +164 -0
  97. opentrons/protocol_engine/commands/flex_stacker/open_latch.py +70 -0
  98. opentrons/protocol_engine/commands/flex_stacker/prepare_shuttle.py +112 -0
  99. opentrons/protocol_engine/commands/flex_stacker/retrieve.py +394 -0
  100. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +190 -0
  101. opentrons/protocol_engine/commands/flex_stacker/store.py +291 -0
  102. opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
  103. opentrons/protocol_engine/commands/labware_handling_common.py +29 -0
  104. opentrons/protocol_engine/commands/liquid_probe.py +27 -13
  105. opentrons/protocol_engine/commands/load_labware.py +42 -39
  106. opentrons/protocol_engine/commands/load_lid.py +21 -13
  107. opentrons/protocol_engine/commands/load_lid_stack.py +130 -47
  108. opentrons/protocol_engine/commands/load_module.py +18 -17
  109. opentrons/protocol_engine/commands/load_pipette.py +3 -0
  110. opentrons/protocol_engine/commands/move_labware.py +139 -20
  111. opentrons/protocol_engine/commands/move_to_well.py +5 -11
  112. opentrons/protocol_engine/commands/pick_up_tip.py +5 -2
  113. opentrons/protocol_engine/commands/pipetting_common.py +159 -8
  114. opentrons/protocol_engine/commands/prepare_to_aspirate.py +15 -5
  115. opentrons/protocol_engine/commands/{evotip_dispense.py → pressure_dispense.py} +33 -34
  116. opentrons/protocol_engine/commands/reload_labware.py +6 -19
  117. opentrons/protocol_engine/commands/{evotip_seal_pipette.py → seal_pipette_to_tip.py} +97 -76
  118. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +3 -1
  119. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +6 -1
  120. opentrons/protocol_engine/commands/{evotip_unseal_pipette.py → unseal_pipette_from_tip.py} +31 -40
  121. opentrons/protocol_engine/errors/__init__.py +10 -0
  122. opentrons/protocol_engine/errors/exceptions.py +62 -0
  123. opentrons/protocol_engine/execution/equipment.py +123 -106
  124. opentrons/protocol_engine/execution/labware_movement.py +8 -6
  125. opentrons/protocol_engine/execution/pipetting.py +235 -25
  126. opentrons/protocol_engine/execution/tip_handler.py +82 -32
  127. opentrons/protocol_engine/labware_offset_standardization.py +194 -0
  128. opentrons/protocol_engine/protocol_engine.py +22 -13
  129. opentrons/protocol_engine/resources/deck_configuration_provider.py +98 -2
  130. opentrons/protocol_engine/resources/deck_data_provider.py +1 -1
  131. opentrons/protocol_engine/resources/labware_data_provider.py +32 -12
  132. opentrons/protocol_engine/resources/labware_validation.py +7 -5
  133. opentrons/protocol_engine/slot_standardization.py +11 -23
  134. opentrons/protocol_engine/state/addressable_areas.py +84 -46
  135. opentrons/protocol_engine/state/frustum_helpers.py +36 -14
  136. opentrons/protocol_engine/state/geometry.py +892 -227
  137. opentrons/protocol_engine/state/labware.py +252 -55
  138. opentrons/protocol_engine/state/module_substates/__init__.py +4 -0
  139. opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +68 -0
  140. opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +22 -0
  141. opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +13 -0
  142. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +20 -0
  143. opentrons/protocol_engine/state/modules.py +210 -67
  144. opentrons/protocol_engine/state/pipettes.py +54 -0
  145. opentrons/protocol_engine/state/state.py +1 -1
  146. opentrons/protocol_engine/state/tips.py +14 -0
  147. opentrons/protocol_engine/state/update_types.py +180 -25
  148. opentrons/protocol_engine/state/wells.py +55 -9
  149. opentrons/protocol_engine/types/__init__.py +300 -0
  150. opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
  151. opentrons/protocol_engine/types/command_annotations.py +53 -0
  152. opentrons/protocol_engine/types/deck_configuration.py +72 -0
  153. opentrons/protocol_engine/types/execution.py +96 -0
  154. opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
  155. opentrons/protocol_engine/types/instrument.py +47 -0
  156. opentrons/protocol_engine/types/instrument_sensors.py +47 -0
  157. opentrons/protocol_engine/types/labware.py +111 -0
  158. opentrons/protocol_engine/types/labware_movement.py +22 -0
  159. opentrons/protocol_engine/types/labware_offset_location.py +111 -0
  160. opentrons/protocol_engine/types/labware_offset_vector.py +33 -0
  161. opentrons/protocol_engine/types/liquid.py +40 -0
  162. opentrons/protocol_engine/types/liquid_class.py +59 -0
  163. opentrons/protocol_engine/types/liquid_handling.py +13 -0
  164. opentrons/protocol_engine/types/liquid_level_detection.py +131 -0
  165. opentrons/protocol_engine/types/location.py +194 -0
  166. opentrons/protocol_engine/types/module.py +301 -0
  167. opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
  168. opentrons/protocol_engine/types/run_time_parameters.py +133 -0
  169. opentrons/protocol_engine/types/tip.py +18 -0
  170. opentrons/protocol_engine/types/util.py +21 -0
  171. opentrons/protocol_engine/types/well_position.py +124 -0
  172. opentrons/protocol_reader/extract_labware_definitions.py +7 -3
  173. opentrons/protocol_reader/file_format_validator.py +5 -3
  174. opentrons/protocol_runner/json_translator.py +4 -2
  175. opentrons/protocol_runner/legacy_command_mapper.py +6 -2
  176. opentrons/protocol_runner/run_orchestrator.py +4 -1
  177. opentrons/protocols/advanced_control/transfers/common.py +48 -1
  178. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +204 -0
  179. opentrons/protocols/api_support/definitions.py +1 -1
  180. opentrons/protocols/api_support/instrument.py +16 -3
  181. opentrons/protocols/labware.py +27 -23
  182. opentrons/protocols/models/__init__.py +0 -21
  183. opentrons/simulate.py +4 -2
  184. opentrons/types.py +20 -7
  185. opentrons/util/logging_config.py +94 -25
  186. opentrons/util/logging_queue_handler.py +61 -0
  187. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/METADATA +4 -4
  188. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/RECORD +192 -151
  189. opentrons/calibration_storage/ot2/models/defaults.py +0 -0
  190. opentrons/calibration_storage/ot3/models/defaults.py +0 -0
  191. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  192. opentrons/protocol_engine/types.py +0 -1311
  193. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/LICENSE +0 -0
  194. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/WHEEL +0 -0
  195. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/entry_points.txt +0 -0
  196. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,291 @@
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
+ from pydantic.json_schema import SkipJsonSchema
8
+
9
+ from ..command import (
10
+ AbstractCommandImpl,
11
+ BaseCommand,
12
+ BaseCommandCreate,
13
+ SuccessData,
14
+ DefinedErrorData,
15
+ )
16
+ from ..flex_stacker.common import FlexStackerStallOrCollisionError
17
+ from ...errors import (
18
+ ErrorOccurrence,
19
+ CannotPerformModuleAction,
20
+ LabwareNotLoadedOnModuleError,
21
+ FlexStackerLabwarePoolNotYetDefinedError,
22
+ )
23
+ from ...resources import ModelUtils
24
+ from ...state import update_types
25
+ from ...types import (
26
+ LabwareLocationSequence,
27
+ InStackerHopperLocation,
28
+ )
29
+
30
+ from opentrons_shared_data.errors.exceptions import FlexStackerStallError
31
+ from opentrons.calibration_storage.helpers import uri_from_details
32
+
33
+
34
+ if TYPE_CHECKING:
35
+ from opentrons.protocol_engine.state.state import StateView
36
+ from opentrons.protocol_engine.state.module_substates import FlexStackerSubState
37
+ from opentrons.protocol_engine.execution import EquipmentHandler
38
+
39
+
40
+ StoreCommandType = Literal["flexStacker/store"]
41
+
42
+
43
+ class StoreParams(BaseModel):
44
+ """Input parameters for a labware storage command."""
45
+
46
+ moduleId: str = Field(
47
+ ...,
48
+ description="Unique ID of the flex stacker.",
49
+ )
50
+
51
+
52
+ class StoreResult(BaseModel):
53
+ """Result data from a labware storage command."""
54
+
55
+ eventualDestinationLocationSequence: LabwareLocationSequence | SkipJsonSchema[
56
+ None
57
+ ] = Field(
58
+ None,
59
+ description=(
60
+ "The full location in which all labware moved by this command will eventually reside."
61
+ ),
62
+ )
63
+ primaryOriginLocationSequence: LabwareLocationSequence | SkipJsonSchema[
64
+ None
65
+ ] = Field(None, description=("The origin location of the primary labware."))
66
+ primaryLabwareId: str | SkipJsonSchema[None] = Field(
67
+ None, description="The primary labware in the stack that was stored."
68
+ )
69
+ adapterOriginLocationSequence: LabwareLocationSequence | SkipJsonSchema[
70
+ None
71
+ ] = Field(None, description=("The origin location of the adapter labware, if any."))
72
+ adapterLabwareId: str | SkipJsonSchema[None] = Field(
73
+ None, description="The adapter in the stack that was stored, if any."
74
+ )
75
+ lidOriginLocationSequence: LabwareLocationSequence | SkipJsonSchema[None] = Field(
76
+ None, description=("The origin location of the lid labware, if any.")
77
+ )
78
+ lidLabwareId: str | SkipJsonSchema[None] = Field(
79
+ None, description="The lid in the stack that was stored, if any."
80
+ )
81
+ primaryLabwareURI: str = Field(
82
+ ...,
83
+ description="The labware definition URI of the primary labware.",
84
+ )
85
+ adapterLabwareURI: str | SkipJsonSchema[None] = Field(
86
+ None,
87
+ description="The labware definition URI of the adapter labware.",
88
+ )
89
+ lidLabwareURI: str | SkipJsonSchema[None] = Field(
90
+ None,
91
+ description="The labware definition URI of the lid labware.",
92
+ )
93
+
94
+
95
+ _ExecuteReturn = Union[
96
+ SuccessData[StoreResult],
97
+ DefinedErrorData[FlexStackerStallOrCollisionError],
98
+ ]
99
+
100
+
101
+ class StoreImpl(AbstractCommandImpl[StoreParams, _ExecuteReturn]):
102
+ """Implementation of a labware storage command."""
103
+
104
+ def __init__(
105
+ self,
106
+ state_view: StateView,
107
+ equipment: EquipmentHandler,
108
+ model_utils: ModelUtils,
109
+ **kwargs: object,
110
+ ) -> None:
111
+ self._state_view = state_view
112
+ self._equipment = equipment
113
+ self._model_utils = model_utils
114
+
115
+ def _verify_labware_to_store(
116
+ self, params: StoreParams, stacker_state: FlexStackerSubState
117
+ ) -> tuple[str, str | None, str | None]:
118
+ try:
119
+ bottom_id = self._state_view.labware.get_id_by_module(params.moduleId)
120
+ except LabwareNotLoadedOnModuleError:
121
+ raise CannotPerformModuleAction(
122
+ "Cannot store labware if Flex Stacker carriage is empty"
123
+ )
124
+ labware_ids = self._state_view.labware.get_labware_stack_from_parent(bottom_id)
125
+ labware_defs = [
126
+ self._state_view.labware.get_definition(id) for id in labware_ids
127
+ ]
128
+
129
+ lid_id: str | None = None
130
+
131
+ pool_list = stacker_state.get_pool_definition_ordered_list()
132
+ assert pool_list is not None
133
+ if len(labware_ids) != len(pool_list):
134
+ raise CannotPerformModuleAction(
135
+ "Cannot store labware stack that does not correspond with Flex Stacker configuration"
136
+ )
137
+ if stacker_state.pool_lid_definition is not None:
138
+ if labware_defs[-1] != stacker_state.pool_lid_definition:
139
+ raise CannotPerformModuleAction(
140
+ "Cannot store labware stack that does not correspond with Flex Stacker configuration"
141
+ )
142
+ lid_id = labware_ids[-1]
143
+
144
+ if stacker_state.pool_adapter_definition is not None:
145
+ if (
146
+ labware_defs[0] != stacker_state.pool_adapter_definition
147
+ or labware_defs[1] != stacker_state.pool_primary_definition
148
+ ):
149
+ raise CannotPerformModuleAction(
150
+ "Cannot store labware stack that does not correspond with Flex Stacker configuration"
151
+ )
152
+ else:
153
+ return labware_ids[1], labware_ids[0], lid_id
154
+ else:
155
+ if labware_defs[0] != stacker_state.pool_primary_definition:
156
+ raise CannotPerformModuleAction(
157
+ "Cannot store labware stack that does not correspond with Flex Stacker configuration"
158
+ )
159
+ return labware_ids[0], None, lid_id
160
+
161
+ async def execute(self, params: StoreParams) -> _ExecuteReturn:
162
+ """Execute the labware storage command."""
163
+ stacker_state = self._state_view.modules.get_flex_stacker_substate(
164
+ params.moduleId
165
+ )
166
+
167
+ if stacker_state.pool_count == stacker_state.max_pool_count:
168
+ raise CannotPerformModuleAction(
169
+ "Cannot store labware in Flex Stacker while it is full"
170
+ )
171
+
172
+ pool_definitions = stacker_state.get_pool_definition_ordered_list()
173
+ if pool_definitions is None:
174
+ raise FlexStackerLabwarePoolNotYetDefinedError(
175
+ message="The Flex Stacker has not been configured yet and cannot be filled."
176
+ )
177
+
178
+ primary_id, maybe_adapter_id, maybe_lid_id = self._verify_labware_to_store(
179
+ params, stacker_state
180
+ )
181
+
182
+ # Allow propagation of ModuleNotAttachedError.
183
+ stacker_hw = self._equipment.get_module_hardware_api(stacker_state.module_id)
184
+
185
+ eventual_target_location_sequence = (
186
+ self._state_view.geometry.get_predicted_location_sequence(
187
+ InStackerHopperLocation(moduleId=params.moduleId)
188
+ )
189
+ )
190
+ stack_height = self._state_view.geometry.get_height_of_labware_stack(
191
+ pool_definitions
192
+ )
193
+
194
+ state_update = update_types.StateUpdate()
195
+ try:
196
+ if stacker_hw is not None:
197
+ await stacker_hw.store_labware(labware_height=stack_height)
198
+ except FlexStackerStallError as e:
199
+ return DefinedErrorData(
200
+ public=FlexStackerStallOrCollisionError(
201
+ id=self._model_utils.generate_id(),
202
+ createdAt=self._model_utils.get_timestamp(),
203
+ wrappedErrors=[
204
+ ErrorOccurrence.from_failed(
205
+ id=self._model_utils.generate_id(),
206
+ createdAt=self._model_utils.get_timestamp(),
207
+ error=e,
208
+ )
209
+ ],
210
+ ),
211
+ )
212
+
213
+ id_list = [
214
+ id for id in (primary_id, maybe_adapter_id, maybe_lid_id) if id is not None
215
+ ]
216
+
217
+ state_update.set_batch_labware_location(
218
+ new_locations_by_id={
219
+ id: InStackerHopperLocation(moduleId=params.moduleId) for id in id_list
220
+ },
221
+ new_offset_ids_by_id={id: None for id in id_list},
222
+ )
223
+
224
+ state_update.update_flex_stacker_labware_pool_count(
225
+ module_id=params.moduleId, count=stacker_state.pool_count + 1
226
+ )
227
+ if stacker_state.pool_primary_definition is None:
228
+ raise FlexStackerLabwarePoolNotYetDefinedError(
229
+ "The Primary Labware must be defined in the stacker pool."
230
+ )
231
+
232
+ return SuccessData(
233
+ public=StoreResult(
234
+ eventualDestinationLocationSequence=eventual_target_location_sequence,
235
+ primaryOriginLocationSequence=self._state_view.geometry.get_location_sequence(
236
+ primary_id
237
+ ),
238
+ primaryLabwareId=primary_id,
239
+ adapterOriginLocationSequence=(
240
+ self._state_view.geometry.get_location_sequence(maybe_adapter_id)
241
+ if maybe_adapter_id is not None
242
+ else None
243
+ ),
244
+ adapterLabwareId=maybe_adapter_id,
245
+ lidOriginLocationSequence=(
246
+ self._state_view.geometry.get_location_sequence(maybe_lid_id)
247
+ if maybe_lid_id is not None
248
+ else None
249
+ ),
250
+ lidLabwareId=maybe_lid_id,
251
+ primaryLabwareURI=uri_from_details(
252
+ stacker_state.pool_primary_definition.namespace,
253
+ stacker_state.pool_primary_definition.parameters.loadName,
254
+ stacker_state.pool_primary_definition.version,
255
+ ),
256
+ adapterLabwareURI=uri_from_details(
257
+ stacker_state.pool_adapter_definition.namespace,
258
+ stacker_state.pool_adapter_definition.parameters.loadName,
259
+ stacker_state.pool_adapter_definition.version,
260
+ )
261
+ if stacker_state.pool_adapter_definition is not None
262
+ else None,
263
+ lidLabwareURI=uri_from_details(
264
+ stacker_state.pool_lid_definition.namespace,
265
+ stacker_state.pool_lid_definition.parameters.loadName,
266
+ stacker_state.pool_lid_definition.version,
267
+ )
268
+ if stacker_state.pool_lid_definition is not None
269
+ else None,
270
+ ),
271
+ state_update=state_update,
272
+ )
273
+
274
+
275
+ class Store(BaseCommand[StoreParams, StoreResult, ErrorOccurrence]):
276
+ """A command to store a labware in a Flex Stacker."""
277
+
278
+ commandType: StoreCommandType = "flexStacker/store"
279
+ params: StoreParams
280
+ result: Optional[StoreResult] = None
281
+
282
+ _ImplementationCls: Type[StoreImpl] = StoreImpl
283
+
284
+
285
+ class StoreCreate(BaseCommandCreate[StoreParams]):
286
+ """A request to execute a Flex Stacker store command."""
287
+
288
+ commandType: StoreCommandType = "flexStacker/store"
289
+ params: StoreParams
290
+
291
+ _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,29 @@
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=(
15
+ "The full location down to the deck on which this labware exists."
16
+ " The reason this can be `null` or omitted is just backwards compatibility,"
17
+ " for older runs and analyses. This should always be present"
18
+ " for new runs and analyses, even for labware whose location is off-deck."
19
+ ),
20
+ )
21
+
22
+
23
+ class LabwarePositionResultMixin(LabwareHandlingResultMixin):
24
+ """A result for commands that create an offsetable labware entity."""
25
+
26
+ offsetId: str | None = Field(
27
+ None,
28
+ description="An ID referencing the labware offset that will apply to this labware in this location.",
29
+ )
@@ -22,7 +22,7 @@ from opentrons_shared_data.errors.exceptions import (
22
22
  PipetteOverpressureError,
23
23
  )
24
24
 
25
- from ..types import DeckPoint
25
+ from ..types import DeckPoint, LiquidTrackingType
26
26
  from .pipetting_common import (
27
27
  LiquidNotFoundError,
28
28
  PipetteIdMixin,
@@ -80,7 +80,7 @@ class TryLiquidProbeParams(_CommonParams):
80
80
  class LiquidProbeResult(DestinationPositionResult):
81
81
  """Result data from the execution of a `liquidProbe` command."""
82
82
 
83
- z_position: float = Field(
83
+ z_position: LiquidTrackingType = Field(
84
84
  ..., description="The Z coordinate, in mm, of the found liquid in deck space."
85
85
  )
86
86
  # New fields should use camelCase. z_position is snake_case for historical reasons.
@@ -89,7 +89,7 @@ class LiquidProbeResult(DestinationPositionResult):
89
89
  class TryLiquidProbeResult(DestinationPositionResult):
90
90
  """Result data from the execution of a `tryLiquidProbe` command."""
91
91
 
92
- z_position: float | SkipJsonSchema[None] = Field(
92
+ z_position: Union[LiquidTrackingType, SkipJsonSchema[None]] = Field(
93
93
  ...,
94
94
  description=(
95
95
  "The Z coordinate, in mm, of the found liquid in deck space."
@@ -116,8 +116,7 @@ class _ExecuteCommonResult(NamedTuple):
116
116
  # If the probe succeeded, the z_pos that it returned.
117
117
  # Or, if the probe found no liquid, the error representing that,
118
118
  # so calling code can propagate those details up.
119
- z_pos_or_error: float | PipetteLiquidNotFoundError | PipetteOverpressureError
120
-
119
+ z_pos_or_error: LiquidTrackingType | PipetteLiquidNotFoundError | PipetteOverpressureError
121
120
  state_update: update_types.StateUpdate
122
121
  deck_point: DeckPoint
123
122
 
@@ -189,7 +188,16 @@ async def _execute_common( # noqa: C901
189
188
  well_name=well_name,
190
189
  well_location=params.wellLocation,
191
190
  )
191
+ state_view.geometry.validate_probed_height(
192
+ labware_id=labware_id,
193
+ well_name=well_name,
194
+ pipette_id=pipette_id,
195
+ probed_height=z_pos,
196
+ )
192
197
  except PipetteLiquidNotFoundError as exception:
198
+ move_result.state_update.set_pipette_ready_to_aspirate(
199
+ pipette_id=pipette_id, ready_to_aspirate=True
200
+ )
193
201
  return _ExecuteCommonResult(
194
202
  z_pos_or_error=exception,
195
203
  state_update=move_result.state_update,
@@ -223,6 +231,9 @@ async def _execute_common( # noqa: C901
223
231
  ),
224
232
  )
225
233
  else:
234
+ move_result.state_update.set_pipette_ready_to_aspirate(
235
+ pipette_id=pipette_id, ready_to_aspirate=True
236
+ )
226
237
  return _ExecuteCommonResult(
227
238
  z_pos_or_error=z_pos,
228
239
  state_update=move_result.state_update,
@@ -303,12 +314,13 @@ class LiquidProbeImplementation(
303
314
  )
304
315
  else:
305
316
  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
- )
317
+ well_volume: Union[
318
+ LiquidTrackingType,
319
+ update_types.ClearType,
320
+ ] = self._state_view.geometry.get_well_volume_at_height(
321
+ labware_id=params.labwareId,
322
+ well_name=params.wellName,
323
+ height=z_pos_or_error,
312
324
  )
313
325
  except IncompleteLabwareDefinitionError:
314
326
  well_volume = update_types.CLEAR
@@ -370,7 +382,10 @@ class TryLiquidProbeImplementation(
370
382
  z_pos_or_error, (PipetteLiquidNotFoundError, PipetteOverpressureError)
371
383
  ):
372
384
  z_pos = None
373
- well_volume: float | update_types.ClearType = update_types.CLEAR
385
+ well_volume: Union[
386
+ LiquidTrackingType,
387
+ update_types.ClearType,
388
+ ] = update_types.CLEAR
374
389
  else:
375
390
  z_pos = z_pos_or_error
376
391
  try:
@@ -387,7 +402,6 @@ class TryLiquidProbeImplementation(
387
402
  volume=well_volume,
388
403
  last_probed=self._model_utils.get_timestamp(),
389
404
  )
390
-
391
405
  return SuccessData(
392
406
  public=TryLiquidProbeResult(
393
407
  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
  )