Skip to content

Commit e4fa84f

Browse files
feat(api): don't make UserDefinedVolumes require (max_height, max_volume) (#19066)
1 parent e273525 commit e4fa84f

File tree

5 files changed

+80
-29
lines changed

5 files changed

+80
-29
lines changed

api/src/opentrons/protocol_engine/errors/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
LiquidHeightUnknownError,
7979
LiquidVolumeUnknownError,
8080
IncompleteLabwareDefinitionError,
81+
InvalidUserDefinedVolumesError,
8182
IncompleteWellDefinitionError,
8283
OperationLocationNotInWellError,
8384
InvalidDispenseVolumeError,
@@ -180,6 +181,7 @@
180181
"LiquidHeightUnknownError",
181182
"LiquidVolumeUnknownError",
182183
"IncompleteLabwareDefinitionError",
184+
"InvalidUserDefinedVolumesError",
183185
"IncompleteWellDefinitionError",
184186
"OperationLocationNotInWellError",
185187
"InvalidDispenseVolumeError",

api/src/opentrons/protocol_engine/errors/exceptions.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,6 +1196,19 @@ def __init__(
11961196
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
11971197

11981198

1199+
class InvalidUserDefinedVolumesError(ProtocolEngineError):
1200+
"""Raised when a UserDefinedVolumes type InnerLabwareDefinition is invalid."""
1201+
1202+
def __init__(
1203+
self,
1204+
message: Optional[str] = None,
1205+
details: Optional[Dict[str, Any]] = None,
1206+
wrapping: Optional[Sequence[EnumeratedError]] = None,
1207+
) -> None:
1208+
"""Build an InvalidUserDefinedVolumesError."""
1209+
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
1210+
1211+
11991212
class IncompleteWellDefinitionError(ProtocolEngineError):
12001213
"""Raised when a well definition lacks a geometryDefinitionId."""
12011214

api/src/opentrons/protocol_engine/state/inner_well_math_utils.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from numpy import pi, iscomplex, roots, real
44
from math import isclose
55

6-
from ..errors.exceptions import InvalidLiquidHeightFound
6+
from ..errors.exceptions import InvalidLiquidHeightFound, InvalidUserDefinedVolumesError
77

88
from opentrons.protocol_engine.types.liquid_level_detection import (
99
LiquidTrackingType,
@@ -363,10 +363,12 @@ def _find_volume_in_partial_frustum(
363363
def _linear_interpolation(
364364
interpolating_from: List[float], to_interpolate: List[float], target_val: float
365365
) -> float:
366-
assert len(interpolating_from) == len(to_interpolate), "Invalid height/volume data"
367-
assert (
368-
interpolating_from[0] == to_interpolate[0] == 0.0
369-
), "height and volume data must start with 0.0"
366+
if len(interpolating_from) != len(to_interpolate):
367+
raise InvalidUserDefinedVolumesError(
368+
"Height and volume data have unequal sizes"
369+
)
370+
if not (interpolating_from[0] == to_interpolate[0] == 0.0):
371+
raise ValueError("linear interpolation datasets must start with 0.0")
370372

371373
if target_val == 0.0:
372374
return 0.0
@@ -395,7 +397,7 @@ def find_volume_user_defined_volumes(
395397
max_height = sorted_volume_map[-1].height
396398
if target_height < 0 or target_height > max_height:
397399
raise InvalidLiquidHeightFound(
398-
f"Invalid target height {target_height} mm; max well height is {max_height} mm."
400+
f"Invalid target height {target_height} mm; greatest well height in InnerLabwareGeometry is {max_height} mm."
399401
)
400402
volumes = [0.0]
401403
heights = [0.0]
@@ -426,7 +428,7 @@ def find_height_user_defined_volumes(
426428
max_volume = sorted_volume_map[-1].volume
427429
if target_volume < 0 or target_volume > max_volume:
428430
raise InvalidLiquidHeightFound(
429-
f"Invalid target volume {target_volume} mm; max well volume is {max_volume} uL."
431+
f"Invalid target volume {target_volume}µL; greatest well volume in InnerLabwareGeometry is {max_volume}µL."
430432
)
431433

432434
volumes = [0.0]
@@ -516,7 +518,7 @@ def _find_height_in_partial_frustum(
516518
# also this code should never be reached bc an invalid target volume should be changed
517519
# by find_height_at_well_volume
518520
raise InvalidLiquidHeightFound(
519-
f"Target volume {target_volume} uL exceeds the well volume {total_well_volume} uL."
521+
f"Target volume {target_volume}µL exceeds the well volume {total_well_volume}µL."
520522
)
521523

522524

api/tests/opentrons/protocols/geometry/test_inner_well_math_utils.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,61 @@ def test_volume_at_section_boundary_heights(well: List[Any]) -> None:
404404
assert isclose(cast(float, top_ul), tot_ul)
405405

406406

407+
def test_user_volumes_raises_error_for_invalid_target(
408+
user_defined_volumes_params: Dict[str, Any]
409+
) -> None:
410+
"""Test that UserDefinedVolumes calculations reject target inputs that are not allowed."""
411+
user_defined_volumes_obj = user_defined_volumes_params["obj"]
412+
max_defined_height = user_defined_volumes_obj.heightToVolumeMap[-1].height
413+
max_defined_volume = user_defined_volumes_obj.heightToVolumeMap[-1].volume
414+
min_defined_height = user_defined_volumes_obj.heightToVolumeMap[0].height
415+
min_defined_volume = user_defined_volumes_obj.heightToVolumeMap[0].volume
416+
417+
# less than 0 should raise an error
418+
with pytest.raises(InvalidLiquidHeightFound):
419+
find_volume_user_defined_volumes(
420+
target_height=-0.01, well_geometry=user_defined_volumes_obj
421+
)
422+
with pytest.raises(InvalidLiquidHeightFound):
423+
find_height_user_defined_volumes(
424+
target_volume=-0.01, well_geometry=user_defined_volumes_obj
425+
)
426+
427+
# between 0 and min defined height should be ok
428+
vol = find_volume_user_defined_volumes(
429+
target_height=(min_defined_height / 2), well_geometry=user_defined_volumes_obj
430+
)
431+
assert vol is not None
432+
height = find_height_user_defined_volumes(
433+
target_volume=(min_defined_volume / 2), well_geometry=user_defined_volumes_obj
434+
)
435+
assert height is not None
436+
437+
# betwen min defined height and max defined height should be ok
438+
vol = find_volume_user_defined_volumes(
439+
target_height=((min_defined_height + max_defined_height) / 2),
440+
well_geometry=user_defined_volumes_obj,
441+
)
442+
assert vol is not None
443+
height = find_height_user_defined_volumes(
444+
target_volume=((min_defined_volume + max_defined_volume) / 2),
445+
well_geometry=user_defined_volumes_obj,
446+
)
447+
assert height is not None
448+
449+
# any greater than max defined height should cause an error
450+
with pytest.raises(InvalidLiquidHeightFound):
451+
find_volume_user_defined_volumes(
452+
target_height=max_defined_height + 0.01,
453+
well_geometry=user_defined_volumes_obj,
454+
)
455+
with pytest.raises(InvalidLiquidHeightFound):
456+
find_height_user_defined_volumes(
457+
target_volume=max_defined_volume + 0.01,
458+
well_geometry=user_defined_volumes_obj,
459+
)
460+
461+
407462
def test_get_user_volumes(user_defined_volumes_params: Dict[str, Any]) -> None:
408463
"""Test linear interpolation math for user-defined volumes."""
409464
user_defined_volumes_obj = user_defined_volumes_params["obj"]

shared-data/js/__tests__/labwareDefSchemaV2.test.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -285,21 +285,6 @@ const checkGeometryDefinitions = (labwareDef: LabwareDefinition2): void => {
285285
}
286286
})
287287

288-
test('the last entry for UserDefinedVolumes should be the max volume of the well at its depth', () => {
289-
for (const well of Object.values(labwareDef.wells)) {
290-
const wellGeometryId = well.geometryDefinitionId
291-
if (wellGeometryId === undefined) return
292-
const wellDepth = well.depth
293-
const innerGeometryObject =
294-
labwareDef.innerLabwareGeometry?.[wellGeometryId]
295-
if (innerGeometryObject === undefined) return
296-
if (!isUserDefinedVolumes(innerGeometryObject)) return
297-
const pairingList = innerGeometryObject.heightToVolumeMap
298-
const firstEntry = pairingList[0]
299-
expect(firstEntry.height).toStrictEqual(wellDepth)
300-
}
301-
})
302-
303288
test('the bottom of a well geometry should be at height 0', () => {
304289
for (const geometry of Object.values(
305290
labwareDef.innerLabwareGeometry ?? {}
@@ -341,12 +326,6 @@ const checkGeometryDefinitions = (labwareDef: LabwareDefinition2): void => {
341326
return 'sections' in def
342327
}
343328

344-
function isUserDefinedVolumes(
345-
def: InnerWellGeometry | UserDefinedVolumes
346-
): def is UserDefinedVolumes {
347-
return 'heightToVolumeMap' in def
348-
}
349-
350329
test("a well's depth should equal the height of its geometry", () => {
351330
for (const well of Object.values(labwareDef.wells)) {
352331
const wellGeometryId = well.geometryDefinitionId

0 commit comments

Comments
 (0)