Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\utils\_bounds.py: 85%

1 from typing import List

2

3 import numpy as np

4 import numpy.typing as npt

5

6 try:

7 import cupy as cp

8

9 NO_GPU = False

10 except ImportError:

11 NO_GPU = True

12 cp = np

13

14

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