opentrons 8.3.0a0__py2.py3-none-any.whl → 8.3.0a2__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 (229) hide show
  1. opentrons/calibration_storage/deck_configuration.py +3 -3
  2. opentrons/calibration_storage/file_operators.py +3 -3
  3. opentrons/calibration_storage/helpers.py +3 -1
  4. opentrons/calibration_storage/ot2/models/v1.py +16 -29
  5. opentrons/calibration_storage/ot2/tip_length.py +7 -4
  6. opentrons/calibration_storage/ot3/models/v1.py +14 -23
  7. opentrons/cli/analyze.py +18 -6
  8. opentrons/drivers/asyncio/communication/__init__.py +2 -0
  9. opentrons/drivers/asyncio/communication/errors.py +16 -3
  10. opentrons/drivers/asyncio/communication/serial_connection.py +24 -9
  11. opentrons/drivers/command_builder.py +2 -2
  12. opentrons/drivers/flex_stacker/__init__.py +9 -0
  13. opentrons/drivers/flex_stacker/abstract.py +89 -0
  14. opentrons/drivers/flex_stacker/driver.py +260 -0
  15. opentrons/drivers/flex_stacker/simulator.py +109 -0
  16. opentrons/drivers/flex_stacker/types.py +138 -0
  17. opentrons/drivers/heater_shaker/driver.py +18 -3
  18. opentrons/drivers/temp_deck/driver.py +13 -3
  19. opentrons/drivers/thermocycler/driver.py +17 -3
  20. opentrons/execute.py +3 -1
  21. opentrons/hardware_control/__init__.py +1 -2
  22. opentrons/hardware_control/api.py +28 -20
  23. opentrons/hardware_control/backends/flex_protocol.py +4 -6
  24. opentrons/hardware_control/backends/ot3controller.py +177 -59
  25. opentrons/hardware_control/backends/ot3simulator.py +10 -8
  26. opentrons/hardware_control/backends/ot3utils.py +3 -13
  27. opentrons/hardware_control/dev_types.py +2 -0
  28. opentrons/hardware_control/emulation/heater_shaker.py +4 -0
  29. opentrons/hardware_control/emulation/module_server/client.py +1 -1
  30. opentrons/hardware_control/emulation/module_server/server.py +5 -3
  31. opentrons/hardware_control/emulation/settings.py +3 -4
  32. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +2 -1
  33. opentrons/hardware_control/instruments/ot2/pipette.py +9 -21
  34. opentrons/hardware_control/instruments/ot2/pipette_handler.py +8 -1
  35. opentrons/hardware_control/instruments/ot3/gripper.py +2 -2
  36. opentrons/hardware_control/instruments/ot3/pipette.py +13 -22
  37. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -1
  38. opentrons/hardware_control/modules/mod_abc.py +2 -2
  39. opentrons/hardware_control/motion_utilities.py +68 -0
  40. opentrons/hardware_control/nozzle_manager.py +39 -41
  41. opentrons/hardware_control/ot3_calibration.py +1 -1
  42. opentrons/hardware_control/ot3api.py +34 -22
  43. opentrons/hardware_control/protocols/gripper_controller.py +3 -0
  44. opentrons/hardware_control/protocols/hardware_manager.py +5 -1
  45. opentrons/hardware_control/protocols/liquid_handler.py +18 -0
  46. opentrons/hardware_control/protocols/motion_controller.py +6 -0
  47. opentrons/hardware_control/robot_calibration.py +1 -1
  48. opentrons/hardware_control/types.py +61 -0
  49. opentrons/protocol_api/__init__.py +20 -1
  50. opentrons/protocol_api/_liquid.py +24 -49
  51. opentrons/protocol_api/_liquid_properties.py +754 -0
  52. opentrons/protocol_api/_types.py +24 -0
  53. opentrons/protocol_api/core/common.py +2 -0
  54. opentrons/protocol_api/core/engine/instrument.py +67 -10
  55. opentrons/protocol_api/core/engine/labware.py +29 -7
  56. opentrons/protocol_api/core/engine/protocol.py +130 -5
  57. opentrons/protocol_api/core/engine/robot.py +139 -0
  58. opentrons/protocol_api/core/engine/well.py +4 -1
  59. opentrons/protocol_api/core/instrument.py +42 -4
  60. opentrons/protocol_api/core/labware.py +13 -4
  61. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +34 -3
  62. opentrons/protocol_api/core/legacy/legacy_labware_core.py +13 -4
  63. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +32 -1
  64. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  65. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +34 -3
  66. opentrons/protocol_api/core/protocol.py +34 -1
  67. opentrons/protocol_api/core/robot.py +51 -0
  68. opentrons/protocol_api/instrument_context.py +145 -43
  69. opentrons/protocol_api/labware.py +231 -7
  70. opentrons/protocol_api/module_contexts.py +21 -17
  71. opentrons/protocol_api/protocol_context.py +125 -4
  72. opentrons/protocol_api/robot_context.py +204 -32
  73. opentrons/protocol_api/validation.py +261 -3
  74. opentrons/protocol_engine/__init__.py +4 -0
  75. opentrons/protocol_engine/actions/actions.py +2 -3
  76. opentrons/protocol_engine/clients/sync_client.py +18 -0
  77. opentrons/protocol_engine/commands/__init__.py +81 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +0 -2
  79. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +19 -5
  80. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +0 -1
  81. opentrons/protocol_engine/commands/absorbance_reader/read.py +32 -9
  82. opentrons/protocol_engine/commands/air_gap_in_place.py +160 -0
  83. opentrons/protocol_engine/commands/aspirate.py +103 -53
  84. opentrons/protocol_engine/commands/aspirate_in_place.py +55 -51
  85. opentrons/protocol_engine/commands/blow_out.py +44 -39
  86. opentrons/protocol_engine/commands/blow_out_in_place.py +21 -32
  87. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +13 -6
  88. opentrons/protocol_engine/commands/calibration/calibrate_module.py +1 -1
  89. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +3 -3
  90. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +1 -1
  91. opentrons/protocol_engine/commands/command.py +73 -66
  92. opentrons/protocol_engine/commands/command_unions.py +101 -1
  93. opentrons/protocol_engine/commands/comment.py +1 -1
  94. opentrons/protocol_engine/commands/configure_for_volume.py +10 -3
  95. opentrons/protocol_engine/commands/configure_nozzle_layout.py +6 -4
  96. opentrons/protocol_engine/commands/custom.py +6 -12
  97. opentrons/protocol_engine/commands/dispense.py +82 -48
  98. opentrons/protocol_engine/commands/dispense_in_place.py +71 -51
  99. opentrons/protocol_engine/commands/drop_tip.py +52 -31
  100. opentrons/protocol_engine/commands/drop_tip_in_place.py +13 -3
  101. opentrons/protocol_engine/commands/generate_command_schema.py +4 -11
  102. opentrons/protocol_engine/commands/get_next_tip.py +134 -0
  103. opentrons/protocol_engine/commands/get_tip_presence.py +1 -1
  104. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +1 -1
  105. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +1 -1
  106. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +1 -1
  107. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +1 -1
  108. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +1 -1
  109. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +1 -1
  110. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +10 -4
  111. opentrons/protocol_engine/commands/home.py +13 -4
  112. opentrons/protocol_engine/commands/liquid_probe.py +60 -25
  113. opentrons/protocol_engine/commands/load_labware.py +29 -7
  114. opentrons/protocol_engine/commands/load_lid.py +146 -0
  115. opentrons/protocol_engine/commands/load_lid_stack.py +189 -0
  116. opentrons/protocol_engine/commands/load_liquid.py +12 -4
  117. opentrons/protocol_engine/commands/load_liquid_class.py +144 -0
  118. opentrons/protocol_engine/commands/load_module.py +31 -10
  119. opentrons/protocol_engine/commands/load_pipette.py +19 -8
  120. opentrons/protocol_engine/commands/magnetic_module/disengage.py +1 -1
  121. opentrons/protocol_engine/commands/magnetic_module/engage.py +1 -1
  122. opentrons/protocol_engine/commands/move_labware.py +19 -6
  123. opentrons/protocol_engine/commands/move_relative.py +35 -25
  124. opentrons/protocol_engine/commands/move_to_addressable_area.py +40 -27
  125. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +53 -32
  126. opentrons/protocol_engine/commands/move_to_coordinates.py +36 -22
  127. opentrons/protocol_engine/commands/move_to_well.py +40 -24
  128. opentrons/protocol_engine/commands/movement_common.py +338 -0
  129. opentrons/protocol_engine/commands/pick_up_tip.py +49 -27
  130. opentrons/protocol_engine/commands/pipetting_common.py +169 -87
  131. opentrons/protocol_engine/commands/prepare_to_aspirate.py +24 -33
  132. opentrons/protocol_engine/commands/reload_labware.py +1 -1
  133. opentrons/protocol_engine/commands/retract_axis.py +1 -1
  134. opentrons/protocol_engine/commands/robot/__init__.py +69 -0
  135. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +86 -0
  136. opentrons/protocol_engine/commands/robot/common.py +18 -0
  137. opentrons/protocol_engine/commands/robot/move_axes_relative.py +101 -0
  138. opentrons/protocol_engine/commands/robot/move_axes_to.py +100 -0
  139. opentrons/protocol_engine/commands/robot/move_to.py +94 -0
  140. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +77 -0
  141. opentrons/protocol_engine/commands/save_position.py +14 -5
  142. opentrons/protocol_engine/commands/set_rail_lights.py +1 -1
  143. opentrons/protocol_engine/commands/set_status_bar.py +1 -1
  144. opentrons/protocol_engine/commands/temperature_module/deactivate.py +1 -1
  145. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +1 -1
  146. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +10 -4
  147. opentrons/protocol_engine/commands/thermocycler/close_lid.py +1 -1
  148. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +1 -1
  149. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +1 -1
  150. opentrons/protocol_engine/commands/thermocycler/open_lid.py +1 -1
  151. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +8 -2
  152. opentrons/protocol_engine/commands/thermocycler/run_profile.py +9 -3
  153. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +11 -4
  154. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +1 -1
  155. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +1 -1
  156. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +1 -1
  157. opentrons/protocol_engine/commands/touch_tip.py +65 -16
  158. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +4 -1
  159. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -3
  160. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +1 -4
  161. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +1 -4
  162. opentrons/protocol_engine/commands/verify_tip_presence.py +11 -4
  163. opentrons/protocol_engine/commands/wait_for_duration.py +10 -3
  164. opentrons/protocol_engine/commands/wait_for_resume.py +10 -3
  165. opentrons/protocol_engine/errors/__init__.py +8 -0
  166. opentrons/protocol_engine/errors/error_occurrence.py +19 -20
  167. opentrons/protocol_engine/errors/exceptions.py +50 -0
  168. opentrons/protocol_engine/execution/command_executor.py +1 -1
  169. opentrons/protocol_engine/execution/equipment.py +73 -5
  170. opentrons/protocol_engine/execution/gantry_mover.py +364 -8
  171. opentrons/protocol_engine/execution/movement.py +27 -0
  172. opentrons/protocol_engine/execution/pipetting.py +5 -1
  173. opentrons/protocol_engine/execution/tip_handler.py +4 -6
  174. opentrons/protocol_engine/notes/notes.py +1 -1
  175. opentrons/protocol_engine/protocol_engine.py +7 -6
  176. opentrons/protocol_engine/resources/labware_data_provider.py +1 -1
  177. opentrons/protocol_engine/resources/labware_validation.py +5 -0
  178. opentrons/protocol_engine/resources/module_data_provider.py +1 -1
  179. opentrons/protocol_engine/resources/pipette_data_provider.py +12 -0
  180. opentrons/protocol_engine/slot_standardization.py +9 -9
  181. opentrons/protocol_engine/state/_move_types.py +9 -5
  182. opentrons/protocol_engine/state/_well_math.py +193 -0
  183. opentrons/protocol_engine/state/addressable_areas.py +25 -61
  184. opentrons/protocol_engine/state/command_history.py +12 -0
  185. opentrons/protocol_engine/state/commands.py +17 -13
  186. opentrons/protocol_engine/state/files.py +10 -12
  187. opentrons/protocol_engine/state/fluid_stack.py +138 -0
  188. opentrons/protocol_engine/state/frustum_helpers.py +57 -32
  189. opentrons/protocol_engine/state/geometry.py +47 -1
  190. opentrons/protocol_engine/state/labware.py +79 -25
  191. opentrons/protocol_engine/state/liquid_classes.py +82 -0
  192. opentrons/protocol_engine/state/liquids.py +16 -4
  193. opentrons/protocol_engine/state/modules.py +52 -70
  194. opentrons/protocol_engine/state/motion.py +6 -1
  195. opentrons/protocol_engine/state/pipettes.py +135 -58
  196. opentrons/protocol_engine/state/state.py +21 -2
  197. opentrons/protocol_engine/state/state_summary.py +4 -2
  198. opentrons/protocol_engine/state/tips.py +11 -44
  199. opentrons/protocol_engine/state/update_types.py +343 -48
  200. opentrons/protocol_engine/state/wells.py +19 -11
  201. opentrons/protocol_engine/types.py +176 -28
  202. opentrons/protocol_reader/extract_labware_definitions.py +5 -2
  203. opentrons/protocol_reader/file_format_validator.py +5 -5
  204. opentrons/protocol_runner/json_file_reader.py +9 -3
  205. opentrons/protocol_runner/json_translator.py +51 -25
  206. opentrons/protocol_runner/legacy_command_mapper.py +66 -64
  207. opentrons/protocol_runner/protocol_runner.py +35 -4
  208. opentrons/protocol_runner/python_protocol_wrappers.py +1 -1
  209. opentrons/protocol_runner/run_orchestrator.py +13 -3
  210. opentrons/protocols/advanced_control/common.py +38 -0
  211. opentrons/protocols/advanced_control/mix.py +1 -1
  212. opentrons/protocols/advanced_control/transfers/__init__.py +0 -0
  213. opentrons/protocols/advanced_control/transfers/common.py +56 -0
  214. opentrons/protocols/advanced_control/{transfers.py → transfers/transfer.py} +10 -85
  215. opentrons/protocols/api_support/definitions.py +1 -1
  216. opentrons/protocols/api_support/instrument.py +1 -1
  217. opentrons/protocols/api_support/util.py +10 -0
  218. opentrons/protocols/labware.py +70 -8
  219. opentrons/protocols/models/json_protocol.py +5 -9
  220. opentrons/simulate.py +3 -1
  221. opentrons/types.py +162 -2
  222. opentrons/util/entrypoint_util.py +2 -5
  223. opentrons/util/logging_config.py +1 -1
  224. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/METADATA +16 -15
  225. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/RECORD +229 -202
  226. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/WHEEL +1 -1
  227. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/LICENSE +0 -0
  228. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/entry_points.txt +0 -0
  229. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  """Equipment command side-effect logic."""
