opentrons 8.6.0a11__py3-none-any.whl → 8.7.0__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 (40) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/cli/analyze.py +58 -2
  3. opentrons/drivers/asyncio/communication/serial_connection.py +8 -5
  4. opentrons/drivers/flex_stacker/driver.py +6 -1
  5. opentrons/hardware_control/backends/flex_protocol.py +1 -0
  6. opentrons/hardware_control/backends/ot3controller.py +25 -13
  7. opentrons/hardware_control/backends/ot3simulator.py +2 -1
  8. opentrons/hardware_control/dev_types.py +3 -1
  9. opentrons/hardware_control/instruments/ot2/pipette_handler.py +1 -0
  10. opentrons/hardware_control/instruments/ot3/pipette_handler.py +1 -0
  11. opentrons/hardware_control/ot3api.py +3 -1
  12. opentrons/hardware_control/protocols/gripper_controller.py +1 -0
  13. opentrons/protocol_api/core/engine/_default_liquid_class_versions.py +56 -0
  14. opentrons/protocol_api/core/engine/instrument.py +143 -18
  15. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +77 -17
  16. opentrons/protocol_api/core/engine/protocol.py +53 -7
  17. opentrons/protocol_api/core/engine/transfer_components_executor.py +36 -20
  18. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -1
  19. opentrons/protocol_api/core/protocol.py +1 -1
  20. opentrons/protocol_api/labware.py +36 -2
  21. opentrons/protocol_api/module_contexts.py +146 -14
  22. opentrons/protocol_api/protocol_context.py +162 -12
  23. opentrons/protocol_api/validation.py +4 -0
  24. opentrons/protocol_engine/commands/command_unions.py +2 -0
  25. opentrons/protocol_engine/commands/flex_stacker/common.py +13 -0
  26. opentrons/protocol_engine/commands/flex_stacker/store.py +20 -2
  27. opentrons/protocol_engine/execution/labware_movement.py +14 -12
  28. opentrons/protocol_engine/resources/pipette_data_provider.py +3 -0
  29. opentrons/protocol_engine/state/geometry.py +33 -5
  30. opentrons/protocol_engine/state/labware.py +66 -0
  31. opentrons/protocol_engine/state/modules.py +6 -0
  32. opentrons/protocol_engine/state/pipettes.py +12 -3
  33. opentrons/protocol_engine/types/__init__.py +2 -0
  34. opentrons/protocol_engine/types/labware.py +9 -0
  35. opentrons/protocols/api_support/definitions.py +1 -1
  36. {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/METADATA +4 -4
  37. {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/RECORD +40 -39
  38. {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/WHEEL +0 -0
  39. {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/entry_points.txt +0 -0
  40. {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/licenses/LICENSE +0 -0
opentrons/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '8.6.0a11'
32
- __version_tuple__ = version_tuple = (8, 6, 0, 'a11')
31
+ __version__ = version = '8.7.0'
32
+ __version_tuple__ = version_tuple = (8, 7, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
opentrons/cli/analyze.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Opentrons analyze CLI."""
2
+
2
3
  import click
3
4
 
4
5
  from anyio import run
@@ -24,7 +25,9 @@ from typing import (
24
25
  import logging
25
26
  import sys
26
27
  import json
28
+ import gc
27
29
 
30
+ from opentrons.protocol_engine import ProtocolEngine
28
31
  from opentrons.protocol_engine.types import (
29
32
  RunTimeParameter,
30
33
  CSVRuntimeParamPaths,
@@ -94,6 +97,18 @@ class _Output:
94
97
  help="Return analysis results as JSON, formatted for human eyes. Specify --human-json-output=- to use stdout, but be aware that Python protocols may contain print() which will make the output JSON invalid.",
95
98
  type=click.File(mode="wb"),
96
99
  )
100
+ @click.option(
101
+ "--leaks",
102
+ help="Fail (via exit code) if the analysis engine has not been garbage collected after analysis is complete.",
103
+ is_flag=True,
104
+ default=False,
105
+ )
106
+ @click.option(
107
+ "--leaks-debug",
108
+ help="Drop into a PDB shell if a leak is detected",
109
+ is_flag=True,
110
+ default=False,
111
+ )
97
112
  @click.option(
98
113
  "--check",
99
114
  help="Fail (via exit code) if the protocol had an error. If not specified, always succeed.",
@@ -133,6 +148,8 @@ def analyze(
133
148
  log_output: str,
134
149
  log_level: str,
135
150
  check: bool,
151
+ leaks: bool,
152
+ leaks_debug: bool,
136
153
  ) -> int:
137
154
  """Analyze a protocol.
138
155
 
@@ -147,7 +164,18 @@ def analyze(
147
164
 
148
165
  try:
149
166
  with _capture_logs(log_output, log_level):
150
- sys.exit(run(_analyze, files, rtp_values, rtp_files, outputs, check))
167
+ sys.exit(
168
+ run(
169
+ _analyze,
170
+ files,
171
+ rtp_values,
172
+ rtp_files,
173
+ outputs,
174
+ check,
175
+ leaks or leaks_debug,
176
+ leaks_debug,
177
+ )
178
+ )
151
179
  except click.ClickException:
152
180
  raise
153
181
  except Exception as e:
@@ -344,12 +372,14 @@ async def _do_analyze(
344
372
  return await orchestrator.run(deck_configuration=[])
345
373
 
346
374
 
347
- async def _analyze(
375
+ async def _analyze( # noqa: C901
348
376
  files_and_dirs: Sequence[Path],
349
377
  rtp_values: str,
350
378
  rtp_files: str,
351
379
  outputs: Sequence[_Output],
352
380
  check: bool,
381
+ fail_on_leak: bool,
382
+ debug_on_leak: bool,
353
383
  ) -> int:
354
384
  input_files = _get_input_files(files_and_dirs)
355
385
  parsed_rtp_values = _get_runtime_parameter_values(rtp_values)
@@ -366,6 +396,32 @@ async def _analyze(
366
396
  analysis = await _do_analyze(protocol_source, parsed_rtp_values, rtp_paths)
367
397
  return_code = _get_return_code(analysis)
368
398
 
399
+ # This ugly code checks to see if an engine remains past garbage collection
400
+ # after analysis is complete.
401
+ # It should be here and open coded to make it a little easier to present
402
+ # the debug option.
403
+ if fail_on_leak or debug_on_leak:
404
+ gc.collect()
405
+ leaked_engine = next(
406
+ (obj for obj in gc.get_objects() if isinstance(obj, ProtocolEngine)), None
407
+ )
408
+ if leaked_engine:
409
+ if fail_on_leak:
410
+ print(
411
+ "A ProtocolEngine instance exists even after garbage collection; "
412
+ "some thing (likely in the protocol) has caused it to be leaked, "
413
+ "likely by reference to the engine or something that refers to the "
414
+ "engine after the run function ends.",
415
+ file=sys.stderr,
416
+ )
417
+ return_code = -2
418
+ if debug_on_leak:
419
+ print(
420
+ "You are now in an interactive PDB (https://docs.python.org/3.10/library/pdb.html) "
421
+ "session; the leaked engine is bound to the variable leaked_engine."
422
+ )
423
+ breakpoint()
424
+
369
425
  if not outputs:
370
426
  return return_code
371
427
 
@@ -462,7 +462,10 @@ class AsyncResponseSerialConnection(SerialConnection):
462
462
  self._async_error_ack = async_error_ack.lower()
463
463
 
464
464
  async def send_command(
465
- self, command: CommandBuilder, retries: int = 0, timeout: Optional[float] = None
465
+ self,
466
+ command: CommandBuilder,
467
+ retries: int | None = None,
468
+ timeout: float | None = None,
466
469
  ) -> str:
467
470
  """
468
471
  Send a command and return the response.
@@ -478,12 +481,12 @@ class AsyncResponseSerialConnection(SerialConnection):
478
481
  """
479
482
  return await self.send_data(
480
483
  data=command.build(),
481
- retries=retries or self._number_of_retries,
484
+ retries=retries if retries is not None else self._number_of_retries,
482
485
  timeout=timeout,
483
486
  )
484
487
 
485
488
  async def send_data(
486
- self, data: str, retries: int = 0, timeout: Optional[float] = None
489
+ self, data: str, retries: int | None = None, timeout: float | None = None
487
490
  ) -> str:
488
491
  """
489
492
  Send data and return the response.
@@ -501,7 +504,8 @@ class AsyncResponseSerialConnection(SerialConnection):
501
504
  "timeout", timeout
502
505
  ):
503
506
  return await self._send_data(
504
- data=data, retries=retries or self._number_of_retries
507
+ data=data,
508
+ retries=retries if retries is not None else self._number_of_retries,
505
509
  )
506
510
 
507
511
  async def _send_data(self, data: str, retries: int = 0) -> str:
@@ -517,7 +521,6 @@ class AsyncResponseSerialConnection(SerialConnection):
517
521
  Raises: SerialException
518
522
  """
519
523
  data_encode = data.encode()
520
- retries = retries or self._number_of_retries
521
524
 
522
525
  for retry in range(retries + 1):
523
526
  log.debug(f"{self._name}: Write -> {data_encode!r}")
@@ -461,7 +461,12 @@ class FlexStackerDriver(AbstractFlexStackerDriver):
461
461
  command = GCODE.GET_TOF_MEASUREMENT.build_command().add_element(sensor.name)
462
462
  if resend:
463
463
  command.add_element("R")
464
- resp = await self._connection.send_command(command)
464
+
465
+ # Note: We DONT want to auto resend the request if it fails, because the
466
+ # firmware will send the next frame id instead of the current one missed.
467
+ # So lets set `retries=0` so we only send the frame once and we can
468
+ # use the retry mechanism of the `get_tof_histogram` method instead.
469
+ resp = await self._connection.send_command(command, retries=0)
465
470
  return self.parse_get_tof_measurement(resp)
466
471
 
467
472
  async def get_tof_histogram(self, sensor: TOFSensor) -> TOFMeasurementResult:
@@ -451,6 +451,7 @@ class FlexBackend(Protocol):
451
451
  max_allowed_grip_error: float,
452
452
  hard_limit_lower: float,
453
453
  hard_limit_upper: float,
454
+ disable_geometry_grip_check: bool = False,
454
455
  ) -> None:
455
456
  ...
456
457
 
@@ -686,9 +686,9 @@ class OT3Controller(FlexBackend):
686
686
  return (
687
687
  MoveGroupRunner(
688
688
  move_groups=[move_group],
689
- ignore_stalls=True
690
- if not self._feature_flags.stall_detection_enabled
691
- else False,
689
+ ignore_stalls=(
690
+ True if not self._feature_flags.stall_detection_enabled else False
691
+ ),
692
692
  ),
693
693
  False,
694
694
  )
@@ -712,9 +712,9 @@ class OT3Controller(FlexBackend):
712
712
  return (
713
713
  MoveGroupRunner(
714
714
  move_groups=[tip_motor_move_group],
715
- ignore_stalls=True
716
- if not self._feature_flags.stall_detection_enabled
717
- else False,
715
+ ignore_stalls=(
716
+ True if not self._feature_flags.stall_detection_enabled else False
717
+ ),
718
718
  ),
719
719
  True,
720
720
  )
@@ -939,9 +939,9 @@ class OT3Controller(FlexBackend):
939
939
 
940
940
  runner = MoveGroupRunner(
941
941
  move_groups=[move_group],
942
- ignore_stalls=True
943
- if not self._feature_flags.stall_detection_enabled
944
- else False,
942
+ ignore_stalls=(
943
+ True if not self._feature_flags.stall_detection_enabled else False
944
+ ),
945
945
  )
946
946
  try:
947
947
  positions = await runner.run(can_messenger=self._messenger)
@@ -976,9 +976,9 @@ class OT3Controller(FlexBackend):
976
976
  move_group = self._build_tip_action_group(origin, targets)
977
977
  runner = MoveGroupRunner(
978
978
  move_groups=[move_group],
979
- ignore_stalls=True
980
- if not self._feature_flags.stall_detection_enabled
981
- else False,
979
+ ignore_stalls=(
980
+ True if not self._feature_flags.stall_detection_enabled else False
981
+ ),
982
982
  )
983
983
  try:
984
984
  positions = await runner.run(can_messenger=self._messenger)
@@ -1763,6 +1763,7 @@ class OT3Controller(FlexBackend):
1763
1763
  max_allowed_grip_error: float,
1764
1764
  hard_limit_lower: float,
1765
1765
  hard_limit_upper: float,
1766
+ disable_geometry_grip_check: bool = False,
1766
1767
  ) -> None:
1767
1768
  """
1768
1769
  Check if the gripper is at the expected location.
@@ -1777,7 +1778,16 @@ class OT3Controller(FlexBackend):
1777
1778
  expected_grip_width + grip_width_uncertainty_wider
1778
1779
  )
1779
1780
  current_gripper_position = jaw_width
1780
- if isclose(current_gripper_position, hard_limit_lower):
1781
+ log.info(
1782
+ f"Checking gripper position: current {jaw_width}; max error {max_allowed_grip_error}; hard limits {hard_limit_lower}, {hard_limit_upper}; expected {expected_gripper_position_min}, {expected_grip_width}, {expected_gripper_position_max}; uncertainty {grip_width_uncertainty_narrower}, {grip_width_uncertainty_wider}"
1783
+ )
1784
+ if (
1785
+ isclose(current_gripper_position, hard_limit_lower)
1786
+ # this odd check handles internal backlash that can lead the position to read as if
1787
+ # the gripper has overshot its lower bound; this is physically impossible and an
1788
+ # artifact of the gearing, so it always indicates a hard stop
1789
+ or current_gripper_position < hard_limit_lower
1790
+ ):
1781
1791
  raise FailedGripperPickupError(
1782
1792
  message="Failed to grip: jaws all the way closed",
1783
1793
  details={
@@ -1796,6 +1806,7 @@ class OT3Controller(FlexBackend):
1796
1806
  if (
1797
1807
  current_gripper_position - expected_gripper_position_min
1798
1808
  < -max_allowed_grip_error
1809
+ and not disable_geometry_grip_check
1799
1810
  ):
1800
1811
  raise FailedGripperPickupError(
1801
1812
  message="Failed to grip: jaws closed too far",
@@ -1809,6 +1820,7 @@ class OT3Controller(FlexBackend):
1809
1820
  if (
1810
1821
  current_gripper_position - expected_gripper_position_max
1811
1822
  > max_allowed_grip_error
1823
+ and not disable_geometry_grip_check
1812
1824
  ):
1813
1825
  raise FailedGripperPickupError(
1814
1826
  message="Failed to grip: jaws could not close far enough",
@@ -781,7 +781,7 @@ class OT3Simulator(FlexBackend):
781
781
  next_fw_version=1,
782
782
  fw_update_needed=False,
783
783
  current_fw_sha="simulated",
784
- pcba_revision="A1",
784
+ pcba_revision="A1.0",
785
785
  update_state=None,
786
786
  )
787
787
  for axis in self._present_axes
@@ -848,6 +848,7 @@ class OT3Simulator(FlexBackend):
848
848
  max_allowed_grip_error: float,
849
849
  hard_limit_lower: float,
850
850
  hard_limit_upper: float,
851
+ disable_geometry_grip_check: bool = False,
851
852
  ) -> None:
852
853
  # This is a (pretty bad) simulation of the gripper actually gripping something,
853
854
  # but it should work.
@@ -14,8 +14,9 @@ from opentrons_shared_data.pipette.types import (
14
14
  PipetteModel,
15
15
  PipetteName,
16
16
  ChannelCount,
17
+ PipetteTipType,
18
+ LiquidClasses,
17
19
  )
18
- from opentrons_shared_data.pipette.types import PipetteTipType
19
20
  from opentrons_shared_data.pipette.pipette_definition import (
20
21
  PipetteConfigurations,
21
22
  SupportedTipsDefinition,
@@ -104,6 +105,7 @@ class PipetteDict(InstrumentDict):
104
105
  plunger_positions: Dict[str, float]
105
106
  shaft_ul_per_mm: float
106
107
  available_sensors: AvailableSensorDefinition
108
+ volume_mode: LiquidClasses # LiquidClasses refer to volume mode in this context
107
109
 
108
110
 
109
111
  class PipetteStateDict(TypedDict):
@@ -267,6 +267,7 @@ class PipetteHandlerProvider(Generic[MountType]):
267
267
  "drop_tip": instr.plunger_positions.drop_tip,
268
268
  }
269
269
  result["shaft_ul_per_mm"] = instr.config.shaft_ul_per_mm
270
+ result["volume_mode"] = instr.liquid_class_name
270
271
  return cast(PipetteDict, result)
271
272
 
272
273
  @property
@@ -294,6 +294,7 @@ class OT3PipetteHandler:
294
294
  }
295
295
  result["shaft_ul_per_mm"] = instr.config.shaft_ul_per_mm
296
296
  result["available_sensors"] = instr.config.available_sensors
297
+ result["volume_mode"] = instr.liquid_class_name
297
298
  return cast(PipetteDict, result)
298
299
 
299
300
  @property
@@ -1462,6 +1462,7 @@ class OT3API(
1462
1462
  expected_grip_width: float,
1463
1463
  grip_width_uncertainty_wider: float,
1464
1464
  grip_width_uncertainty_narrower: float,
1465
+ disable_geometry_grip_check: bool = False,
1465
1466
  ) -> None:
1466
1467
  """Ensure that a gripper pickup succeeded.
1467
1468
 
@@ -1480,8 +1481,9 @@ class OT3API(
1480
1481
  grip_width_uncertainty_narrower,
1481
1482
  gripper.jaw_width,
1482
1483
  gripper.max_allowed_grip_error,
1483
- gripper.max_jaw_width,
1484
1484
  gripper.min_jaw_width,
1485
+ gripper.max_jaw_width,
1486
+ disable_geometry_grip_check,
1485
1487
  )
1486
1488
 
1487
1489
  def gripper_jaw_can_home(self) -> bool:
@@ -41,6 +41,7 @@ class GripperController(Protocol):
41
41
  expected_grip_width: float,
42
42
  grip_width_uncertainty_wider: float,
43
43
  grip_width_uncertainty_narrower: float,
44
+ disable_geometry_grip_check: bool = False,
44
45
  ) -> None:
45
46
  """Ensure that a gripper pickup succeeded."""
46
47
 
@@ -0,0 +1,56 @@
1
+ """The versions of standard liquid classes that the Protocol API should load by default."""
2
+
3
+ from typing import TypeAlias
4
+ from opentrons.protocols.api_support.types import APIVersion
5
+
6
+
7
+ DefaultLiquidClassVersions: TypeAlias = dict[APIVersion, dict[str, int]]
8
+
9
+
10
+ # This:
11
+ #
12
+ # {
13
+ # APIVersion(2, 100): {
14
+ # "foo_liquid": 3,
15
+ # },
16
+ # APIVersion(2, 105): {
17
+ # "foo_liquid": 7
18
+ # }
19
+ # }
20
+ #
21
+ # Means this:
22
+ #
23
+ # apiLevels name Default liquid class version
24
+ # ---------------------------------------------------------------
25
+ # <2.100 foo_liquid 1
26
+ # >=2.100,<2.105 foo_liquid 3
27
+ # >=2.105 foo_liquid 7
28
+ # [any] [anything else] 1
29
+ DEFAULT_LIQUID_CLASS_VERSIONS: DefaultLiquidClassVersions = {
30
+ APIVersion(2, 26): {
31
+ "ethanol_80": 2,
32
+ "glycerol_50": 2,
33
+ "water": 2,
34
+ },
35
+ }
36
+
37
+
38
+ def get_liquid_class_version(
39
+ api_version: APIVersion,
40
+ liquid_class_name: str,
41
+ ) -> int:
42
+ """Return what version of a liquid class the Protocol API should load by default."""
43
+ default_lc_versions_newest_to_oldest = sorted(
44
+ DEFAULT_LIQUID_CLASS_VERSIONS.items(), key=lambda kv: kv[0], reverse=True
45
+ )
46
+ for (
47
+ breakpoint_api_version,
48
+ breakpoint_liquid_class_versions,
49
+ ) in default_lc_versions_newest_to_oldest:
50
+ if (
51
+ api_version >= breakpoint_api_version
52
+ and liquid_class_name in breakpoint_liquid_class_versions
53
+ ):
54
+ return breakpoint_liquid_class_versions[liquid_class_name]
55
+
56
+ return 1