opentrons 8.3.2__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.2.dist-info → opentrons-8.4.0.dist-info}/METADATA +4 -4
  188. {opentrons-8.3.2.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.2.dist-info → opentrons-8.4.0.dist-info}/LICENSE +0 -0
  194. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/WHEEL +0 -0
  195. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/entry_points.txt +0 -0
  196. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,18 @@
1
+ """Protocol Engine types to deal with tips."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class TipGeometry:
8
+ """Tip geometry data.
9
+
10
+ Props:
11
+ length: The effective length (total length minus overlap) of a tip in mm.
12
+ diameter: Tip diameter in mm.
13
+ volume: Maximum volume in µL.
14
+ """
15
+
16
+ length: float
17
+ diameter: float
18
+ volume: float
@@ -0,0 +1,21 @@
1
+ """Protocol engine utility types for model components."""
2
+ from dataclasses import dataclass
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Dimensions:
9
+ """Dimensions of an object in deck-space."""
10
+
11
+ x: float
12
+ y: float
13
+ z: float
14
+
15
+
16
+ class Vec3f(BaseModel):
17
+ """A 3D vector of floats."""
18
+
19
+ x: float
20
+ y: float
21
+ z: float
@@ -0,0 +1,124 @@
1
+ """Protocol engine types to do with positions inside wells."""
2
+ from enum import Enum, auto
3
+ from typing import Union, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class WellOrigin(str, Enum):
9
+ """Origin of WellLocation offset.
10
+
11
+ Props:
12
+ TOP: the top-center of the well
13
+ BOTTOM: the bottom-center of the well
14
+ CENTER: the middle-center of the well
15
+ MENISCUS: the meniscus-center of the well
16
+ """
17
+
18
+ TOP = "top"
19
+ BOTTOM = "bottom"
20
+ CENTER = "center"
21
+ MENISCUS = "meniscus"
22
+
23
+
24
+ class PickUpTipWellOrigin(str, Enum):
25
+ """The origin of a PickUpTipWellLocation offset.
26
+
27
+ Props:
28
+ TOP: the top-center of the well
29
+ BOTTOM: the bottom-center of the well
30
+ CENTER: the middle-center of the well
31
+ """
32
+
33
+ TOP = "top"
34
+ BOTTOM = "bottom"
35
+ CENTER = "center"
36
+
37
+
38
+ class DropTipWellOrigin(str, Enum):
39
+ """The origin of a DropTipWellLocation offset.
40
+
41
+ Props:
42
+ TOP: the top-center of the well
43
+ BOTTOM: the bottom-center of the well
44
+ CENTER: the middle-center of the well
45
+ DEFAULT: the default drop-tip location of the well,
46
+ based on pipette configuration and length of the tip.
47
+ """
48
+
49
+ TOP = "top"
50
+ BOTTOM = "bottom"
51
+ CENTER = "center"
52
+ DEFAULT = "default"
53
+
54
+
55
+ class WellLocationFunction(int, Enum):
56
+ """The type of well location object to be created."""
57
+
58
+ BASE = auto()
59
+ LIQUID_HANDLING = auto()
60
+ PICK_UP_TIP = auto()
61
+ DROP_TIP = auto()
62
+
63
+
64
+ # This is deliberately a separate type from Vec3f to let components default to 0.
65
+ class WellOffset(BaseModel):
66
+ """An offset vector in (x, y, z)."""
67
+
68
+ x: float = 0
69
+ y: float = 0
70
+ z: float = 0
71
+
72
+
73
+ class WellLocation(BaseModel):
74
+ """A relative location in reference to a well's location."""
75
+
76
+ origin: WellOrigin = WellOrigin.TOP
77
+ offset: WellOffset = Field(default_factory=WellOffset)
78
+ volumeOffset: float = Field(
79
+ default=0.0,
80
+ description="""A volume of liquid, in µL, to offset the z-axis offset.""",
81
+ )
82
+
83
+
84
+ class LiquidHandlingWellLocation(BaseModel):
85
+ """A relative location in reference to a well's location.
86
+
87
+ To be used with commands that handle liquids.
88
+ """
89
+
90
+ origin: WellOrigin = WellOrigin.TOP
91
+ offset: WellOffset = Field(default_factory=WellOffset)
92
+ volumeOffset: Union[float, Literal["operationVolume"]] = Field(
93
+ default=0.0,
94
+ description="""A volume of liquid, in µL, to offset the z-axis offset. When "operationVolume" is specified, this volume is pulled from the command volume parameter.""",
95
+ )
96
+
97
+
98
+ class PickUpTipWellLocation(BaseModel):
99
+ """A relative location in reference to a well's location.
100
+
101
+ To be used for picking up tips.
102
+ """
103
+
104
+ origin: PickUpTipWellOrigin = PickUpTipWellOrigin.TOP
105
+ offset: WellOffset = Field(default_factory=WellOffset)
106
+
107
+
108
+ class DropTipWellLocation(BaseModel):
109
+ """Like WellLocation, but for dropping tips.
110
+
111
+ Unlike a typical WellLocation, the location for a drop tip
112
+ defaults to location based on the tip length rather than the well's top.
113
+ """
114
+
115
+ origin: DropTipWellOrigin = DropTipWellOrigin.DEFAULT
116
+ offset: WellOffset = Field(default_factory=WellOffset)
117
+
118
+
119
+ WellLocationType = Union[
120
+ WellLocation,
121
+ LiquidHandlingWellLocation,
122
+ PickUpTipWellLocation,
123
+ DropTipWellLocation,
124
+ ]
@@ -6,7 +6,10 @@ from typing import List
6
6
 
