Skip to content

Commit b4831cf

Browse files
feat(api): add new user-facing helper functions for static liquid tracking (#17360)
1 parent 3bab4df commit b4831cf

File tree

16 files changed

+390
-20
lines changed

16 files changed

+390
-20
lines changed

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

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import (
6-
Optional,
7-
TYPE_CHECKING,
8-
cast,
9-
Union,
10-
List,
11-
Tuple,
12-
NamedTuple,
13-
)
5+
from typing import Optional, TYPE_CHECKING, cast, Union, List, Tuple, NamedTuple
146
from opentrons.types import Location, Mount, NozzleConfigurationType, NozzleMapInterface
157
from opentrons.hardware_control import SyncHardwareAPI
168
from opentrons.hardware_control.dev_types import PipetteDict
@@ -1369,6 +1361,27 @@ def detect_liquid_presence(self, well_core: WellCore, loc: Location) -> bool:
13691361

13701362
return result.z_position is not None
13711363

1364+
def get_minimum_liquid_sense_height(self) -> float:
1365+
attached_tip = self._engine_client.state.pipettes.get_attached_tip(
1366+
self._pipette_id
1367+
)
1368+
if attached_tip:
1369+
tip_volume = attached_tip.volume
1370+
else:
1371+
raise TipNotAttachedError(
1372+
"Need to have a tip attached for liquid-sense operations."
1373+
)
1374+
lld_settings = self._engine_client.state.pipettes.get_pipette_lld_settings(
1375+
pipette_id=self.pipette_id
1376+
)
1377+
if lld_settings:
1378+
lld_min_height_for_tip_attached = lld_settings[f"t{tip_volume}"][
1379+
"minHeight"
1380+
]
1381+
return lld_min_height_for_tip_attached
1382+
else:
1383+
raise ValueError("liquid-level detection settings not found.")
1384+
13721385
def liquid_probe_with_recovery(self, well_core: WellCore, loc: Location) -> None:
13731386
labware_id = well_core.labware_id
13741387
well_name = well_core.get_name()

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,35 @@ def from_center_cartesian(self, x: float, y: float, z: float) -> Point:
155155
y_ratio=y,
156156
z_ratio=z,
157157
)
158+
159+
def estimate_liquid_height_after_pipetting(
160+
self,
161+
operation_volume: float,
162+
) -> float:
163+
"""Return an estimate of liquid height after pipetting without raising an error."""
164+
labware_id = self.labware_id
165+
well_name = self._name
166+
starting_liquid_height = self.current_liquid_height()
167+
projected_final_height = self._engine_client.state.geometry.get_well_height_after_liquid_handling_no_error(
168+
labware_id=labware_id,
169+
well_name=well_name,
170+
initial_height=starting_liquid_height,
171+
volume=operation_volume,
172+
)
173+
return projected_final_height
174+
175+
def current_liquid_height(self) -> float:
176+
"""Return the current liquid height within a well."""
177+
labware_id = self.labware_id
178+
well_name = self._name
179+
return self._engine_client.state.geometry.get_meniscus_height(
180+
labware_id=labware_id, well_name=well_name
181+
)
182+
183+
def get_liquid_volume(self) -> float:
184+
"""Return the current volume in a well."""
185+
labware_id = self.labware_id
186+
well_name = self._name
187+
return self._engine_client.state.geometry.get_current_well_volume(
188+
labware_id=labware_id, well_name=well_name
189+
)

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ def get_current_volume(self) -> float:
225225
def get_available_volume(self) -> float:
226226
...
227227

228+
@abstractmethod
229+
def get_minimum_liquid_sense_height(self) -> float:
230+
...
231+
228232
@abstractmethod
229233
def get_hardware_state(self) -> PipetteDict:
230234
"""Get the current state of the pipette hardware as a dictionary."""

api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,3 +643,14 @@ def _pressure_supported_by_pipette(self) -> bool:
643643
def nozzle_configuration_valid_for_lld(self) -> bool:
644644
"""Check if the nozzle configuration currently supports LLD."""
645645
return False
646+
647+
def get_minimum_liquid_sense_height(self) -> float:
648+
return 0.0
649+
650+
def estimate_liquid_height(
651+
self,
652+
well_core: LegacyWellCore,
653+
starting_liquid_height: float,
654+
operation_volume: float,
655+
) -> float:
656+
return 0.0

api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ def from_center_cartesian(self, x: float, y: float, z: float) -> Point:
118118
"""Gets point in deck coordinates based on percentage of the radius of each axis."""
119119
return self._geometry.from_center_cartesian(x, y, z)
120120

