Files
EOS/tests/test_devicesprofilecurve.py
T

93 lines
3.6 KiB
Python
Raw Normal View History

2026-11-10 17:56:13 +00:00
"""Tests for the one-dimensional curves shared by the device profiles."""
import numpy as np
import pytest
from akkudoktoreos.devices.profile.profilecurve import (
CurveInterpolation,
OutOfRangeBehavior,
PrecomputedCurve,
reference_curve_lookup,
validate_curve_points,
)
X = (0.0, 0.2, 0.5, 1.0)
Y = (0.80, 0.95, 0.97, 0.96)
class TestValidateCurvePoints:
def test_accepts_increasing_points(self):
validate_curve_points(X, Y, "load_fraction", "efficiency")
@pytest.mark.parametrize("x", [(0.0, 0.0, 1.0), (0.0, 0.5, 0.4)])
def test_rejects_not_strictly_increasing(self, x):
with pytest.raises(ValueError, match="load_fraction breakpoints must be strictly"):
validate_curve_points(x, (1.0, 1.0, 1.0), "load_fraction", "efficiency")
def test_rejects_mismatched_lengths(self):
with pytest.raises(ValueError, match="same number of values"):
validate_curve_points(X, Y[:-1], "load_fraction", "efficiency")
class TestPrecomputedCurve:
@pytest.mark.parametrize("interpolation", list(CurveInterpolation))
@pytest.mark.parametrize("behavior", list(OutOfRangeBehavior))
def test_lookup_matches_reference(self, interpolation, behavior):
curve = PrecomputedCurve.from_points(X, Y, interpolation, behavior)
queries = [-0.5, 0.0, 0.1, 0.2, 0.35, 0.5, 0.75, 1.0, 1.5, 0.6]
for query in queries:
expected = reference_curve_lookup(X, Y, interpolation, behavior, query)
actual = curve.lookup(query)
if expected is None:
assert actual is None
else:
assert actual == pytest.approx(expected)
def test_linear_interpolation(self):
curve = PrecomputedCurve.from_points(X, Y)
assert curve.lookup(0.1) == pytest.approx(0.875)
assert curve.lookup(0.75) == pytest.approx(0.965)
def test_clamps_by_default(self):
curve = PrecomputedCurve.from_points(X, Y)
assert curve.lookup(-1.0) == pytest.approx(0.80)
assert curve.lookup(2.0) == pytest.approx(0.96)
def test_nearest_tie_takes_lower_breakpoint(self):
curve = PrecomputedCurve.from_points(X, Y, CurveInterpolation.NEAREST)
assert curve.lookup(0.1) == pytest.approx(0.80)
def test_none_interpolation_only_at_breakpoints(self):
curve = PrecomputedCurve.from_points(X, Y, CurveInterpolation.NONE)
assert curve.lookup(0.5) == pytest.approx(0.97)
assert curve.lookup(0.4) is None
def test_reject_out_of_range(self):
curve = PrecomputedCurve.from_points(X, Y, out_of_range_behavior=OutOfRangeBehavior.REJECT)
assert curve.lookup(-0.1) is None
assert curve.lookup(1.1) is None
def test_precomputes_coefficients(self):
curve = PrecomputedCurve.from_points(X, Y)
assert len(curve.slopes) == len(X) - 1
assert curve.midpoints == pytest.approx((0.1, 0.35, 0.75))
for i, slope in enumerate(curve.slopes):
assert slope * X[i] + curve.intercepts[i] == pytest.approx(Y[i])
def test_accepts_numpy_input(self):
curve = PrecomputedCurve.from_points(np.array(X), np.array(Y))
assert curve.x == X
@pytest.mark.parametrize(
"x, y",
[((0.0,), (1.0,)), ((0.0, 0.0), (1.0, 1.0)), ((0.0, 1.0), (1.0,))],
)
def test_rejects_invalid_points(self, x, y):
with pytest.raises(ValueError):
PrecomputedCurve.from_points(x, y)
def test_is_immutable(self):
curve = PrecomputedCurve.from_points(X, Y)
with pytest.raises(AttributeError):
curve.x = (0.0, 1.0) # type: ignore[misc]