Coverage for suppy\utils\_bounds.py: 85%
34 statements
« prev ^ index » next coverage.py v7.6.4, created at 2026-05-08 13:56 +0200
« prev ^ index » next coverage.py v7.6.4, created at 2026-05-08 13:56 +0200
1from typing import List
3import numpy as np
4import numpy.typing as npt
6try:
7 import cupy as cp
9 NO_GPU = False
10except ImportError:
11 NO_GPU = True
12 cp = np
15class Bounds:
16 """
17 A class to help with hyperslab calculations.
19 Parameters
20 ----------
21 lb : None or array_like, optional
22 Lower bounds. If None, defaults to negative infinity if `ub` is provided.
23 ub : None or array_like, optional
24 Upper bounds. If None, defaults to positive infinity if `lb` is provided.
26 Attributes
27 ----------
28 l : array_like
29 Lower bounds.
30 u : array_like
31 Upper bounds.
32 half_distance : array_like
33 Half the distance between lower and upper bounds.
34 center : array_like
35 Center point between lower and upper bounds.
37 Raises
38 ------
39 ValueError
40 If the sizes of the lower and upper bounds do not match.
41 If any lower bound is greater than the corresponding upper bound.
42 """
44 def __init__(self, lb: None | npt.NDArray = None, ub: None | npt.NDArray = None):
45 # TODO: Rework validity check? Should be possible to just pass a scaler
46 # TODO: default values for lower and upper bounds and check
47 if lb is None and ub is not None:
48 lb = -np.inf
49 elif ub is None and lb is not None:
50 ub = np.inf
51 elif lb is None and ub is None:
52 raise ValueError("At least one of the bounds must be provided")
54 self.l = lb
55 self.u = ub
56 self.half_distance = self._half_distance()
57 self.center = self._center()
59 def residual(self, x: npt.NDArray) -> tuple[npt.NDArray, npt.NDArray]:
60 """
61 Calculate the residuals between the input vector `x` and the bounds
62 `l` and `u`.
64 Parameters
65 ----------
66 x : npt.NDArray
67 Input vector for which the residuals are to be calculated.
69 Returns
70 -------
71 tuple of npt.NDArray
72 A tuple containing two arrays:
73 - The residuals between `x` and the lower bound `l`.
74 - The residuals between the upper bound `u` and `x`.
75 """
76 return x - self.l, self.u - x
78 def single_residual(self, x: float, i: int) -> tuple[float, float]:
79 """
80 Calculate the residuals for a given value for a specific constraint
81 with respect to the lower and upper bounds.
83 Parameters
84 ----------
85 x : float
86 The value for which the residuals are calculated.
87 i : int
88 The index of the bounds to use.
90 Returns
91 -------
92 tuple of float
93 A tuple containing the residuals (x - lower_bound, upper_bound - x).
94 """
95 return x - self.l[i], self.u[i] - x
97 def indexed_residual(
98 self, x: npt.NDArray, i: List[int] | npt.NDArray
99 ) -> tuple[npt.NDArray, npt.NDArray]:
100 """
101 Compute the residuals for the given indices.
103 Parameters
104 ----------
105 x : npt.NDArray
106 The input array.
107 i : List[int] or npt.NDArray
108 The indices for which to compute the residuals.
110 Returns
111 -------
112 tuple of npt.NDArray
113 A tuple containing two arrays:
114 - The residuals of `x` with respect to the lower bounds.
115 - The residuals of `x` with respect to the upper bounds.
116 """
117 return x - self.l[i], self.u[i] - x
119 def _center(self) -> npt.NDArray:
120 """
121 Calculate the center point between the lower bound (self.l) and the
122 upper bound (self.u).
124 Returns
125 -------
126 npt.NDArray
127 The midpoint value between self.l and self.u.
128 """
129 return (self.l + self.u) / 2
131 def _half_distance(self) -> npt.NDArray:
132 """
133 Calculate half the distance between the upper and lower bounds.
135 Returns
136 -------
137 npt.NDArray
138 Half the distance between the upper bound (self.u) and the lower bound (self.l).
139 """
140 return (self.u - self.l) / 2
142 def project(self, x: npt.NDArray) -> npt.NDArray:
143 """
144 Project the input array `x` onto the bounds defined by `self.l` and
145 `self.u`.
147 Parameters
148 ----------
149 x : npt.NDArray
150 Input array to be projected.
152 Returns
153 -------
154 npt.NDArray
155 The projected array where each element is clipped to be within the bounds
156 defined by `self.l` and `self.u`.
157 """
158 xp = cp if isinstance(x, cp.ndarray) else np
159 return xp.minimum(self.u, xp.maximum(self.l, x))