121+
def estimate_liquid_height_after_pipetting(
122+
self,
123+
operation_volume: float,
124+
) -> float:
125+
"""Estimate what the liquid height will be after pipetting, without raising an error."""
126+
return 0.0
127+
128+
def current_liquid_height(self) -> float:
129+
"""Get the current liquid height."""
130+
return 0.0
131+
132+
def get_liquid_volume(self) -> float:
133+
"""Get the current well volume."""
134+
return 0.0
135+
121136
# TODO(mc, 2022-10-28): is this used and/or necessary?
122137
def __repr__(self) -> str:
123138
"""Use the well's display name as its repr."""

api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,3 +561,15 @@ def _pressure_supported_by_pipette(self) -> bool:
561561
def nozzle_configuration_valid_for_lld(self) -> bool:
562562
"""Check if the nozzle configuration currently supports LLD."""
563563
return False
564+
565+
def get_minimum_liquid_sense_height(self) -> float:
566+
return 0.0
567+
568+
def estimate_liquid_height(
569+
self,
570+
well_core: LegacyWellCore,
571+
starting_liquid_height: float,
572+
operation_volume: float,
573+
) -> float:
574+
"""This will never be called because it was added in API 2.21."""
575+
assert False, "estimate_liquid_height only supported in API 2.21 & later"

api/src/opentrons/protocol_api/core/well.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,20 @@ def load_liquid(
8383
def from_center_cartesian(self, x: float, y: float, z: float) -> Point:
8484
"""Gets point in deck coordinates based on percentage of the radius of each axis."""
8585

86+
@abstractmethod
87+
def estimate_liquid_height_after_pipetting(
88+
self,
89+
operation_volume: float,
90+
) -> float:
91+
"""Estimate what the liquid height will be after pipetting, without raising an error."""
92+
93+
@abstractmethod
94+
def current_liquid_height(self) -> float:
95+
"""Get the current liquid height."""
96+
97+
@abstractmethod
98+
def get_liquid_volume(self) -> float:
99+
"""Get the current volume within a well."""
100+
86101

87102
WellCoreType = TypeVar("WellCoreType", bound=AbstractWellCore)

api/src/opentrons/protocol_api/instrument_context.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ def default_speed(self) -> float:
165165
def default_speed(self, speed: float) -> None:
166166
self._core.set_default_speed(speed)
167167

168+
@requires_version(2, 21)
169+
def get_minimum_liquid_sense_height(self) -> float:
170+
"""Get the minimum allowed height for liquid-level detection."""
171+
return self._core.get_minimum_liquid_sense_height()
172+
168173
@requires_version(2, 0)
169174
def aspirate(
170175
self,

api/src/opentrons/protocol_api/labware.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
UnsupportedAPIError,
3737
)
3838

39-
4039
# TODO(mc, 2022-09-02): re-exports provided for backwards compatibility
4140
# remove when their usage is no longer needed
4241
from opentrons.protocols.labware import ( # noqa: F401
@@ -49,7 +48,10 @@
4948
from ._liquid import Liquid
5049
from ._types import OffDeckType
5150
from .core import well_grid
52-
from .core.engine import ENGINE_CORE_API_VERSION, SET_OFFSET_RESTORED_API_VERSION
51+
from .core.engine import (
52+
ENGINE_CORE_API_VERSION,
53+
SET_OFFSET_RESTORED_API_VERSION,
54+
)
5355
from .core.labware import AbstractLabware
5456
from .core.module import AbstractModuleCore
5557
from .core.core_map import LoadedCoreMap
@@ -301,6 +303,32 @@ def load_liquid(self, liquid: Liquid, volume: float) -> None:
301303
volume=volume,
302304
)
303305

306+
@requires_version(2, 21)
307+
def current_liquid_height(self) -> float:
308+
"""Get the current liquid height in a well."""
309+
return self._core.current_liquid_height()
310+
311+
@requires_version(2, 21)
312+
def current_liquid_volume(self) -> float:
313+
"""Get the current liquid volume in a well."""
314+
return self._core.get_liquid_volume()
315+
316+
@requires_version(2, 21)
317+
def estimate_liquid_height_after_pipetting(self, operation_volume: float) -> float:
318+
"""Check the height of the liquid within a well.
319+
320+
:returns: The height, in mm, of the liquid from the deck.
321+
322+
:meta private:
323+
324+
This is intended for Opentrons internal use only and is not a guaranteed API.
325+
"""
326+
327+
projected_final_height = self._core.estimate_liquid_height_after_pipetting(
328+
operation_volume=operation_volume,
329+
)
330+
return projected_final_height
331+
304332
def _from_center_cartesian(self, x: float, y: float, z: float) -> Point:
305333
"""
306334
Private version of from_center_cartesian. Present only for backward

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn:
152152
labware_id=labware_id,
153153
well_name=well_name,
154154
)
155-
156155
move_result = await move_to_well(
157156
movement=self._movement,
158157
model_utils=self._model_utils,

0 commit comments

Comments
 (0)