opentrons 8.6.0a12__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.
Files changed (38) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/drivers/asyncio/communication/serial_connection.py +8 -5
  3. opentrons/drivers/flex_stacker/driver.py +6 -1
  4. opentrons/hardware_control/backends/flex_protocol.py +1 -0
  5. opentrons/hardware_control/backends/ot3controller.py +25 -13
  6. opentrons/hardware_control/backends/ot3simulator.py +2 -1
  7. opentrons/hardware_control/dev_types.py +3 -1
  8. opentrons/hardware_control/instruments/ot2/pipette_handler.py +1 -0
  9. opentrons/hardware_control/instruments/ot3/pipette_handler.py +1 -0
  10. opentrons/hardware_control/ot3api.py +3 -1
  11. opentrons/hardware_control/protocols/gripper_controller.py +1 -0
  12. opentrons/protocol_api/core/engine/_default_liquid_class_versions.py +56 -0
  13. opentrons/protocol_api/core/engine/instrument.py +143 -18
  14. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +77 -17
  15. opentrons/protocol_api/core/engine/protocol.py +53 -7
  16. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -1
  17. opentrons/protocol_api/core/protocol.py +1 -1
  18. opentrons/protocol_api/labware.py +36 -2
  19. opentrons/protocol_api/module_contexts.py +146 -14
  20. opentrons/protocol_api/protocol_context.py +162 -12
  21. opentrons/protocol_api/validation.py +4 -0
  22. opentrons/protocol_engine/commands/command_unions.py +2 -0
  23. opentrons/protocol_engine/commands/flex_stacker/common.py +13 -0
  24. opentrons/protocol_engine/commands/flex_stacker/store.py +20 -2
  25. opentrons/protocol_engine/execution/labware_movement.py +14 -12
  26. opentrons/protocol_engine/resources/pipette_data_provider.py +3 -0
  27. opentrons/protocol_engine/state/geometry.py +33 -5
  28. opentrons/protocol_engine/state/labware.py +66 -0
  29. opentrons/protocol_engine/state/modules.py +6 -0
  30. opentrons/protocol_engine/state/pipettes.py +12 -3
  31. opentrons/protocol_engine/types/__init__.py +2 -0
  32. opentrons/protocol_engine/types/labware.py +9 -0
  33. opentrons/protocols/api_support/definitions.py +1 -1
  34. {opentrons-8.6.0a12.dist-info → opentrons-8.7.0.dist-info}/METADATA +4 -4
  35. {opentrons-8.6.0a12.dist-info → opentrons-8.7.0.dist-info}/RECORD +38 -37
  36. {opentrons-8.6.0a12.dist-info → opentrons-8.7.0.dist-info}/WHEEL +0 -0
  37. {opentrons-8.6.0a12.dist-info → opentrons-8.7.0.dist-info}/entry_points.txt +0 -0
  38. {opentrons-8.6.0a12.dist-info → opentrons-8.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -405,7 +405,7 @@ class ProtocolContext(CommandPublisher):
405
405
  )
406
406
 
407
407
  @requires_version(2, 0)