2
2
  from dataclasses import dataclass
3
- from typing import Optional, overload, Union
3
+ from typing import Optional, overload, Union, List
4
4
 
5
5
  from opentrons_shared_data.pipette.types import PipetteNameType
6
6
 
@@ -152,10 +152,6 @@ class EquipmentHandler:
152
152
  Returns:
153
153
  A LoadedLabwareData object.
154
154
  """
155
- labware_id = (
156
- labware_id if labware_id is not None else self._model_utils.generate_id()
157
- )
158
-
159
155
  definition_uri = uri_from_details(
160
156
  load_name=load_name,
161
157
  namespace=namespace,
@@ -172,6 +168,10 @@ class EquipmentHandler:
172
168
  version=version,
173
169
  )
174
170
 
171
+ labware_id = (
172
+ labware_id if labware_id is not None else self._model_utils.generate_id()
173
+ )
174
+
175
175
  # Allow propagation of ModuleNotLoadedError.
176
176
  offset_id = self.find_applicable_labware_offset_id(
177
177
  labware_definition_uri=definition_uri,
@@ -379,6 +379,74 @@ class EquipmentHandler:
379
379
  definition=attached_module.definition,
380
380
  )
381
381
 
382
+ async def load_lids(
383
+ self,
384
+ load_name: str,
385
+ namespace: str,
386
+ version: int,
387
+ location: LabwareLocation,
388
+ quantity: int,
389
+ ) -> List[LoadedLabwareData]:
390
+ """Load one or many lid labware by assigning an identifier and pulling required data.
391
+
392
+ Args:
393
+ load_name: The lid labware's load name.
394
+ namespace: The lid labware's namespace.
395
+ version: The lid labware's version.
396
+ location: The deck location at which lid(s) will be placed.
397
+ labware_ids: An optional list of identifiers to assign the labware. If None,
398
+ an identifier will be generated.
399
+
400
+ Raises:
401
+ ModuleNotLoadedError: If `location` references a module ID
402
+ that doesn't point to a valid loaded module.
403
+
404
+ Returns:
405
+ A list of LoadedLabwareData objects.
406
+ """
407
+ definition_uri = uri_from_details(
408
+ load_name=load_name,
409
+ namespace=namespace,
410
+ version=version,
411
+ )
412
+ try:
413
+ # Try to use existing definition in state.
414
+ definition = self._state_store.labware.get_definition_by_uri(definition_uri)
415
+ except LabwareDefinitionDoesNotExistError:
416
+ definition = await self._labware_data_provider.get_labware_definition(
417
+ load_name=load_name,
418
+ namespace=namespace,
419
+ version=version,
420
+ )
421
+
422
+ stack_limit = definition.stackLimit if definition.stackLimit is not None else 1
423
+ if quantity > stack_limit:
424
+ raise ValueError(
425
+ f"Requested quantity {quantity} is greater than the stack limit of {stack_limit} provided by definition for {load_name}."
426
+ )
427
+
428
+ # Allow propagation of ModuleNotLoadedError.
429
+ if (
430
+ isinstance(location, DeckSlotLocation)
431
+ and definition.parameters.isDeckSlotCompatible is not None
432
+ and not definition.parameters.isDeckSlotCompatible
433
+ ):
434
+ raise ValueError(
435
+ f"Lid Labware {load_name} cannot be loaded onto a Deck Slot."
436
+ )
437
+
438
+ load_labware_data_list = []
439
+ for i in range(quantity):
440
+ load_labware_data_list.append(
441
+ LoadedLabwareData(
442
+ labware_id=self._model_utils.generate_id(),
443
+ definition=definition,
444
+ offsetId=None,
445
+ )
446
+ )
447
+
448
+ return load_labware_data_list
449
+
382
450
  async def configure_for_volume(
383
451
  self, pipette_id: str, volume: float, tip_overlap_version: Optional[str]
384
452
  ) -> LoadedConfigureForVolumeData:
@@ -1,11 +1,18 @@
1
1
  """Gantry movement wrapper for hardware and simulation based movement."""
2
- from typing import Optional, List, Dict
2
+ from logging import getLogger
3
+ from opentrons.config.types import OT3Config
4
+ from functools import partial
5
+ from typing import Optional, List, Dict, Tuple
3
6
  from typing_extensions import Protocol as TypingProtocol
4
7
 
5
- from opentrons.types import Point, Mount
8
+ from opentrons.types import Point, Mount, MountType
6
9
 
7
10
  from opentrons.hardware_control import HardwareControlAPI
8
- from opentrons.hardware_control.types import Axis as HardwareAxis
11
+ from opentrons.hardware_control.types import Axis as HardwareAxis, CriticalPoint
12
+ from opentrons.hardware_control.motion_utilities import (
13
+ target_axis_map_from_relative,
14
+ target_axis_map_from_absolute,
15
+ )
9
16
  from opentrons_shared_data.errors.exceptions import PositionUnknownError
10
17
 
11
18
  from opentrons.motion_planning import Waypoint
@@ -14,6 +21,8 @@ from ..state.state import StateView
14
21
  from ..types import MotorAxis, CurrentWell
15
22
  from ..errors import MustHomeError, InvalidAxisForRobotType
16
23
 
24
+ log = getLogger(__name__)
25
+
17
26
 
18
27
  _MOTOR_AXIS_TO_HARDWARE_AXIS: Dict[MotorAxis, HardwareAxis] = {
19
28
  MotorAxis.X: HardwareAxis.X,
@@ -24,8 +33,38 @@ _MOTOR_AXIS_TO_HARDWARE_AXIS: Dict[MotorAxis, HardwareAxis] = {
24
33
  MotorAxis.RIGHT_PLUNGER: HardwareAxis.C,
25
34
  MotorAxis.EXTENSION_Z: HardwareAxis.Z_G,
26
35
  MotorAxis.EXTENSION_JAW: HardwareAxis.G,
36
+ MotorAxis.AXIS_96_CHANNEL_CAM: HardwareAxis.Q,
37
+ }
38
+
39
+ _MOTOR_AXIS_TO_HARDWARE_MOUNT: Dict[MotorAxis, Mount] = {
40
+ MotorAxis.LEFT_Z: Mount.LEFT,
41
+ MotorAxis.RIGHT_Z: Mount.RIGHT,
42
+ MotorAxis.EXTENSION_Z: Mount.EXTENSION,
43
+ }
44
+
45
+ _HARDWARE_MOUNT_MOTOR_AXIS_TO: Dict[Mount, MotorAxis] = {
46
+ Mount.LEFT: MotorAxis.LEFT_Z,
47
+ Mount.RIGHT: MotorAxis.RIGHT_Z,
48
+ Mount.EXTENSION: MotorAxis.EXTENSION_Z,
49
+ }
50
+
51
+ _HARDWARE_AXIS_TO_MOTOR_AXIS: Dict[HardwareAxis, MotorAxis] = {
52
+ HardwareAxis.X: MotorAxis.X,
53
+ HardwareAxis.Y: MotorAxis.Y,
54
+ HardwareAxis.Z: MotorAxis.LEFT_Z,
55
+ HardwareAxis.A: MotorAxis.RIGHT_Z,
56
+ HardwareAxis.B: MotorAxis.LEFT_PLUNGER,
57
+ HardwareAxis.C: MotorAxis.RIGHT_PLUNGER,
58
+ HardwareAxis.P_L: MotorAxis.LEFT_PLUNGER,
59
+ HardwareAxis.P_R: MotorAxis.RIGHT_PLUNGER,
60
+ HardwareAxis.Z_L: MotorAxis.LEFT_Z,
61
+ HardwareAxis.Z_R: MotorAxis.RIGHT_Z,
62
+ HardwareAxis.Z_G: MotorAxis.EXTENSION_Z,
63
+ HardwareAxis.G: MotorAxis.EXTENSION_JAW,
64
+ HardwareAxis.Q: MotorAxis.AXIS_96_CHANNEL_CAM,
27
65
  }
28
66
 
67
+
29
68
  # The height of the bottom of the pipette nozzle at home position without any tips.
30
69
  # We rely on this being the same for every OT-3 pipette.
31
70
  #
@@ -36,6 +75,8 @@ _MOTOR_AXIS_TO_HARDWARE_AXIS: Dict[MotorAxis, HardwareAxis] = {
36
75
  # That OT3Simulator return value is what Protocol Engine uses for simulation when Protocol Engine
37
76
  # is configured to not virtualize pipettes, so this number should match it.
38
77
  VIRTUAL_MAX_OT3_HEIGHT = 248.0
78
+ # This number was found by using the longest pipette's P1000V2 default configuration values.
79
+ VIRTUAL_MAX_OT2_HEIGHT = 268.14
39
80
 
40
81
 
41
82
  class GantryMover(TypingProtocol):
@@ -50,16 +91,45 @@ class GantryMover(TypingProtocol):
50
91
  """Get the current position of the gantry."""
51
92
  ...
52
93
 
94
+ async def get_position_from_mount(
95
+ self,
96
+ mount: Mount,
97
+ critical_point: Optional[CriticalPoint] = None,
98
+ fail_on_not_homed: bool = False,
99
+ ) -> Point:
100
+ """Get the current position of the gantry based on the given mount."""
101
+ ...
102
+
53
103
  def get_max_travel_z(self, pipette_id: str) -> float:
54
104
  """Get the maximum allowed z-height for pipette movement."""
55
105
  ...
56
106
 
107
+ def get_max_travel_z_from_mount(self, mount: MountType) -> float:
108
+ """Get the maximum allowed z-height for mount movement."""
109
+ ...
110
+
111
+ async def move_axes(
112
+ self,
113
+ axis_map: Dict[MotorAxis, float],
114
+ critical_point: Optional[Dict[MotorAxis, float]] = None,
115
+ speed: Optional[float] = None,
116
+ relative_move: bool = False,
117
+ ) -> Dict[MotorAxis, float]:
118
+ """Move a set of axes a given distance."""
119
+ ...
120
+
57
121
  async def move_to(
58
122
  self, pipette_id: str, waypoints: List[Waypoint], speed: Optional[float]
59
123
  ) -> Point:
60
124
  """Move the hardware gantry to a waypoint."""
61
125
  ...
62
126
 
127
+ async def move_mount_to(
128
+ self, mount: Mount, waypoints: List[Waypoint], speed: Optional[float]
129
+ ) -> Point:
130
+ """Move the provided hardware mount to a waypoint."""
131
+ ...
132
+
63
133
  async def move_relative(
64
134
  self,
65
135
  pipette_id: str,
@@ -85,6 +155,16 @@ class GantryMover(TypingProtocol):
85
155
  """Transform an engine motor axis into a hardware axis."""
86
156
  ...
87
157
 
158
+ def pick_mount_from_axis_map(self, axis_map: Dict[MotorAxis, float]) -> Mount:
159
+ """Find a mount axis in the axis_map if it exists otherwise default to left mount."""
160
+ ...
161
+
162
+ def motor_axes_to_present_hardware_axes(
163
+ self, motor_axes: List[MotorAxis]
164
+ ) -> List[HardwareAxis]:
165
+ """Transform a list of engine axes into a list of hardware axes, filtering out non-present axes."""
166
+ ...
167
+
88
168
 
89
169
  class HardwareGantryMover(GantryMover):
90
170
  """Hardware API based gantry movement handler."""
@@ -93,10 +173,70 @@ class HardwareGantryMover(GantryMover):
93
173
  self._hardware_api = hardware_api
94
174
  self._state_view = state_view
95
175
 
176
+ def motor_axes_to_present_hardware_axes(
177
+ self, motor_axes: List[MotorAxis]
178
+ ) -> List[HardwareAxis]:
179
+ """Get hardware axes from engine axes while filtering out non-present axes."""
180
+ return [
181
+ self.motor_axis_to_hardware_axis(motor_axis)
182
+ for motor_axis in motor_axes
183
+ if self._hardware_api.axis_is_present(
184
+ self.motor_axis_to_hardware_axis(motor_axis)
185
+ )
186
+ ]
187
+
96
188
  def motor_axis_to_hardware_axis(self, motor_axis: MotorAxis) -> HardwareAxis:
97
189
  """Transform an engine motor axis into a hardware axis."""
98
190
  return _MOTOR_AXIS_TO_HARDWARE_AXIS[motor_axis]
99
191
 
192
+ def _hardware_axis_to_motor_axis(self, motor_axis: HardwareAxis) -> MotorAxis:
193
+ """Transform an hardware axis into a engine motor axis."""
194
+ return _HARDWARE_AXIS_TO_MOTOR_AXIS[motor_axis]
195
+
196
+ def _convert_axis_map_for_hw(
197
+ self, axis_map: Dict[MotorAxis, float]
198
+ ) -> Dict[HardwareAxis, float]:
199
+ """Transform an engine motor axis map to a hardware axis map."""
200
+ return {_MOTOR_AXIS_TO_HARDWARE_AXIS[ax]: dist for ax, dist in axis_map.items()}
201
+
202
+ def _critical_point_for(
203
+ self, mount: Mount, cp_override: Optional[Dict[MotorAxis, float]] = None
204
+ ) -> Point:
205
+ if cp_override:
206
+ return Point(
207
+ x=cp_override[MotorAxis.X],
208
+ y=cp_override[MotorAxis.Y],
209
+ z=cp_override[_HARDWARE_MOUNT_MOTOR_AXIS_TO[mount]],
210
+ )
211
+ else:
212
+ return self._hardware_api.critical_point_for(mount)
213
+
214
+ def _get_gantry_offsets_for_robot_type(
215
+ self,
216
+ ) -> Tuple[Point, Point, Optional[Point]]:
217
+ if isinstance(self._hardware_api.config, OT3Config):
218
+ return (
219
+ Point(*self._hardware_api.config.left_mount_offset),
220
+ Point(*self._hardware_api.config.right_mount_offset),
221
+ Point(*self._hardware_api.config.gripper_mount_offset),
222
+ )
223
+ else:
224
+ return (
225
+ Point(*self._hardware_api.config.left_mount_offset),
226
+ Point(0, 0, 0),
227
+ None,
228
+ )
229
+
230
+ def pick_mount_from_axis_map(self, axis_map: Dict[MotorAxis, float]) -> Mount:
231
+ """Find a mount axis in the axis_map if it exists otherwise default to left mount."""
232
+ found_mount = Mount.LEFT
233
+ mounts = list(_MOTOR_AXIS_TO_HARDWARE_MOUNT.keys())
234
+ for k in axis_map.keys():
235
+ if k in mounts:
236
+ found_mount = _MOTOR_AXIS_TO_HARDWARE_MOUNT[k]
237
+ break
238
+ return found_mount
239
+
100
240
  async def get_position(
101
241
  self,
102
242
  pipette_id: str,
@@ -114,12 +254,33 @@ class HardwareGantryMover(GantryMover):
114
254
  pipette_id=pipette_id,
115
255
  current_location=current_well,
116
256
  )
257
+ point = await self.get_position_from_mount(
258
+ mount=pipette_location.mount.to_hw_mount(),
259
+ critical_point=pipette_location.critical_point,
260
+ fail_on_not_homed=fail_on_not_homed,
261
+ )
262
+ return point
263
+
264
+ async def get_position_from_mount(
265
+ self,
266
+ mount: Mount,
267
+ critical_point: Optional[CriticalPoint] = None,
268
+ fail_on_not_homed: bool = False,
269
+ ) -> Point:
270
+ """Get the current position of the gantry based on the mount.
271
+
272
+ Args:
273
+ mount: The mount to get the position for.
274
+ critical_point: Optional parameter for getting instrument location data, effects critical point.
275
+ fail_on_not_homed: Raise PositionUnknownError if gantry position is not known.
276
+ """
117
277
  try:
118
- return await self._hardware_api.gantry_position(
119
- mount=pipette_location.mount.to_hw_mount(),
120
- critical_point=pipette_location.critical_point,
278
+ point = await self._hardware_api.gantry_position(
279
+ mount=mount,
280
+ critical_point=critical_point,
121
281
  fail_on_not_homed=fail_on_not_homed,
122
282
  )
283
+ return point
123
284
  except PositionUnknownError as e:
124
285
  raise MustHomeError(message=str(e), wrapping=[e])
125
286
 
@@ -129,8 +290,16 @@ class HardwareGantryMover(GantryMover):
129
290
  Args:
130
291
  pipette_id: Pipette ID to get max travel z-height for.
131
292
  """