7
7
  import anyio
8
8
 
9
- from opentrons.protocols.models import LabwareDefinition
9
+ from opentrons_shared_data.labware.labware_definition import (
10
+ LabwareDefinition,
11
+ labware_definition_type_adapter,
12
+ )
10
13
 
11
14
  from .protocol_source import ProtocolFileRole, ProtocolSource, ProtocolType
12
15
 
@@ -42,7 +45,7 @@ async def extract_labware_definitions(
42
45
 
43
46
  async def _extract_from_labware_file(path: Path) -> LabwareDefinition:
44
47
  def _do_parse() -> LabwareDefinition:
45
- return LabwareDefinition.model_validate_json(path.read_bytes())
48
+ return labware_definition_type_adapter.validate_json(path.read_bytes())
46
49
 
47
50
  return await anyio.to_thread.run_sync(_do_parse)
48
51
 
@@ -55,7 +58,8 @@ async def _extract_from_json_protocol_file(path: Path) -> List[LabwareDefinition
55
58
  # which require this labwareDefinitions key.
56
59
  unvalidated_definitions = json_contents["labwareDefinitions"].values()
57
60
  validated_definitions = [
58
- LabwareDefinition.model_validate(u) for u in unvalidated_definitions
61
+ labware_definition_type_adapter.validate_python(u)
62
+ for u in unvalidated_definitions
59
63
  ]
60
64
  return validated_definitions
61
65
 
@@ -6,7 +6,9 @@ from typing import Iterable
6
6
  import anyio
7
7
  from pydantic import ValidationError as PydanticValidationError
8
8
 
9
- from opentrons_shared_data.labware.labware_definition import LabwareDefinition
9
+ from opentrons_shared_data.labware.labware_definition import (
10
+ labware_definition_type_adapter,
11
+ )
10
12
  from opentrons_shared_data.protocol.models import (
11
13
  ProtocolSchemaV6 as JsonProtocolV6,
12
14
  ProtocolSchemaV7 as JsonProtocolV7,
@@ -14,7 +16,7 @@ from opentrons_shared_data.protocol.models import (
14
16
  )
15
17
  from opentrons_shared_data.errors.exceptions import PythonException
16
18
 
17
- from opentrons.protocols.models import JsonProtocol as JsonProtocolUpToV5
19
+ from opentrons.protocols.models.json_protocol import Model as JsonProtocolUpToV5
18
20
 
19
21
  from .file_identifier import (
20
22
  IdentifiedFile,
@@ -60,7 +62,7 @@ class FileFormatValidator:
60
62
  async def _validate_labware_definition(info: IdentifiedLabwareDefinition) -> None:
61
63
  def validate_sync() -> None:
62
64
  try:
63
- LabwareDefinition.model_validate(info.unvalidated_json)
65
+ labware_definition_type_adapter.validate_python(info.unvalidated_json)
64
66
  except PydanticValidationError as e:
65
67
  raise FileFormatValidationError(
66
68
  message=f"{info.original_file.name} could not be read as a labware definition.",
@@ -20,7 +20,7 @@ from opentrons_shared_data.errors.exceptions import InvalidProtocolData, PythonE
20
20
  from opentrons.types import MountType
21
21
  from opentrons.protocol_engine import (
22
22
  commands as pe_commands,
23
- LabwareLocation,
23
+ LoadableLabwareLocation,
24
24
  ModuleModel,
25
25
  DeckSlotLocation,
26
26
  Liquid,
@@ -37,7 +37,9 @@ class CommandTranslatorError(Exception):
37
37
  # Each time a TypeAdapter is instantiated, it will construct a new validator and
38
38
  # serializer. To improve performance, TypeAdapters are instantiated once.
39
39
  # See https://docs.pydantic.dev/latest/concepts/performance/#typeadapter-instantiated-once
40
- LabwareLocationAdapter: TypeAdapter[LabwareLocation] = TypeAdapter(LabwareLocation)
40
+ LabwareLocationAdapter: TypeAdapter[LoadableLabwareLocation] = TypeAdapter(
41
+ LoadableLabwareLocation
42
+ )
41
43
  CommandAnnotationAdapter: TypeAdapter[CommandAnnotation] = TypeAdapter(
42
44
  CommandAnnotation
43
45
  )
@@ -38,7 +38,9 @@ from opentrons.protocol_engine.state.update_types import (
38
38
  StateUpdate,
39
39
  )
40
40
 
41
- from opentrons_shared_data.labware.labware_definition import LabwareDefinition
41
+ from opentrons_shared_data.labware.labware_definition import (
42
+ labware_definition_type_adapter,
43
+ )
42
44
  from opentrons_shared_data.errors import ErrorCodes, EnumeratedError, PythonException
43
45
 
44
46
 
@@ -659,10 +661,12 @@ class LegacyCommandMapper:
659
661
  notes=[],
660
662
  result=pe_commands.LoadLabwareResult.model_construct(
661
663
  labwareId=labware_id,
662
- definition=LabwareDefinition.model_validate(
664
+ definition=labware_definition_type_adapter.validate_python(
663
665
  labware_load_info.labware_definition
664
666
  ),
665
667
  offsetId=labware_load_info.offset_id,
668
+ # These legacy json protocols don't get location sequences because
669
+ # to do so we'd have to go back and look up where the module gets loaded
666
670
  ),
667
671
  )
668
672
  queue_action = pe_actions.QueueCommandAction(
@@ -32,6 +32,7 @@ from ..protocol_engine.types import (
32
32
  PostRunHardwareState,
33
33
  EngineStatus,
34
34
  LabwareOffsetCreate,
35
+ LegacyLabwareOffsetCreate,
35
36
  LabwareOffset,
36
37
  DeckConfigurationType,
37
38
  RunTimeParameter,
@@ -346,7 +347,9 @@ class RunOrchestrator:
346
347
  """Get whether the run has stopped."""
347
348
  return self._protocol_engine.state_view.commands.get_is_stopped()
348
349
 
349
- def add_labware_offset(self, request: LabwareOffsetCreate) -> LabwareOffset:
350
+ def add_labware_offset(
351
+ self, request: LabwareOffsetCreate | LegacyLabwareOffsetCreate
352
+ ) -> LabwareOffset:
350
353
  """Add a new labware offset to state."""
351
354
  return self._protocol_engine.add_labware_offset(request)
352
355
 
@@ -1,6 +1,11 @@
1
1
  """Common functions between v1 transfer and liquid-class-based transfer."""
2
2
  import enum
3
- from typing import Iterable, Generator, Tuple, TypeVar, Literal
3
+ import math
4
+ from typing import Iterable, Generator, Tuple, TypeVar, Literal, List
5
+
6
+
7
+ class NoLiquidClassPropertyError(ValueError):
8
+ """An error raised when a liquid class property cannot be found for a pipette/tip combination"""
4
9
 
5
10
 
6
11
  class TransferTipPolicyV2(enum.Enum):
@@ -33,6 +38,23 @@ def check_valid_volume_parameters(
33
38
  )
34
39
 
35
40
 
41
+ def check_valid_liquid_class_volume_parameters(
42
+ aspirate_volume: float, air_gap: float, disposal_volume: float, max_volume: float
43
+ ) -> None:
44
+ if air_gap + aspirate_volume > max_volume:
45
+ raise ValueError(
46
+ f"Cannot have an air gap of {air_gap} µL for an aspiration of {aspirate_volume} µL"
47
+ f" with a max volume of {max_volume} µL. Please adjust the retract air gap to fit within"
48
+ f" the bounds of the tip."
49
+ )
50
+ elif disposal_volume + aspirate_volume > max_volume:
51
+ raise ValueError(
52
+ f"Cannot have a dispense volume of {disposal_volume} µL for an aspiration of {aspirate_volume} µL"
53
+ f" with a max volume of {max_volume} µL. Please adjust the dispense volume to fit within"
54
+ f" the bounds of the tip."
55
+ )
56
+
57
+
36
58
  def expand_for_volume_constraints(
37
59
  volumes: Iterable[float],
38
60
  targets: Iterable[Target],
@@ -54,3 +76,28 @@ def expand_for_volume_constraints(
54
76
  volume /= 2
55
77
  yield volume, target
56
78
  yield volume, target
79
+
80
+
81
+ def _split_volume_equally(volume: float, max_volume: float) -> List[float]:
82
+ """
83
+ Splits a given volume into a list of volumes that are all less than or equal to max volume.
84
+
85
+ If volume provided is more than the max volume, the volumes will be split evenly.
86
+ """
87
+ if volume <= max_volume:
88
+ return [volume]
89
+ else:
90
+ iterations = math.ceil(volume / max_volume)
91
+ return [volume / iterations for _ in range(iterations)]
92
+
93
+
94
+ def expand_for_volume_constraints_for_liquid_classes(
95
+ volumes: Iterable[float],
96
+ targets: Iterable[Target],
97
+ max_volume: float,
98
+ ) -> Generator[Tuple[float, "Target"], None, None]:
99
+ """Split a sequence of proposed transfers to keep each under the max volume, splitting larger ones equally."""
100
+ assert max_volume > 0
101
+ for volume, target in zip(volumes, targets):
102
+ for split_volume in _split_volume_equally(volume, max_volume):
103
+ yield split_volume, target
@@ -0,0 +1,204 @@
1
+ """Utility functions for transfer_liquid, consolidate_liquid and distribute_liquid"""
2
+ from __future__ import annotations
3
+
4
+ from typing import Literal, Sequence, List, Optional, TYPE_CHECKING
5
+ from dataclasses import dataclass
6
+
7
+ from opentrons.protocol_engine.errors import LiquidHeightUnknownError
8
+ from opentrons.protocol_engine.state._well_math import (
9
+ wells_covered_by_pipette_configuration,
10
+ )
11
+ from opentrons.types import NozzleMapInterface, NozzleConfigurationType
12
+
13
+ if TYPE_CHECKING:
14
+ from logging import Logger
15
+ from opentrons.types import Location
16
+ from opentrons.protocol_api.core.engine import WellCore
17
+ from opentrons.protocol_api.labware import Well, Labware
18
+
19
+
20
+ @dataclass
21
+ class LocationCheckDescriptors:
22
+ location_type: Literal["submerge start", "retract end"]
23
+ pipetting_action: Literal["aspirate", "dispense"]
24
+
25
+
26
+ def raise_if_location_inside_liquid(
27
+ location: Location,
28
+ well_location: Location,
29
+ well_core: WellCore,
30
+ location_check_descriptors: LocationCheckDescriptors,
31
+ logger: Logger,
32
+ ) -> None:
33
+ """Raise an error if the location in question would be inside the liquid.
34
+
35
+ This checker will raise an error if:
36
+ - the location in question is below the target well location during aspirate/dispense or,
37
+ - if we can find the liquid height AND the location in question is below this height.
38
+ If we can't find the liquid height, then we simply log a warning and no error is raised.
39
+ """
40
+ if location.point.z < well_location.point.z:
41
+ raise RuntimeError(
42
+ f"Received {location_check_descriptors.location_type} location of {location}"
43
+ f" and {location_check_descriptors.pipetting_action} location of {well_location}."
44
+ f" {location_check_descriptors.location_type.capitalize()} location z should not be lower"
45
+ f" than the {location_check_descriptors.pipetting_action} location z."
46
+ )
47
+ try:
48
+ liquid_height_from_bottom = well_core.current_liquid_height()
49
+ except LiquidHeightUnknownError:
50
+ liquid_height_from_bottom = None
51
+ if liquid_height_from_bottom is not None:
52
+ if liquid_height_from_bottom + well_core.get_bottom(0).z > location.point.z:
53
+ raise RuntimeError(
54
+ f"{location_check_descriptors.location_type.capitalize()} location {location} is"
55
+ f" inside the liquid in well {well_core.get_display_name()} when it should be outside"
56
+ f"(above) the liquid."
57
+ )
58
+ else:
59
+ # We could raise an error here but that would restrict the use of
60
+ # liquid classes-based transfer to only when LPD is enabled or when liquids are
61
+ # loaded in protocols using `load_liquid`. This can be quite restrictive
62
+ # so we will not raise but just log a warning.
63
+ logger.warning(
64
+ f"Could not verify height of liquid in well {well_core.get_display_name()}, either"
65
+ f" because the liquid in this well has not been probed or because"
66
+ f" liquid was not loaded in this well using `load_liquid`."
67
+ f" Proceeding without verifying if {location_check_descriptors.location_type}"
68
+ f" location is outside the liquid."
69
+ )
70
+
71
+
72
+ def group_wells_for_multi_channel_transfer(
73
+ targets: Sequence[Well],
74
+ nozzle_map: NozzleMapInterface,
75
+ ) -> List[Well]:
76
+ """Takes a list of wells and a nozzle map and returns a list of target wells to address every well given
77
+
78
+ This currently only supports 8-tip columns, 12-tip rows and full 96-channel configurations,
79
+ and only is used for 96 and 384 well plates. This assumes the wells are being given in a
80
+ contiguous order (or every other for 384), and will raise if a well is found that does not overlap
81
+ with the first target well given for a sequence, or if not all wells are given for that sequence.
82
+ """
83
+ configuration = nozzle_map.configuration
84
+ active_nozzles = nozzle_map.tip_count
85
+
86
+ if (
87
+ (
88
+ (
89
+ configuration == NozzleConfigurationType.COLUMN
90
+ or configuration == NozzleConfigurationType.FULL
91
+ )
92
+ and active_nozzles == 8
93
+ )
94
+ or (configuration == NozzleConfigurationType.ROW and active_nozzles == 12)
95
+ or active_nozzles == 96
96
+ ):
97
+ return _group_wells_for_nozzle_configuration(list(targets), nozzle_map)
98
+ else:
99
+ raise ValueError("Unsupported tip configuration for well grouping")
100
+
101
+
102
+ def _group_wells_for_nozzle_configuration( # noqa: C901
103
+ targets: List[Well], nozzle_map: NozzleMapInterface
104
+ ) -> List[Well]:
105
+ """Groups wells together for a column, row, or full 96 configuration and returns a reduced list of target wells."""
106
+ grouped_wells = []
107
+ active_wells_covered: List[str] = []
108
+ active_labware: Optional[Labware] = None
109
+ alternate_384_well_coverage_count = 0
110
+ labware_format: Optional[str] = None
111
+
112
+ # We are assuming the wells are ordered A1, B1, C1... A2, B2, C2..., for columns and
113
+ # A1, A2, A3... B1, B2, B3 for rows. So if the active nozzle is on H row/12 column,
114
+ # reverse the list so the correct primary nozzle is chosen
115
+ reverse_lookup = (
116
+ nozzle_map.starting_nozzle == "H12"
117
+ or (
118
+ nozzle_map.configuration == NozzleConfigurationType.COLUMN
119
+ and nozzle_map.starting_nozzle == "H1"
120
+ )
121
+ or (
122
+ nozzle_map.configuration == NozzleConfigurationType.ROW
123
+ and nozzle_map.starting_nozzle == "A12"
124
+ )
125
+ )
126
+ if reverse_lookup:
127
+ targets.reverse()
128
+
129
+ for well in targets:
130
+ # If we have wells that are covered by the pipette's nozzles while primary nozzle is over
131
+ # a target well that aren't accounted for, check if the current well is in that list
132
+ if active_wells_covered:
133
+ if well.parent != active_labware:
134
+ raise ValueError(
135
+ "Could not resolve wells provided to pipette's nozzle configuration. "
136
+ "Please ensure wells are ordered to match pipette's nozzle layout."
137
+ )
138
+
139
+ if well.well_name in active_wells_covered:
140
+ active_wells_covered.remove(well.well_name)
141
+ # If it's a 384 well plate, contiguous wells are not covered by the pipette targeting the
142
+ # initial target well. To support these kinds of transfers given a list of contiguous wells,
143
+ # allow another target well (or up to 4 total for a full 96-tip config) and add those wells
144
+ # to the list of covered wells
145
+ elif labware_format == "384Standard" and (
146
+ alternate_384_well_coverage_count == 0
147
+ or (
148
+ nozzle_map.tip_count == 96 and alternate_384_well_coverage_count < 3
149
+ )
150
+ ):
151
+ active_wells_covered.extend(
152
+ list(
153
+ wells_covered_by_pipette_configuration(
154
+ nozzle_map, # type: ignore[arg-type]
155
+ well.well_name,
156
+ labware_wells_by_column=[
157
+ [labware_well.well_name for labware_well in column]
158
+ for column in well.parent.columns()
159
+ ],
160
+ )
161
+ )
162
+ )
163
+ active_wells_covered.remove(well.well_name)
164
+ grouped_wells.append(well)
165
+ alternate_384_well_coverage_count += 1
166
+ else:
167
+ raise ValueError(
168
+ "Could not resolve wells provided to pipette's nozzle configuration. "
169
+ "Please ensure wells are ordered to match pipette's nozzle layout."
170
+ )
171
+ # If we have no active wells covered to account for, add a new target well and list of covered wells to check
172
+ else:
173
+ # If the labware is not a 96 or 384 well plate, add this well to the final result and move on to the next
174
+ labware_format = well.parent.parameters["format"]
175
+ if labware_format != "96Standard" and labware_format != "384Standard":
176
+ grouped_wells.append(well)
177
+ continue
178
+
179
+ active_wells_covered = list(
180
+ wells_covered_by_pipette_configuration(
181
+ nozzle_map, # type: ignore[arg-type]
182
+ well.well_name,
183
+ labware_wells_by_column=[
184
+ [labware_well.well_name for labware_well in column]
185
+ for column in well.parent.columns()
186
+ ],
187
+ )
188
+ )
189
+ active_wells_covered.remove(well.well_name)
190
+ grouped_wells.append(well)
191
+ active_labware = well.parent
192
+ alternate_384_well_coverage_count = 0
193
+
194
+ if active_wells_covered:
195
+ raise ValueError(
196
+ "Could not target all wells provided without aspirating or dispensing from other wells. "
197
+ f"Other wells that would be targeted: {active_wells_covered}"
198
+ )
199
+
200
+ # If we reversed the lookup of wells, reverse the grouped wells we will return
201
+ if reverse_lookup:
202
+ grouped_wells.reverse()
203
+
204
+ return grouped_wells
@@ -1,6 +1,6 @@
1
1
  from .types import APIVersion
2
2
 
3
- MAX_SUPPORTED_VERSION = APIVersion(2, 22)
3
+ MAX_SUPPORTED_VERSION = APIVersion(2, 23)
4
4
  """The maximum supported protocol API version in this release."""
5
5
 
6
6
  MIN_SUPPORTED_VERSION = APIVersion(2, 0)
@@ -2,7 +2,7 @@ import logging
2
2
  from typing import Optional, Any
3
3
 
4
4
  from opentrons_shared_data.labware.types import (
5
- LabwareDefinition as LabwareDefinitionDict,
5
+ LabwareDefinition2 as LabwareDefinition2Dict,
6
6
  )
7
7
 
8
8
  from opentrons import types
@@ -56,7 +56,7 @@ def validate_blowout_location(
56
56
 
57
57
 
58
58
  def tip_length_for(
59
- pipette: PipetteDict, tip_rack_definition: LabwareDefinitionDict
59
+ pipette: PipetteDict, tip_rack_definition: LabwareDefinition2Dict
60
60
  ) -> float:
61
61
  """Get the tip length, including overlap, for a tip from this rack"""
62
62
  try:
@@ -99,7 +99,20 @@ def validate_tiprack(
99
99
  gen_lookup = (
100
100
  "FLEX" if ("flex" in instr_metadata or "96" in instr_metadata) else "OT2"
101
101
  )
102
- valid_vols = VALID_PIP_TIPRACK_VOL[gen_lookup][instrument_name.split("_")[0]]
102
+
103
+ # TODO (spp, 2025-01-30): do what AA's note above says or at least,
104
+ # fetch the 'pip_type' below from the 'model' field in pipette definitions
105
+ # so that we don't have to figure it out from pipette names
106
+ if instrument_name.split("_")[0] == "flex":
107
+ # Flex's API load names have the format 'flex_1channel_1000'
108
+ # From API v2.23 on, this is the name returned by InstrumentContext.name
109
+ pip_type = "p" + instrument_name.split("_")[2]
110
+ else:
111
+ # Until API v2.23, InstrumentContext.name returned the engine-specific names
112
+ # of Flex pipettes. These names, as well as OT2 pipette names,
113
+ # have the format- 'p1000_single_gen2' or 'p1000_single_flex'
114
+ pip_type = instrument_name.split("_")[0]
115
+ valid_vols = VALID_PIP_TIPRACK_VOL[gen_lookup][pip_type]
103
116
  if tiprack_vol not in valid_vols:
104
117
  log.warning(
105
118
  f"The pipette {instrument_name} and its tip rack {tip_rack.load_name}"