408
- def load_labware(
408
+ def load_labware( # noqa: C901
409
409
  self,
410
410
  load_name: str,
411
411
  location: Union[DeckLocation, OffDeckType],
@@ -414,6 +414,11 @@ class ProtocolContext(CommandPublisher):
414
414
  version: Optional[int] = None,
415
415
  adapter: Optional[str] = None,
416
416
  lid: Optional[str] = None,
417
+ *,
418
+ adapter_namespace: Optional[str] = None,
419
+ adapter_version: Optional[int] = None,
420
+ lid_namespace: Optional[str] = None,
421
+ lid_version: Optional[int] = None,
417
422
  ) -> Labware:
418
423
  """Load a labware onto a location.
419
424
 
@@ -454,18 +459,52 @@ class ProtocolContext(CommandPublisher):
454
459
  :param version: The version of the labware definition. You should normally
455
460
  leave this unspecified to let ``load_labware()`` choose a version
456
461
  automatically.
457
- :param adapter: An adapter to load the labware on top of. Accepts the same
458
- values as the ``load_name`` parameter of :py:meth:`.load_adapter`. The
459
- adapter will use the same namespace as the labware, and the API will
460
- choose the adapter's version automatically.
461
462
 
462
- .. versionadded:: 2.15
463
+ :param adapter: The load name of an adapter to load the labware on top of. Accepts
464
+ the same values as the ``load_name`` parameter of :py:meth:`.load_adapter`.
465
+
466
+ .. versionadded:: 2.15
467
+
468
+ :param adapter_namespace: The namespace of the adapter being loaded.
469
+ Applies to ``adapter`` the same way that ``namespace`` applies to ``load_name``.
470
+
471
+ .. versionchanged:: 2.26
472
+ ``adapter_namespace`` may now be specified explicitly.
473
+ Also, when you've specified ``namespace`` but not ``adapter_namespace``,
474
+ ``adapter_namespace`` will now independently follow the same search rules
475
+ described in ``namespace``. Formerly, it took ``namespace``'s exact value.
476
+
477
+ :param adapter_version: The version of the adapter being loaded.
478
+ Applies to ``adapter`` the same way that ``version`` applies to ``load_name``.
479
+
480
+ .. versionchanged:: 2.26
481
+ ``adapter_version`` may now be specified explicitly. Also, when it's unspecified,
482
+ the algorithm to select a version automatically has improved to avoid
483
+ selecting versions that do not exist.
484
+
463
485
  :param lid: A lid to load on the top of the main labware. Accepts the same
464
486
  values as the ``load_name`` parameter of :py:meth:`.load_lid_stack`. The
465
487
  lid will use the same namespace as the labware, and the API will
466
488
  choose the lid's version automatically.
467
489
 
468
- .. versionadded:: 2.23
490
+ .. versionadded:: 2.23
491
+
492
+ :param lid_namespace: The namespace of the lid being loaded.
493
+ Applies to ``lid`` the same way that ``namespace`` applies to ``load_name``.
494
+
495
+ .. versionchanged:: 2.26
496
+ ``lid_namespace`` may now be specified explicitly.
497
+ Also, when you've specified ``namespace`` but not ``lid_namespace``,
498
+ ``lid_namespace`` will now independently follow the same search rules
499
+ described in ``namespace``. Formerly, it took ``namespace``'s exact value.
500
+
501
+ :param lid_version: The version of the adapter being loaded.
502
+ Applies to ``lid`` the same way that ``version`` applies to ``load_name``.
503
+
504
+ .. versionchanged:: 2.26
505
+ ``lid_version`` may now be specified explicitly. Also, when it's unspecified,
506
+ the algorithm to select a version automatically has improved to avoid
507
+ selecting versions that do not exist.
469
508
  """
470
509
 
471
510
  if isinstance(location, OffDeckType) and self._api_version < APIVersion(2, 15):
@@ -475,6 +514,40 @@ class ProtocolContext(CommandPublisher):
475
514
  current_version=f"{self._api_version}",
476
515
  )
477
516
 
517
+ if self._api_version < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE:
518
+ if adapter_namespace is not None:
519
+ raise APIVersionError(
520
+ api_element="The `adapter_namespace` parameter",
521
+ until_version=str(
522
+ validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE
523
+ ),
524
+ current_version=str(self._api_version),
525
+ )
526
+ if adapter_version is not None:
527
+ raise APIVersionError(
528
+ api_element="The `adapter_version` parameter",
529
+ until_version=str(
530
+ validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE
531
+ ),
532
+ current_version=str(self._api_version),
533
+ )
534
+ if lid_namespace is not None:
535
+ raise APIVersionError(
536
+ api_element="The `lid_namespace` parameter",
537
+ until_version=str(
538
+ validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE
539
+ ),
540
+ current_version=str(self._api_version),
541
+ )
542
+ if lid_version is not None:
543
+ raise APIVersionError(
544
+ api_element="The `lid_version` parameter",
545
+ until_version=str(
546
+ validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE
547
+ ),
548
+ current_version=str(self._api_version),
549
+ )
550
+
478
551
  load_name = validation.ensure_lowercase_name(load_name)
479
552
  load_location: Union[OffDeckType, DeckSlotName, StagingSlotName, LabwareCore]
480
553
  if adapter is not None:
@@ -484,10 +557,22 @@ class ProtocolContext(CommandPublisher):
484
557
  until_version="2.15",
485
558
  current_version=f"{self._api_version}",
486
559
  )
