Skip to content

Commit 876bd1e

Browse files
fix(api): Fix floating point errors and backwards incompatibility in volume validation (#14253)
1 parent 1536eda commit 876bd1e

File tree

21 files changed

+414
-129
lines changed

21 files changed

+414
-129
lines changed

api/docs/v2/versioning.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ This table lists the correspondence between Protocol API versions and robot soft
124124
Changes in API Versions
125125
=======================
126126

127+
Version 2.17
128+
------------
129+
130+
- :py:meth:`.dispense` will now raise an error if you try to dispense more than is available.
131+
127132
Version 2.16
128133
------------
129134

api/release-notes.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ log][]. For a list of currently known issues, please see the [Opentrons issue tr
66

77
---
88

9+
## Opentrons Robot Software Changes in [!!EDIT ME WITH THE ACTUAL NUMBER OF THE NEXT RELEASE!!]
10+
11+
### HTTP API
12+
13+
- In the `/runs/commands`, `/maintenance_runs/commands`, and `/protocols` endpoints, the `dispense` command will now return an error if you try to dispense more than you've aspirated, instead of silently clamping.
14+
15+
---
16+
917
## Opentrons Robot Software Changes in 7.1.1
1018

1119
Welcome to the v7.1.1 release of the Opentrons robot software!

api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,12 @@ def plan_check_dispense( # type: ignore[no-untyped-def]
619619
else:
620620
disp_vol = volume
621621

622-
# Ensure we don't dispense more than the current volume
622+
# Ensure we don't dispense more than the current volume.
623+
#
624+
# This clamping is inconsistent with plan_check_aspirate(), which asserts
625+
# that its input is in bounds instead of clamping it. This is left to avoid
626+
# disturbing Python protocols with apiLevel <= 2.13. In newer Python protocols,
627+
# the Protocol Engine layer applies its own bounds checking.
623628
disp_vol = min(instrument.current_volume, disp_vol)
624629

625630
if disp_vol == 0:

api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,12 @@ def plan_check_dispense(
597597
else:
598598
disp_vol = volume
599599

600-
# Ensure we don't dispense more than the current volume
600+
# Ensure we don't dispense more than the current volume.
601+
#
602+
# This clamping is inconsistent with plan_check_aspirate(), which asserts
603+
# that its input is in bounds instead of clamping it. This is to match a quirk
604+
# of the OT-2 version of this class. Protocol Engine does its own clamping,
605+
# so we don't expect this to trigger in practice.
601606
disp_vol = min(instrument.current_volume, disp_vol)
602607
is_full_dispense = numpy.isclose(instrument.current_volume - disp_vol, 0)
603608

api/src/opentrons/hardware_control/ot3api.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1908,7 +1908,6 @@ async def dispense(
19081908
mount: Union[top_types.Mount, OT3Mount],
19091909
volume: Optional[float] = None,
19101910
rate: float = 1.0,
1911-
# TODO (tz, 8-24-24): add implementation https://opentrons.atlassian.net/browse/RET-1373
19121911
push_out: Optional[float] = None,
19131912
) -> None:
19141913
"""

api/src/opentrons/protocol_api/core/engine/instrument.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import annotations
33

44
from typing import Optional, TYPE_CHECKING, cast, Union
5+
from opentrons.protocols.api_support.types import APIVersion
56

67
from opentrons.types import Location, Mount
78
from opentrons.hardware_control import SyncHardwareAPI
@@ -44,6 +45,9 @@
4445
from .protocol import ProtocolCore
4546

4647

48+
_DISPENSE_VOLUME_VALIDATION_ADDED_IN = APIVersion(2, 17)
49+
50+
4751
class InstrumentCore(AbstractInstrument[WellCore]):
4852
"""Instrument API core using a ProtocolEngine.
4953
@@ -180,6 +184,15 @@ def dispense(
180184
in_place: whether this is a in-place command.
181185
push_out: The amount to push the plunger below bottom position.
182186
"""
187+
if self._protocol_core.api_version < _DISPENSE_VOLUME_VALIDATION_ADDED_IN:
188+
# In older API versions, when you try to dispense more than you can,
189+
# it gets clamped.
190+
volume = min(volume, self.get_current_volume())
191+
else:
192+
# Newer API versions raise an error if you try to dispense more than
193+
# you can. Let the error come from Protocol Engine's validation.
194+
pass
195+
183196
if well_core is None:
184197
if not in_place:
185198
if isinstance(location, (TrashBin, WasteChute)):
@@ -733,7 +746,6 @@ def configure_nozzle_layout(
733746
primary_nozzle: Optional[str],
734747
front_right_nozzle: Optional[str],
735748
) -> None:
736-
737749
if style == NozzleLayout.COLUMN:
738750
configuration_model: NozzleLayoutConfigurationType = (
739751
ColumnNozzleLayoutConfiguration(

api/src/opentrons/protocol_api/instrument_context.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,9 @@ def dispense( # noqa: C901
346346
.. versionchanged:: 2.15
347347
Added the ``push_out`` parameter.
348348
349+
.. versionchanged:: 2.17
350+
Now raises an exception if you try to dispense more than is available.
351+
Previously, it would silently clamp.
349352
"""
350353
if self.api_version < APIVersion(2, 15) and push_out:
351354
raise APIVersionError(

api/src/opentrons/protocol_engine/commands/aspirate.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from .pipetting_common import (
77
PipetteIdMixin,
8-
VolumeMixin,
8+
AspirateVolumeMixin,
99
FlowRateMixin,
1010
WellLocationMixin,
1111
BaseLiquidHandlingResult,
@@ -25,7 +25,9 @@
2525
AspirateCommandType = Literal["aspirate"]
2626

2727

28-
class AspirateParams(PipetteIdMixin, VolumeMixin, FlowRateMixin, WellLocationMixin):
28+
class AspirateParams(
29+
PipetteIdMixin, AspirateVolumeMixin, FlowRateMixin, WellLocationMixin
30+
):
2931
"""Parameters required to aspirate from a specific well."""
3032

3133
pass

api/src/opentrons/protocol_engine/commands/aspirate_in_place.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from .pipetting_common import (
1010
PipetteIdMixin,
11-
VolumeMixin,
11+
AspirateVolumeMixin,
1212
FlowRateMixin,
1313
BaseLiquidHandlingResult,
1414
)
@@ -23,7 +23,7 @@
2323
AspirateInPlaceCommandType = Literal["aspirateInPlace"]
2424

2525

26-
class AspirateInPlaceParams(PipetteIdMixin, VolumeMixin, FlowRateMixin):
26+
class AspirateInPlaceParams(PipetteIdMixin, AspirateVolumeMixin, FlowRateMixin):
2727
"""Payload required to aspirate in place."""
2828

2929
pass

api/src/opentrons/protocol_engine/commands/configure_for_volume.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
"""Configure for volume command request, result, and implementation models."""
22
from __future__ import annotations
3-
from pydantic import BaseModel
3+
from pydantic import BaseModel, Field
44
from typing import TYPE_CHECKING, Optional, Type, Tuple
55
from typing_extensions import Literal
66

7-
from .pipetting_common import (
8-
PipetteIdMixin,
9-
VolumeMixin,
10-
)
7+
from .pipetting_common import PipetteIdMixin
118
from .command import (
129
AbstractCommandWithPrivateResultImpl,
1310
BaseCommand,
@@ -22,10 +19,15 @@
2219
ConfigureForVolumeCommandType = Literal["configureForVolume"]
2320

2421

25-
class ConfigureForVolumeParams(PipetteIdMixin, VolumeMixin):
22+
class ConfigureForVolumeParams(PipetteIdMixin):
2623
"""Parameters required to configure volume for a specific pipette."""
2724

28-
pass
25+
volume: float = Field(
26+
...,
27+
description="Amount of liquid in uL. Must be at least 0 and no greater "
28+
"than a pipette-specific maximum volume.",
29+
ge=0,
30+
)
2931

3032

3133
class ConfigureForVolumePrivateResult(PipetteConfigUpdateResultMixin):

0 commit comments

Comments
 (0)