Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\utils\_bounds.py: 85%
34 statements
« prev ^ index » next coverage.py v7.6.4, created at 2025-02-05 10:12 +0100
« prev ^ index » next coverage.py v7.6.4, created at 2025-02-05 10:12 +0100
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
52 elif lb is None and ub is None:
53 raise ValueError("At least one of the bounds must be provided")
55 self.l = lb
56 self.u = ub
57 self.half_distance = self._half_distance()
58 self.center = self._center()
60 def residual(self, x: npt.NDArray):
61 """
62 Calculate the residuals between the input vector `x` and the bounds
63 `l` and `u`.
65 Parameters
66 ----------
67 x : npt.NDArray
68 Input vector for which the residuals are to be calculated.
70 Returns
71 -------
72 tuple of npt.NDArray
73 A tuple containing two arrays:
74 - The residuals between `x` and the lower bound `l`.
75 - The residuals between the upper bound `u` and `x`.
76 """
77 return x - self.l, self.u - x
79 def single_residual(self, x: float, i: int):
80 """
81 Calculate the residuals for a given value for a specific constraint
82 with respect to the lower and upper bounds.
84 Parameters
85 ----------
86 x : float
87 The value for which the residuals are calculated.
88 i : int
89 The index of the bounds to use.
91 Returns
92 -------
93 tuple of float
94 A tuple containing the residuals (x - lower_bound, upper_bound - x).
95 """
96 return x - self.l[i], self.u[i] - x
98 def indexed_residual(self, x: npt.NDArray, i: List[int] | npt.NDArray):
99 """
100 Compute the residuals for the given indices.
102 Parameters
103 ----------
104 x : npt.NDArray
105 The input array.
106 i : List[int] or npt.NDArray
107 The indices for which to compute the residuals.
109 Returns
110 -------
111 tuple of npt.NDArray
112 A tuple containing two arrays:
113 - The residuals of `x` with respect to the lower bounds.
114 - The residuals of `x` with respect to the upper bounds.
115 """
116 return x - self.l[i], self.u[i] - x
118 def _center(self):
119 """
120 Calculate the center point between the lower bound (self.l) and the
121 upper bound (self.u).
123 Returns
124 -------
125 float
126 The midpoint value between self.l and self.u.
127 """
128 return (self.l + self.u) / 2
130 def _half_distance(self):
131 """
132 Calculate half the distance between the upper and lower bounds.
134 Returns
135 -------
136 float
137 Half the distance between the upper bound (self.u) and the lower bound (self.l).
138 """
139 return (self.u - self.l) / 2
141 def project(self, x: npt.NDArray):
142 """
143 Project the input array `x` onto the bounds defined by `self.l` and
144 `self.u`.
146 Parameters
147 ----------
148 x : npt.NDArray
149 Input array to be projected.
151 Returns
152 -------
153 npt.NDArray
154 The projected array where each element is clipped to be within the bounds
155 defined by `self.l` and `self.u`.
156 """
157 xp = cp if isinstance(x, cp.ndarray) else np
158 return xp.minimum(self.u, xp.maximum(self.l, x))