560
+
561
+ if (
562
+ self._api_version
563
+ < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE
564
+ ):
565
+ checked_adapter_namespace = namespace
566
+ checked_adapter_version = None
567
+ else:
568
+ checked_adapter_namespace = adapter_namespace
569
+ checked_adapter_version = adapter_version
570
+
487
571
  loaded_adapter = self.load_adapter(
488
572
  load_name=adapter,
489
573
  location=location,
490
- namespace=namespace,
574
+ namespace=checked_adapter_namespace,
575
+ version=checked_adapter_version,
491
576
  )
492
577
  load_location = loaded_adapter._core
493
578
  elif isinstance(location, OffDeckType):
@@ -512,11 +597,22 @@ class ProtocolContext(CommandPublisher):
512
597
  until_version=f"{validation.LID_STACK_VERSION_GATE}",
513
598
  current_version=f"{self._api_version}",
514
599
  )
600
+
601
+ if (
602
+ self._api_version
603
+ < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE
604
+ ):
605
+ checked_lid_namespace = namespace
606
+ checked_lid_version = version
607
+ else:
608
+ checked_lid_namespace = lid_namespace
609
+ checked_lid_version = lid_version
610
+
515
611
  self._core.load_lid(
516
612
  load_name=lid,
517
613
  location=labware_core,
518
- namespace=namespace,
519
- version=version,
614
+ namespace=checked_lid_namespace,
615
+ version=checked_lid_version,
520
616
  )
521
617
 
