Skip to content

Commit

Permalink
feat: remove invalid_value functionality in bisect (#13)
Browse files Browse the repository at this point in the history
* feat: remove invalid_value functionality in bisect

BREAKING CHANGE: remove invalid_value parameter and functionality in bisect method, instead raise ValueError. By extension, changes behaviour of compute_conductor_ampacity which had invaluid_value set to 0 when bisect failed.

* test that bisect raises ValueError

* fix bisect function for array inputs, more tests

* Update tests/test_solver.py

* Update tests/test_solver.py

---------

Co-authored-by: Amund Faller Råheim <[email protected]>
Co-authored-by: Gunnhild Svandal Presthus <[email protected]>
  • Loading branch information
3 people authored Apr 5, 2024
1 parent c90e78c commit 617db5c
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 22 deletions.
18 changes: 6 additions & 12 deletions linerate/solver.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from functools import partial
from typing import Callable, Optional
from typing import Callable

import numpy as np

Expand All @@ -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.
Expand All @@ -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
-------
Expand All @@ -43,20 +39,18 @@ 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))

f_left = f(xmin)
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)
Expand All @@ -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


Expand Down Expand Up @@ -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
-------
Expand All @@ -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)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>", "Yngve Mardal Moe <[email protected]>"]
repository = "https://github.com/statnett/linerate.git"
Expand Down
60 changes: 51 additions & 9 deletions tests/test_solver.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import numpy as np
import pytest

import linerate.solver as solver
Expand Down Expand Up @@ -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,
)

0 comments on commit 617db5c

Please sign in to comment.