132
- hw_mount = self._state_view.pipettes.get_mount(pipette_id).to_hw_mount()
133
- return self._hardware_api.get_instrument_max_height(mount=hw_mount)
293
+ mount = self._state_view.pipettes.get_mount(pipette_id)
294
+ return self.get_max_travel_z_from_mount(mount=mount)
295
+
296
+ def get_max_travel_z_from_mount(self, mount: MountType) -> float:
297
+ """Get the maximum allowed z-height for any mount movement.
298
+
299
+ Args:
300
+ mount: Mount to get max travel z-height for.
301
+ """
302
+ return self._hardware_api.get_instrument_max_height(mount=mount.to_hw_mount())
134
303
 
135
304
  async def move_to(
136
305
  self, pipette_id: str, waypoints: List[Waypoint], speed: Optional[float]
@@ -150,6 +319,88 @@ class HardwareGantryMover(GantryMover):
150
319
 
151
320
  return waypoints[-1].position
152
321
 
322
+ async def move_mount_to(
323
+ self, mount: Mount, waypoints: List[Waypoint], speed: Optional[float]
324
+ ) -> Point:
325
+ """Move the given hardware mount to a waypoint."""
326
+ assert len(waypoints) > 0, "Must have at least one waypoint"
327
+ for waypoint in waypoints:
328
+ log.info(f"The current waypoint moving is {waypoint}")
329
+ await self._hardware_api.move_to(
330
+ mount=mount,
331
+ abs_position=waypoint.position,
332
+ critical_point=waypoint.critical_point,
333
+ speed=speed,
334
+ )
335
+
336
+ return waypoints[-1].position
337
+
338
+ async def move_axes(
339
+ self,
340
+ axis_map: Dict[MotorAxis, float],
341
+ critical_point: Optional[Dict[MotorAxis, float]] = None,
342
+ speed: Optional[float] = None,
343
+ relative_move: bool = False,
344
+ ) -> Dict[MotorAxis, float]:
345
+ """Move a set of axes a given distance.
346
+
347
+ Args:
348
+ axis_map: The mapping of axes to command.
349
+ critical_point: A critical point override for axes
350
+ speed: Optional speed parameter for the move.
351
+ relative_move: Whether the axis map needs to be converted from a relative to absolute move.
352
+ """
353
+ try:
354
+ pos_hw = self._convert_axis_map_for_hw(axis_map)
355
+ mount = self.pick_mount_from_axis_map(axis_map)
356
+ if relative_move:
357
+ current_position = await self._hardware_api.current_position(
358
+ mount, refresh=True
359
+ )
360
+ log.info(f"The current position of the robot is: {current_position}.")
361
+ converted_current_position_deck = (
362
+ self._hardware_api.get_deck_from_machine(current_position)
363
+ )
364
+ log.info(f"The current position of the robot is: {current_position}.")
365
+
366
+ pos_hw = target_axis_map_from_relative(pos_hw, current_position)
367
+ log.info(
368
+ f"The absolute position is: {pos_hw} and hw pos map is {pos_hw}."
369
+ )
370
+ log.info(f"The calculated move {pos_hw} and {mount}")
371
+ (
372
+ left_offset,
373
+ right_offset,
374
+ gripper_offset,
375
+ ) = self._get_gantry_offsets_for_robot_type()
376
+ absolute_pos = target_axis_map_from_absolute(
377
+ mount,
378
+ pos_hw,
379
+ partial(self._critical_point_for, cp_override=critical_point),
380
+ left_mount_offset=left_offset,
381
+ right_mount_offset=right_offset,
382
+ gripper_mount_offset=gripper_offset,
383
+ )
384
+ log.info(f"The prepped abs {absolute_pos}")
385
+ await self._hardware_api.move_axes(
386
+ position=absolute_pos,
387
+ speed=speed,
388
+ )
389
+
390
+ except PositionUnknownError as e:
391
+ raise MustHomeError(message=str(e), wrapping=[e])
392
+
393
+ current_position = await self._hardware_api.current_position(
394
+ mount, refresh=True
395
+ )
396
+ converted_current_position_deck = self._hardware_api.get_deck_from_machine(
397
+ current_position
398
+ )
399
+ return {
400
+ self._hardware_axis_to_motor_axis(ax): pos
401
+ for ax, pos in converted_current_position_deck.items()
402
+ }
403
+
153
404
  async def move_relative(
154
405
  self,
155
406
  pipette_id: str,
@@ -239,6 +490,16 @@ class VirtualGantryMover(GantryMover):
239
490
  """Transform an engine motor axis into a hardware axis."""
240
491
  return _MOTOR_AXIS_TO_HARDWARE_AXIS[motor_axis]
241
492
 
493
+ def pick_mount_from_axis_map(self, axis_map: Dict[MotorAxis, float]) -> Mount:
494
+ """Find a mount axis in the axis_map if it exists otherwise default to left mount."""
495
+ found_mount = Mount.LEFT
496
+ mounts = list(_MOTOR_AXIS_TO_HARDWARE_MOUNT.keys())
497
+ for k in axis_map.keys():
498
+ if k in mounts:
499
+ found_mount = _MOTOR_AXIS_TO_HARDWARE_MOUNT[k]
500
+ break
501
+ return found_mount
502
+
242
503
  async def get_position(
243
504
  self,
244
505
  pipette_id: str,
@@ -261,6 +522,31 @@ class VirtualGantryMover(GantryMover):
261
522
  origin = Point(x=0, y=0, z=0)
262
523
  return origin
263
524
 
525
+ async def get_position_from_mount(
526
+ self,
527
+ mount: Mount,
528
+ critical_point: Optional[CriticalPoint] = None,
529
+ fail_on_not_homed: bool = False,
530
+ ) -> Point:
531
+ """Get the current position of the gantry based on the mount.
532
+
533
+ Args:
534
+ mount: The mount to get the position for.
535
+ critical_point: Optional parameter for getting instrument location data, effects critical point.
536
+ fail_on_not_homed: Raise PositionUnknownError if gantry position is not known.
537
+ """
538
+ pipette = self._state_view.pipettes.get_by_mount(MountType[mount.name])
539
+ origin_deck_point = (
540
+ self._state_view.pipettes.get_deck_point(pipette.id) if pipette else None
541
+ )
542
+ if origin_deck_point is not None:
543
+ origin = Point(
544
+ x=origin_deck_point.x, y=origin_deck_point.y, z=origin_deck_point.z
545
+ )
546
+ else:
547
+ origin = Point(x=0, y=0, z=0)
548
+ return origin
549
+
264
550
  def get_max_travel_z(self, pipette_id: str) -> float:
265
551
  """Get the maximum allowed z-height for pipette movement.
266
552
 
@@ -278,6 +564,68 @@ class VirtualGantryMover(GantryMover):
278
564
  tip_length = tip.length if tip is not None else 0
279
565
  return instrument_height - tip_length
280
566
 
567
+ def get_max_travel_z_from_mount(self, mount: MountType) -> float:
568
+ """Get the maximum allowed z-height for mount."""
569
+ pipette = self._state_view.pipettes.get_by_mount(mount)
570
+ if self._state_view.config.robot_type == "OT-2 Standard":
571
+ instrument_height = (
572
+ self._state_view.pipettes.get_instrument_max_height_ot2(pipette.id)
573
+ if pipette
574
+ else VIRTUAL_MAX_OT2_HEIGHT
575
+ )
576
+ else:
577
+ instrument_height = VIRTUAL_MAX_OT3_HEIGHT
578
+ if pipette:
579
+ tip = self._state_view.pipettes.get_attached_tip(pipette_id=pipette.id)
580
+ tip_length = tip.length if tip is not None else 0.0
581
+ else:
582
+ tip_length = 0.0
583
+ return instrument_height - tip_length
584
+
585
+ async def move_axes(
586
+ self,
587
+ axis_map: Dict[MotorAxis, float],
588
+ critical_point: Optional[Dict[MotorAxis, float]] = None,
589
+ speed: Optional[float] = None,
590
+ relative_move: bool = False,
591
+ ) -> Dict[MotorAxis, float]:
592
+ """Move the give axes map. No-op in virtual implementation."""
593
+ mount = self.pick_mount_from_axis_map(axis_map)
594
+ current_position = await self.get_position_from_mount(mount)
595
+ updated_position = {}
596
+ if relative_move:
597
+ updated_position[MotorAxis.X] = (
598
+ axis_map.get(MotorAxis.X, 0.0) + current_position[0]
599
+ )
600
+ updated_position[MotorAxis.Y] = (
601
+ axis_map.get(MotorAxis.Y, 0.0) + current_position[1]
602
+ )
603
+ if mount == Mount.RIGHT:
604
+ updated_position[MotorAxis.RIGHT_Z] = (
605
+ axis_map.get(MotorAxis.RIGHT_Z, 0.0) + current_position[2]
606
+ )
607
+ elif mount == Mount.EXTENSION:
608
+ updated_position[MotorAxis.EXTENSION_Z] = (
609
+ axis_map.get(MotorAxis.EXTENSION_Z, 0.0) + current_position[2]
610
+ )
611
+ else:
612
+ updated_position[MotorAxis.LEFT_Z] = (
613
+ axis_map.get(MotorAxis.LEFT_Z, 0.0) + current_position[2]
614
+ )
615
+ else:
616
+ critical_point = critical_point or {}
617
+ updated_position = {
618
+ ax: pos - critical_point.get(ax, 0.0) for ax, pos in axis_map.items()
619
+ }
620
+ return updated_position
621
+
622
+ async def move_mount_to(
623
+ self, mount: Mount, waypoints: List[Waypoint], speed: Optional[float]
624
+ ) -> Point:
625
+ """Move the hardware mount to a waypoint. No-op in virtual implementation."""
626
+ assert len(waypoints) > 0, "Must have at least one waypoint"
627
+ return waypoints[-1].position
628
+
281
629
  async def move_to(
282
630
  self, pipette_id: str, waypoints: List[Waypoint], speed: Optional[float]
283
631
  ) -> Point:
@@ -313,6 +661,14 @@ class VirtualGantryMover(GantryMover):
313
661
  """Retract the 'idle' mount if necessary."""
314
662
  pass
315
663
 
664
+ def motor_axes_to_present_hardware_axes(
665
+ self, motor_axes: List[MotorAxis]
666
+ ) -> List[HardwareAxis]:
667
+ """Get present hardware axes from a list of engine axes. In simulation, all axes are present."""
668
+ return [
669
+ self.motor_axis_to_hardware_axis(motor_axis) for motor_axis in motor_axes
670
+ ]
671
+
316
672
 
317
673
  def create_gantry_mover(
318
674
  state_view: StateView, hardware_api: HardwareControlAPI
@@ -149,6 +149,33 @@ class MovementHandler:
149
149
 
150
150
  return final_point
151
151
 
152
+ async def move_mount_to(
153
+ self, mount: MountType, destination: DeckPoint, speed: Optional[float] = None
154
+ ) -> Point:
155
+ """Move mount to a specific location on the deck."""
156
+ hw_mount = mount.to_hw_mount()
157
+ await self._gantry_mover.prepare_for_mount_movement(hw_mount)
158
+ origin = await self._gantry_mover.get_position_from_mount(mount=hw_mount)
159
+ max_travel_z = self._gantry_mover.get_max_travel_z_from_mount(mount=mount)
160
+
161
+ # calculate the movement's waypoints
162
+ waypoints = self._state_store.motion.get_movement_waypoints_to_coords(
163
+ origin=origin,
164
+ dest=Point(x=destination.x, y=destination.y, z=destination.z),
165
+ max_travel_z=max_travel_z,
166
+ direct=False,
167
+ additional_min_travel_z=None,
168
+ )
169
+
170
+ # move through the waypoints
171
+ final_point = await self._gantry_mover.move_mount_to(
172
+ mount=hw_mount,
173
+ waypoints=waypoints,
174
+ speed=speed,
175
+ )
176
+
177
+ return final_point
178
+
152
179
  async def move_to_addressable_area(
153
180
  self,
154
181
  pipette_id: str,
@@ -91,7 +91,11 @@ class HardwarePipettingHandler(PipettingHandler):
91
91
  )
92
92
 
93
93
  async def prepare_for_aspirate(self, pipette_id: str) -> None:
94
- """Prepare for pipette aspiration."""
94
+ """Prepare for pipette aspiration.
95
+
96
+ Raises:
97
+ PipetteOverpressureError, propagated as-is from the hardware controller.
98
+ """
95
99
  hw_mount = self._state_view.pipettes.get_mount(pipette_id).to_hw_mount()
96
100
  await self._hardware_api.prepare_for_aspirate(mount=hw_mount)
97
101
 
@@ -1,11 +1,12 @@
1
1
  """Tip pickup and drop procedures."""
2
+
2
3
  from typing import Optional, Dict
3
4
  from typing_extensions import Protocol as TypingProtocol
4
5
 
5
6
  from opentrons.hardware_control import HardwareControlAPI
6
7
  from opentrons.hardware_control.types import FailedTipStateCheck, InstrumentProbeType
7
8
  from opentrons.protocol_engine.errors.exceptions import PickUpTipTipNotAttachedError
8
- from opentrons.types import Mount
9
+ from opentrons.types import Mount, NozzleConfigurationType
9
10
 
10
11
  from opentrons_shared_data.errors.exceptions import (
11
12
  CommandPreconditionViolated,
@@ -23,9 +24,6 @@ from ..errors import (
23
24
  ProtocolEngineError,
24
25
  )
25
26
 
26
- from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType
27
-
28
-
29
27
  PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP = {
30
28
  "A1": {"COLUMN": "H1", "ROW": "A12"},
31
29
  "H1": {"COLUMN": "A1", "ROW": "H12"},
@@ -326,8 +324,8 @@ class HardwareTipHandler(TipHandler):
326
324
  follow_singular_sensor: Optional[InstrumentProbeType] = None,
327
325
  ) -> None:
328
326
  """See documentation on abstract base class."""
329
- nozzle_configuration = (
330
- self._state_view.pipettes.state.nozzle_configuration_by_id[pipette_id]
327
+ nozzle_configuration = self._state_view.pipettes.get_nozzle_configuration(
328
+ pipette_id=pipette_id
331
329
  )
332
330
 
333
331
  # Configuration metrics by which tip presence checking is ignored
@@ -35,7 +35,7 @@ def make_error_recovery_debug_note(type: "ErrorRecoveryType") -> CommandNote:
35
35
  This is intended to be read by developers and support people, not computers.
36
36
  """
37
37
  message = f"Handling this command failure with {type.name}."
38
- return CommandNote.construct(
38
+ return CommandNote.model_construct(
39
39
  noteKind="debugErrorRecovery",
40
40
  shortMessage=message,
41
41
  longMessage=message,