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

1from typing import List 

2 

3import numpy as np 

4import numpy.typing as npt 

5 

6try: 

7 import cupy as cp 

8 

9 NO_GPU = False 

10except ImportError: 

11 NO_GPU = True 

12 cp = np 

13 

14 

15class Bounds: 

16 """ 

17 A class to help with hyperslab calculations. 

18 

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. 

25 

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. 

36 

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 """ 

43 

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") 

53 

54 self.l = lb 

55 self.u = ub 

56 self.half_distance = self._half_distance() 

57 self.center = self._center() 

58 

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`. 

63 

64 Parameters 

65 ---------- 

66 x : npt.NDArray 

67 Input vector for which the residuals are to be calculated. 

68 

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 

77 

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. 

82 

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. 

89 

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 

96 

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. 

102 

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. 

109 

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 

118 

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). 

123 

124 Returns 

125 ------- 

126 npt.NDArray 

127 The midpoint value between self.l and self.u. 

128 """ 

129 return (self.l + self.u) / 2 

130 

131 def _half_distance(self) -> npt.NDArray: 

132 """ 

133 Calculate half the distance between the upper and lower bounds. 

134 

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 

141 

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`. 

146 

147 Parameters 

148 ---------- 

149 x : npt.NDArray 

150 Input array to be projected. 

151 

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))