diff --git a/linerate/solver.py b/linerate/solver.py index aa0561f..1581c35 100644 --- a/linerate/solver.py +++ b/linerate/solver.py @@ -1,5 +1,5 @@ from functools import partial -from typing import Callable, Optional +from typing import Callable import numpy as np @@ -13,7 +13,6 @@ def bisect( xmin: FloatOrFloatArray, xmax: FloatOrFloatArray, tolerance: float, - invalid_value: Optional[float] = None, ) -> FloatOrFloatArray: r"""Compute the roots of a function using a vectorized bisection method. @@ -32,9 +31,6 @@ def bisect( bounded within an interval of size :math:`\Delta x` or less. The bisection method will run for :math:`\left\lceil\frac{x_\max - x_\min}{\Delta x}\right\rceil` iterations. - invalid_value: - If provided, then the this value is used whenever - :math:`\text{sign}(f(\mathbf{x}_\min)) = \text{sign}(f(\mathbf{x}_\max))`. Returns ------- @@ -43,7 +39,7 @@ def bisect( there is a root :math:`x_i \in [\tilde{x}_i - 0.5 \Delta x, \tilde{x}_i + 0.5 \Delta x]` so :math:`f_i(x_i) = 0`. """ - if not np.isfinite(xmin) or not np.isfinite(xmax): + if not np.all(np.isfinite(xmin)) or not np.all(np.isfinite(xmax)): raise ValueError("xmin and xmax must be finite.") interval = np.max(np.abs(xmax - xmin)) @@ -51,12 +47,10 @@ def bisect( f_right = f(xmax) invalid_mask = np.sign(f_left) == np.sign(f_right) - if np.any(invalid_mask) and invalid_value is None: + if np.any(invalid_mask): raise ValueError( "f(xmin) and f(xmax) have the same sign. Consider increasing the search interval." ) - elif isinstance(invalid_mask, bool) and invalid_mask: - return invalid_value # type: ignore while interval > tolerance: xmid = 0.5 * (xmax + xmin) @@ -69,7 +63,7 @@ def bisect( f_left = np.where(mask, f_mid, f_left) f_right = np.where(mask, f_right, f_mid) - out = np.where(invalid_mask, invalid_value, 0.5 * (xmax + xmin)) # type: ignore + out = 0.5 * (xmax + xmin) return out @@ -139,7 +133,7 @@ def compute_conductor_ampacity( :math:`\Delta I~\left[\text{A}\right]`. The numerical accuracy of the ampacity. The bisection iterations will stop once the numerical ampacity uncertainty is below :math:`\Delta I`. The bisection method will run for - :math:`\left\lceil\frac{I_\text{min} - I_\text{min}}{\Delta I}\right\rceil` iterations. + :math:`\left\lceil\frac{I_\text{max} - I_\text{min}}{\Delta I}\right\rceil` iterations. Returns ------- @@ -148,4 +142,4 @@ def compute_conductor_ampacity( """ f = partial(heat_balance, max_conductor_temperature) - return bisect(f, min_ampacity, max_ampacity, tolerance, invalid_value=0) + return bisect(f, min_ampacity, max_ampacity, tolerance) diff --git a/pyproject.toml b/pyproject.toml index ef877ad..54e4e73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "linerate" -version = "0.0.7-post.3+62231f4" +version = "1.0.0" description = "Library for computing line ampacity ratings for overhead lines" authors = ["Statnett Datascience ", "Yngve Mardal Moe "] repository = "https://github.com/statnett/linerate.git" diff --git a/tests/test_solver.py b/tests/test_solver.py index 37e20fd..1720ef3 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -1,3 +1,4 @@ +import numpy as np import pytest import linerate.solver as solver @@ -31,17 +32,58 @@ def heat_balance(conductor_temperature, current): assert conductor_temperature == pytest.approx(9000, rel=1e-7) -def test_compute_conductor_temperature_caps_ampacity_at_zero(): - def heat_balance(conductor_temperature, current): +def test_bisect_raises_value_error(): + def heat_balance(current): A = current - T = conductor_temperature - return (A + 100 * T) * (current + 200 * T) + T = 90 + return (A + 100 * T) * (A + 100 * T) - conductor_temperature = solver.compute_conductor_ampacity( + with pytest.raises(ValueError): + solver.bisect( + heat_balance, + xmin=0, + xmax=10_000, + tolerance=1e-8, + ) + + +def test_bisect_handles_function_returning_array_happy_path(): + def heat_balance(currents: np.array): + A = currents + T = 90 + res = (A - 100 * T) * (currents + 100 * T) + return res + + solution = solver.bisect( heat_balance, - max_conductor_temperature=90, - min_ampacity=0, - max_ampacity=10_000, + xmin=np.array([0, 0]), + xmax=np.array([10_000, 10_000]), tolerance=1e-8, ) - assert conductor_temperature == pytest.approx(0, rel=1e-7) + np.testing.assert_array_almost_equal(solution, [9_000, 9_000], decimal=8) + + +def test_bisect_raises_valueerror_when_same_sign_for_array_input(): + def heat_balance(currents: np.array): + A = currents + T = 90 + res = (A - 100 * T) * (currents + 100 * T) + return res + + with pytest.raises(ValueError): + solver.bisect( + heat_balance, + xmin=np.array([0, 0]), + xmax=np.array([10_000, 8_000]), + tolerance=1e-8, + ) + + +def test_bisect_raises_valueerror_when_infinite_in_array_input(): + with pytest.raises(ValueError): + solver.bisect( + lambda x: x, + xmin=np.array([-np.inf, 0]), + xmax=np.array([10_000, 10_000]), + tolerance=1e-8, + )