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

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 

52 elif lb is None and ub is None: 

53 raise ValueError("At least one of the bounds must be provided") 

54 

55 self.l = lb 

56 self.u = ub 

57 self.half_distance = self._half_distance() 

58 self.center = self._center() 

59 

60 def residual(self, x: npt.NDArray): 

61 """ 

62 Calculate the residuals between the input vector `x` and the bounds 

63 `l` and `u`. 

64 

65 Parameters 

66 ---------- 

67 x : npt.NDArray 

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

69 

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 

78 

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. 

83 

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. 

90 

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 

97 

98 def indexed_residual(self, x: npt.NDArray, i: List[int] | npt.NDArray): 

99 """ 

100 Compute the residuals for the given indices. 

101 

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. 

108 

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 

117 

118 def _center(self): 

119 """ 

120 Calculate the center point between the lower bound (self.l) and the 

121 upper bound (self.u). 

122 

123 Returns 

124 ------- 

125 float 

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

127 """ 

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

129 

130 def _half_distance(self): 

131 """ 

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

133 

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 

140 

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

145 

146 Parameters 

147 ---------- 

148 x : npt.NDArray 

149 Input array to be projected. 

150 

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