522
618
  labware = Labware(
@@ -1377,6 +1473,7 @@ class ProtocolContext(CommandPublisher):
1377
1473
  def get_liquid_class(
1378
1474
  self,
1379
1475
  name: str,
1476
+ version: Optional[int] = None,
1380
1477
  ) -> LiquidClass:
1381
1478
  """
1382
1479
  Get an instance of an Opentrons-verified liquid class for use in a Flex protocol.
@@ -1386,12 +1483,14 @@ class ProtocolContext(CommandPublisher):
1386
1483
  - ``"water"``: an Opentrons-verified liquid class based on deionized water.
1387
1484
  - ``"glycerol_50"``: an Opentrons-verified liquid class for viscous liquid. Based on 50% glycerol.
1388
1485
  - ``"ethanol_80"``: an Opentrons-verified liquid class for volatile liquid. Based on 80% ethanol.
1486
+ :param version: The version of the liquid class to retrieve. If left unspecified, the latest definition for the
1487
+ protocol's API version will be loaded.
1389
1488
 
1390
1489
  :raises: ``LiquidClassDefinitionDoesNotExist``: if the specified liquid class does not exist.
1391
1490
 
1392
1491
  :returns: A new LiquidClass object.
1393
1492
  """
1394
- return self._core.get_liquid_class(name=name, version=DEFAULT_LC_VERSION)
1493
+ return self._core.get_liquid_class(name=name, version=version)
1395
1494
 
1396
1495
  @requires_version(2, 24)
1397
1496
  def define_liquid_class(
@@ -1461,6 +1560,9 @@ class ProtocolContext(CommandPublisher):
1461
1560
  adapter: Optional[str] = None,
1462
1561
  namespace: Optional[str] = None,
1463
1562
  version: Optional[int] = None,
1563
+ *,
1564
+ adapter_namespace: Optional[str] = None,
1565
+ adapter_version: Optional[int] = None,
1464
1566
  ) -> Labware:
1465
1567
  """
1466
1568
  Load a stack of Opentrons Tough Auto-Sealing Lids onto a valid deck location or adapter.
@@ -1468,13 +1570,17 @@ class ProtocolContext(CommandPublisher):
1468
1570
  :param str load_name: A string to use for looking up a lid definition.
1469
1571
  You can find the ``load_name`` for any compatible lid on the Opentrons
1470
1572
  `Labware Library <https://labware.opentrons.com>`_.
1573
+
1471
1574
  :param location: Either a :ref:`deck slot <deck-slots>`,
1472
1575
  like ``1``, ``"1"``, or ``"D1"``, or a valid Opentrons Adapter.
1576
+
1473
1577
  :param int quantity: The quantity of lids to be loaded in the stack.
1578
+
1474
1579
  :param adapter: An adapter to load the lid stack on top of. Accepts the same
1475
1580
  values as the ``load_name`` parameter of :py:meth:`.load_adapter`. The
1476
1581
  adapter will use the same namespace as the lid labware, and the API will
1477
1582
  choose the adapter's version automatically.
1583
+
1478
1584
  :param str namespace: The namespace that the lid labware definition belongs to.
1479
1585
  If unspecified, the API will automatically search two namespaces:
1480
1586
 
@@ -1490,6 +1596,21 @@ class ProtocolContext(CommandPublisher):
1490
1596
  leave this unspecified to let ``load_lid_stack()`` choose a version
1491
1597
  automatically.
1492
1598
 
1599
+ :param adapter_namespace: The namespace of the adapter being loaded.
1600
+ Applies to ``adapter`` the same way that ``namespace`` applies to ``load_name``.
1601
+
1602
+ .. versionchanged:: 2.26
1603
+ ``adapter_namespace`` may now be specified explicitly.
1604
+ Also, when you've specified ``namespace`` but not ``adapter_namespace``,
1605
+ ``adapter_namespace`` will now independently follow the same search rules
1606
+ described in ``namespace``. Formerly, it took ``namespace``'s exact value.
1607
+
1608
+ :param adapter_version: The version of the adapter being loaded.
1609
+ Applies to ``adapter`` the same way that ``version`` applies to ``load_name``.
1610
+
1611
+ .. versionadded:: 2.26
1612
+ ``adapter_version`` may now be specified explicitly.
1613
+
1493
1614
  :return: The initialized and loaded labware object representing the lid stack.
1494
1615
 
1495
1616
  .. versionadded:: 2.23
@@ -1502,6 +1623,24 @@ class ProtocolContext(CommandPublisher):
1502
1623
  current_version=f"{self._api_version}",
1503
1624
  )
1504
1625
 
1626
+ if self._api_version < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE:
1627
+ if adapter_namespace is not None:
1628
+ raise APIVersionError(
1629
+ api_element="The `adapter_namespace` parameter",
1630
+ until_version=str(
1631
+ validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE
1632
+ ),
1633
+ current_version=str(self._api_version),
1634
+ )
1635
+ if adapter_version is not None:
1636
+ raise APIVersionError(
1637
+ api_element="The `adapter_version` parameter",
1638
+ until_version=str(
1639
+ validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE
1640
+ ),
1641
+ current_version=str(self._api_version),
1642
+ )
1643
+
1505
1644
  load_location: Union[DeckSlotName, StagingSlotName, LabwareCore]
1506
1645
  if isinstance(location, Labware):
1507
1646
  load_location = location._core
@@ -1514,10 +1653,21 @@ class ProtocolContext(CommandPublisher):
1514
1653
  if isinstance(load_location, DeckSlotName) or isinstance(
1515
1654
  load_location, StagingSlotName
1516
1655
  ):
1656
+ if (
1657
+ self._api_version
1658
+ < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE
1659
+ ):
1660
+ checked_adapter_namespace = namespace
1661
+ checked_adapter_version = None
1662
+ else:
1663
+ checked_adapter_namespace = adapter_namespace
1664
+ checked_adapter_version = adapter_version
1665
+
1517
1666
  loaded_adapter = self.load_adapter(
1518
1667
  load_name=adapter,
1519
1668
  location=load_location.value,
1520
- namespace=namespace,
1669
+ namespace=checked_adapter_namespace,
1670
+ version=checked_adapter_version,
1521
1671
  )
1522
1672
  load_location = loaded_adapter._core
1523
1673
  else:
@@ -62,6 +62,10 @@ LID_STACK_VERSION_GATE = APIVersion(2, 23)
62
62
  # The first APIVersion where Python protocols can use the Flex Stacker module.
63
63
  FLEX_STACKER_VERSION_GATE = APIVersion(2, 23)
64
64
 
65
+ # The first APIVersion where various "multi labware load" methods allow you to specify
66
+ # the namespace and version of adapters and lids separately from the main labware.
67
+ NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE = APIVersion(2, 26)
68
+
65
69
 
66
70
  class InvalidPipetteMountError(ValueError):
67
71
  """An error raised when attempting to load pipettes on an invalid mount."""
@@ -20,6 +20,7 @@ from .flex_stacker.common import (
20
20
  FlexStackerHopperError,
21
21
  FlexStackerLabwareRetrieveError,
22
22
  FlexStackerShuttleOccupiedError,
23
+ FlexStackerLabwareStoreError,
23
24
  )
24
25
 
25
26
  from . import absorbance_reader
@@ -948,6 +949,7 @@ CommandDefinedErrorData = Union[
948
949
  DefinedErrorData[FlexStackerHopperError],
949
950
  DefinedErrorData[FlexStackerLabwareRetrieveError],
950
951
  DefinedErrorData[FlexStackerShuttleOccupiedError],
952
+ DefinedErrorData[FlexStackerLabwareStoreError],
951
953
  ]
952
954
 
953
955
 
@@ -148,6 +148,19 @@ class FlexStackerLabwareRetrieveError(ErrorOccurrence):
148
148
  errorInfo: FailedLabware
149
149
 
150
150
 
151
+ class FlexStackerLabwareStoreError(ErrorOccurrence):
152
+ """Returned when the labware was not able to get to the shuttle."""
153
+
154
+ isDefined: bool = True
155
+ errorType: Literal[
156
+ "flexStackerLabwareStoreFailed"
157
+ ] = "flexStackerLabwareStoreFailed"
158
+
159
+ errorCode: str = ErrorCodes.STACKER_SHUTTLE_LABWARE_FAILED.value.code
160
+ detail: str = ErrorCodes.STACKER_SHUTTLE_LABWARE_FAILED.value.detail
161
+ errorInfo: FailedLabware
162
+
163
+
151
164
  class FlexStackerShuttleOccupiedError(ErrorOccurrence):
152
165
  """Returned when the Flex Stacker Shuttle is occupied when it shouldn't be."""
153
166
 
@@ -10,6 +10,7 @@ from opentrons_shared_data.labware.labware_definition import LabwareDefinition
10
10
  from opentrons_shared_data.errors.exceptions import (
11
11
  FlexStackerStallError,
12
12
  FlexStackerShuttleMissingError,
13
+ FlexStackerShuttleLabwareError,
13
14
  )
14
15
 
15
16
  from ..command import (
@@ -22,6 +23,7 @@ from ..command import (
22
23
  from ..flex_stacker.common import (
23
24
  FlexStackerStallOrCollisionError,
24
25
  FlexStackerShuttleError,
26
+ FlexStackerLabwareStoreError,
25
27
  labware_locations_for_group,
26
28
  labware_location_base_sequence,
27
29
  primary_location_sequence,
@@ -115,7 +117,8 @@ class StoreResult(BaseModel):
115
117
  _ExecuteReturn = Union[
116
118
  SuccessData[StoreResult],
117
119
  DefinedErrorData[FlexStackerStallOrCollisionError]
118
- | DefinedErrorData[FlexStackerShuttleError],
120
+ | DefinedErrorData[FlexStackerShuttleError]
121
+ | DefinedErrorData[FlexStackerLabwareStoreError],
119
122
  ]
120
123
 
121
124
 
@@ -180,7 +183,7 @@ class StoreImpl(AbstractCommandImpl[StoreParams, _ExecuteReturn]):
180
183
  )
181
184
  return labware_ids[0], None, lid_id
182
185
 
183
- async def execute(self, params: StoreParams) -> _ExecuteReturn:
186
+ async def execute(self, params: StoreParams) -> _ExecuteReturn: # noqa: C901
184
187
  """Execute the labware storage command."""
185
188
  stacker_state = self._state_view.modules.get_flex_stacker_substate(
186
189
  params.moduleId
@@ -250,6 +253,21 @@ class StoreImpl(AbstractCommandImpl[StoreParams, _ExecuteReturn]):
250
253
  errorInfo={"labwareId": primary_id},
251
254
  ),
252
255
  )
256
+ except FlexStackerShuttleLabwareError as e:
257
+ return DefinedErrorData(
258
+ public=FlexStackerLabwareStoreError(
259
+ id=self._model_utils.generate_id(),
260
+ createdAt=self._model_utils.get_timestamp(),
261
+ wrappedErrors=[
262
+ ErrorOccurrence.from_failed(
263
+ id=self._model_utils.generate_id(),
264
+ createdAt=self._model_utils.get_timestamp(),
265
+ error=e,
266
+ )
267
+ ],
268
+ errorInfo={"labwareId": primary_id},
269
+ ),
270
+ )
253
271
 
254
272
  id_list = [
255
273
  id for id in (primary_id, maybe_adapter_id, maybe_lid_id) if id is not None
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Optional, TYPE_CHECKING, overload
6
6
 
7
- from opentrons_shared_data.labware.labware_definition import LabwareDefinition
7
+ from opentrons_shared_data.labware.labware_definition import LabwareDefinition, Quirks
8
8
 
9
9
  from opentrons.types import Point
10
10
 
@@ -234,23 +234,25 @@ class LabwareMovementHandler:
234
234
  # we only want to check position after the gripper has opened and
235
235
  # should be holding labware
236
236
  if holding_labware:
237
- labware_bbox = self._state_store.labware.get_dimensions(
238
- labware_definition=labware_definition
239
- )
240
- well_bbox = self._state_store.labware.get_well_bbox(
237
+ grip_specs = self._state_store.labware.get_gripper_width_specs(
241
238
  labware_definition=labware_definition
242
239
  )
240
+
241
+ disable_geometry_grip_check = False
242
+ if labware_definition.parameters.quirks is not None:
243
+ disable_geometry_grip_check = (
244
+ Quirks.disableGeometryBasedGripCheck.value
245
+ in labware_definition.parameters.quirks
246
+ )
247
+
243
248
  # todo(mm, 2024-09-26): This currently raises a lower-level 2015 FailedGripperPickupError.
244
249
  # Convert this to a higher-level 3001 LabwareDroppedError or 3002 LabwareNotPickedUpError,
245
250
  # depending on what waypoint we're at, to propagate a more specific error code to users.
246
251
  ot3api.raise_error_if_gripper_pickup_failed(
247
- expected_grip_width=labware_bbox.y,
248
- grip_width_uncertainty_wider=abs(
249
- max(well_bbox.y - labware_bbox.y, 0)
250
- ),
251
- grip_width_uncertainty_narrower=abs(
252
- min(well_bbox.y - labware_bbox.y, 0)
253
- ),
252
+ expected_grip_width=grip_specs.targetY,
253
+ grip_width_uncertainty_wider=grip_specs.uncertaintyWider,
254
+ grip_width_uncertainty_narrower=grip_specs.uncertaintyNarrower,
255
+ disable_geometry_grip_check=disable_geometry_grip_check,
254
256
  )
255
257
  await ot3api.move_to(
256
258
  mount=gripper_mount, abs_position=waypoint_data.position
@@ -70,6 +70,7 @@ class LoadedStaticPipetteData:
70
70
  plunger_positions: Dict[str, float]
71
71
  shaft_ul_per_mm: float
72
72
  available_sensors: pipette_definition.AvailableSensorDefinition
73
+ volume_mode: pip_types.LiquidClasses # pip_types Liquid Classes refers to volume modes
73
74
 
74
75
 
75
76
  class VirtualPipetteDataProvider:
@@ -298,6 +299,7 @@ class VirtualPipetteDataProvider:
298
299
  shaft_ul_per_mm=config.shaft_ul_per_mm,
299
300
  available_sensors=config.available_sensors
300
301
  or pipette_definition.AvailableSensorDefinition(sensors=[]),
302
+ volume_mode=liquid_class,
301
303
  )
302
304
 
303
305
  def get_virtual_pipette_static_config(
@@ -353,6 +355,7 @@ def get_pipette_static_config(
353
355
  plunger_positions=pipette_dict["plunger_positions"],
354
356
  shaft_ul_per_mm=pipette_dict["shaft_ul_per_mm"],
355
357
  available_sensors=available_sensors,
358
+ volume_mode=pipette_dict["volume_mode"],
356
359
  )
357
360
 
358
361
 
@@ -274,12 +274,20 @@ class GeometryView:
274
274
  try:
275
275
  labware_id = self._labware.get_id_by_module(module_id=module_id)
276
276
  except LabwareNotLoadedOnModuleError:
277
- return self._modules.get_module_highest_z(
278
- module_id=module_id,
279
- addressable_areas=self._addressable_areas,
280
- )
277
+ # For the time being we will ignore column 4 modules in this check to avoid conflating results
278
+ if self._modules.is_column_4_module(slot_item.model) is False:
279
+ return self._modules.get_module_highest_z(
280
+ module_id=module_id,
281
+ addressable_areas=self._addressable_areas,
282
+ )
281
283
  else:
282
- return self.get_highest_z_of_labware_stack(labware_id)
284
+ # For the time being we will ignore column 4 modules in this check to avoid conflating results
285
+ if self._modules.is_column_4_module(slot_item.model) is False:
286
+ return self.get_highest_z_of_labware_stack(labware_id)
287
+ # todo (cb, 2025-09-15): For now we skip column 4 modules and handle them seperately in
288
+ # get_highest_z_of_column_4_module, so this will return 0. In the future we may want to consolidate
289
+ # this to make it more apparently at this point in the query process.
290
+ return 0
283
291
  elif isinstance(slot_item, LoadedLabware):
284
292
  # get stacked heights of all labware in the slot
285
293
  return self.get_highest_z_of_labware_stack(slot_item.id)
@@ -301,6 +309,26 @@ class GeometryView:
301
309
  return self.get_labware_highest_z(labware_id)
302
310
  return self.get_highest_z_of_labware_stack(stacked_labware_id)
303
311
 
312
+ def get_highest_z_of_column_4_module(self, module: LoadedModule) -> float:
313
+ """Get the highest Z-point of the topmost labware in the stack of labware on the given column 4 module.
314
+
315
+ If there is no labware on the given module, returns highest z of the module.
316
+ """
317
+ if self._modules.is_column_4_module(module.model):
318
+ try:
319
+ labware_id = self._labware.get_id_by_module(module_id=module.id)
320
+ except LabwareNotLoadedOnModuleError:
321
+ return self._modules.get_module_highest_z(
322
+ module_id=module.id,
323
+ addressable_areas=self._addressable_areas,
324
+ )
325
+ else:
326
+ return self.get_highest_z_of_labware_stack(labware_id)
327
+ else:
328
+ raise ValueError(
329
+ "Module must be a Column 4 Module to determine maximum z height."
330
+ )
331
+
304
332
  def get_min_travel_z(
305
333
  self,
306
334
  pipette_id: str,
@@ -46,6 +46,7 @@ from ..types import (
46
46
  AddressableAreaLocation,
47
47
  NonStackedLocation,
48
48
  Dimensions,
49
+ GripSpecs,
49
50
  LabwareOffset,
50
51
  LabwareOffsetVector,
51
52
  LabwareOffsetLocationSequence,
@@ -1430,3 +1431,68 @@ class LabwareView:
1430
1431
  ):
1431
1432
  return Dimensions(0, 0, 0)
1432
1433
  return Dimensions(max_x - min_x, max_y - min_y, max_z)
1434
+
1435
+ def _gripper_uncertainty_narrower(
1436
+ self, labware_bbox: Dimensions, well_bbox: Dimensions, target_grip_width: float
1437
+ ) -> float:
1438
+ """Most narrower the gripper can be than the target while still likely gripping successfully.
1439
+
1440
+ This number can't just be the 0, because that is not going to be accurate if the labware is
1441
+ skirted - the dimensions are a full bounding box including the skirt, and the labware is
1442
+ narrower than that at the point where it is gripped. The general heuristic is that we can't
1443
+ get to the wells; but some labware don't have wells, so we need alternate values.
1444
+
1445
+ The number will be interpreted relative to the target width, which is (for now) the labware
1446
+ outer bounding box.
1447
+
1448
+ TODO: This should be a number looked up from the definition.
1449
+ """
1450
+ if well_bbox.y == 0:
1451
+ # This labware has no wells; use a fixed minimum
1452
+ return 5
1453
+ if well_bbox.y > labware_bbox.y:
1454
+ # This labware has a very odd definition with wells outside its dimensions.
1455
+ # Return the smaller value.
1456
+ return 0
1457
+ # An ok heuristic for successful grip is if we don't get all the way to the wells.
1458
+ return target_grip_width - well_bbox.y
1459
+
1460
+ def _gripper_uncertainty_wider(
1461
+ self, labware_bbox: Dimensions, well_bbox: Dimensions, target_grip_width: float
1462
+ ) -> float:
1463
+ """Most wider the gripper can be than the target while still likely gripping successfully.
1464
+
1465
+ This can be a lot closer to 0, since the bounding box of the labware will certainly be the
1466
+ widest point (if it's defined without error), but since there might be error in the
1467
+ definition we allow some slop.
1468
+
1469
+ The number will be interpreted relative to the target width, which is (for now) the labware
1470
+ outer bounding box.
1471
+
1472
+ TODO: This should be a number looked up from the definition.
1473
+ """
1474
+ # This will be 0 unless the wells are wider than the labware
1475
+ return max(well_bbox.y - target_grip_width, 0)
1476
+
1477
+ def get_gripper_width_specs(
1478
+ self, labware_definition: LabwareDefinition
1479
+ ) -> GripSpecs:
1480
+ """Get the target and bounds for a successful grip of this labware."""
1481
+ outer_bounds = self.get_dimensions(labware_definition=labware_definition)
1482
+ well_bounds = self.get_well_bbox(labware_definition=labware_definition)
1483
+ narrower = self._gripper_uncertainty_narrower(
1484
+ labware_bbox=outer_bounds,
1485
+ well_bbox=well_bounds,
1486
+ target_grip_width=outer_bounds.y,
1487
+ )
1488
+ wider = self._gripper_uncertainty_wider(
1489
+ labware_bbox=outer_bounds,
1490
+ well_bbox=well_bounds,
1491
+ target_grip_width=outer_bounds.y,
1492
+ )
1493
+ return GripSpecs(
1494
+ # TODO: This should be a number looked up from the definition.
1495
+ targetY=outer_bounds.y,
1496
+ uncertaintyNarrower=narrower,
1497
+ uncertaintyWider=wider,
1498
+ )
@@ -1330,6 +1330,12 @@ class ModuleView:
1330
1330
  f"Module {module.model} is already present at {location}."
1331
1331
  )
1332
1332
 
1333
+ def is_column_4_module(self, model: ModuleModel) -> bool:
1334
+ """Determine whether or not a module is a Column 4 Module."""
1335
+ if model in _COLUMN_4_MODULES:
1336
+ return True
1337
+ return False
1338
+
1333
1339
  def get_default_gripper_offsets(
1334
1340
  self, module_id: str
1335
1341
  ) -> Optional[LabwareMovementOffsetData]:
@@ -17,7 +17,10 @@ from typing_extensions import assert_never
17
17
 
18
18
  from opentrons_shared_data.pipette import pipette_definition
19
19
  from opentrons_shared_data.pipette.ul_per_mm import calculate_ul_per_mm
20
- from opentrons_shared_data.pipette.types import UlPerMmAction
20
+ from opentrons_shared_data.pipette.types import (
21
+ UlPerMmAction,
22
+ LiquidClasses as VolumeModes,
23
+ )
21
24
 
22
25
  from opentrons.config.defaults_ot2 import Z_RETRACT_DISTANCE
23
26
  from opentrons.hardware_control.dev_types import PipetteDict
@@ -107,6 +110,7 @@ class StaticPipetteConfig:
107
110
  plunger_positions: Dict[str, float]
108
111
  shaft_ul_per_mm: float
109
112
  available_sensors: pipette_definition.AvailableSensorDefinition
113
+ volume_mode: VolumeModes
110
114
 
111
115
 
112
116
  @dataclasses.dataclass
@@ -212,7 +216,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
212
216
  # we identify tip classes - looking things up by volume is not enough.
213
217
  tip_configuration = list(
214
218
  static_config.tip_configuration_lookup_table.values()
215
- )[0]
219
+ )[-1]
216
220
  self._state.flow_rates_by_id[pipette_id] = FlowRates(
217
221
  default_blow_out=tip_configuration.default_blowout_flowrate.values_by_api_level,
218
222
  default_aspirate=tip_configuration.default_aspirate_flowrate.values_by_api_level,
@@ -230,7 +234,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
230
234
  # TODO(seth,9/11/2023): bad way to do defaulting, see above.
231
235
  tip_configuration = list(
232
236
  static_config.tip_configuration_lookup_table.values()
233
- )[0]
237
+ )[-1]
234
238
  self._state.flow_rates_by_id[pipette_id] = FlowRates(
235
239
  default_blow_out=tip_configuration.default_blowout_flowrate.values_by_api_level,
236
240
  default_aspirate=tip_configuration.default_aspirate_flowrate.values_by_api_level,
@@ -313,6 +317,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
313
317
  plunger_positions=config.plunger_positions,
314
318
  shaft_ul_per_mm=config.shaft_ul_per_mm,
315
319
  available_sensors=config.available_sensors,
320
+ volume_mode=config.volume_mode,
316
321
  )
317
322
  self._state.flow_rates_by_id[
318
323
  state_update.pipette_config.pipette_id
@@ -867,6 +872,10 @@ class PipetteView:
867
872
  return False
868
873
  return True
869
874
 
875
+ def get_is_low_volume_mode(self, pipette_id: str) -> bool:
876
+ """Determine if the pipette is currently in low volume mode."""
877
+ return self.get_config(pipette_id).volume_mode == VolumeModes.lowVolumeDefault
878
+
870
879
  def lookup_volume_to_mm_conversion(
871
880
  self, pipette_id: str, volume: float, action: str
872
881
  ) -> float: