Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\perturbations\_base.py: 93%

1"""Base class for perturbations applied to feasibility seeking algorithms."""

2 from abc import ABC, abstractmethod

3 from typing import Callable, List

4 import numpy as np

5 import numpy.typing as npt

6 from suppy.utils import FuncWrapper

7

8 try:

9 import cupy as cp

10

11 NO_GPU = False

12 except ImportError:

13 NO_GPU = True

14 cp = np

15

16

17 class Perturbation(ABC):

18 """

19 Abstract base class for perturbations applied to feasibility seeking

20 algorithms.

21 """

22

23 @abstractmethod

24 def perturbation_step(self, x: npt.NDArray) -> npt.NDArray:

25 """

26 Perform a perturbation step.

27

28 Parameters

29 ----------

30 x : npt.NDArray

31 The input array to be perturbed.

32

33 Returns

34 -------

35 npt.NDArray

36 The perturbed array.

37 """

38

39

40 class ObjectivePerturbation(Perturbation, ABC):

41 """

42 Base class for perturbations performed by decreasing an objective

43 function.

44

45 Parameters

46 ----------

47 func : Callable

48 The objective function to be perturbed.

49 func_args : List

50 The arguments to be passed to the objective function.

51 n_red : int, optional

52 The number of reduction steps to perform in one perturbation iteration (default is 1).

53

54 Attributes

55 ----------

56 func : FuncWrapper

57 A wrapped version of the objective function with its arguments.

58 n_red : int

59 The number of reduction steps to perform.

60 _k : int

61 Keeps track of the number of performed perturbations.

62 """

63

64 def __init__(self, func: Callable, func_args: List, n_red=1):

65 self.func = FuncWrapper(func, func_args)

66 self.n_red = n_red

67 self._k = 0 # keeps track of the number of performed perturbations

68

69 def perturbation_step(self, x: npt.NDArray) -> npt.NDArray:

70 """

71 Perform n_red perturbation steps on the input array.

72

73 Parameters

74 ----------

75 x : npt.NDArray

76 The input array to be perturbed.

77

78 Returns

79 -------

80 npt.NDArray

81 The perturbed array after applying the reduction steps.

82 """

83

84 self._k += 1

85 n = 0

86 while n < self.n_red:

87 x = self._function_reduction_step(x)

88 n += 1

89 return x

90

91 @abstractmethod

92 def _function_reduction_step(self, x: npt.NDArray) -> npt.NDArray:

93 """

94 Abstract method to perform that should implement the individual

95 function reduction steps on the input array.

96 Needs to be implemented by subclasses.

97

98 Parameters

99 ----------

100 x : npt.NDArray

101 Input array on which the reduction step is to be performed.

102

103 Returns

104 -------

105 npt.NDArray

106 The array after the reduction step has been applied.

107 """

108

109 def pre_step(self):

110 """

111 If required perform any form of step previous to each

112 perturbation(?) in each iteration.

113

114 This method is intended to be overridden by subclasses to implement

115 specific pre-step logic. By default, it does nothing.

116 """

117

118

119 class GradientPerturbation(ObjectivePerturbation, ABC):

120 """

121 A class for perturbations performed by decreasing an objective function

122 using the gradient.

123

124 Parameters

125 ----------

126 func : Callable

127 The objective function to be perturbed.

128 grad : Callable

129 The gradient function of the objective function.

130 func_args : List

131 The arguments to be passed to the objective function.

132 grad_args : List

133 The arguments to be passed to the gradient function.

134 n_red : int, optional

135 The reduction factor, by default 1.

136

137 Attributes

138 ----------

139 func : FuncWrapper

140 A wrapped version of the objective function with its arguments.

141 grad : FuncWrapper

142 A wrapped gradient function with its arguments.

143 n_red : int

144 The number of reduction steps to perform.

145 _k : int

146 Keeps track of the number of performed perturbations.

147 """

148

149 def __init__(self, func: Callable, grad: Callable, func_args: List, grad_args: List, n_red=1):

150 super().__init__(func, func_args, n_red)

151 self.grad = FuncWrapper(grad, grad_args)

152

153

154 class PowerSeriesGradientPerturbation(GradientPerturbation):

155 """

156 Objective function perturbation using gradient descent with step size

157 reduction according to a power series.

158 Has the option to "restart" the power series after a certain number of

159 steps.

160

161 func : Callable

162 The function to be optimized.

163 grad : Callable

164 The gradient of the function to be optimized.

165 func_args : List, optional

166 Additional arguments to be passed to the function, by default [].

167 grad_args : List, optional

168 Additional arguments to be passed to the gradient function, by default [].

169 n_red : int, optional

170 The number of reductions, by default 1.

171 step_size : float, optional

172 The step size for the gradient descent, by default 0.5.

173 n_restart : int, optional

174 The number of steps after which to restart the power series, by default -1 (no restart).

175 """

176

177 def __init__(

178 self,

179 func: Callable,

180 grad: Callable,

181 func_args: List = [],

182 grad_args: List = [],

183 n_red=1,

184 step_size=0.5,

185 n_restart=-1,

186 ):

187 super().__init__(func, grad, func_args, grad_args, n_red)

188 self.step_size = step_size

189 self._l = -1

190 self.n_restart = np.inf if n_restart == -1 else n_restart

191

192 def _function_reduction_step(self, x: npt.NDArray) -> npt.NDArray:

193 """

194 Perform a function reduction step using gradient descent.

195

196 Parameters

197 ----------

198 x : npt.NDArray

199 The current point in the optimization process.

200

201 Returns

202 -------

203 npt.NDArray

204 The updated point after performing the reduction step.

205 """

206 xp = cp if isinstance(x, cp.ndarray) else np

207 grad_eval = self.grad(x)

208 func_eval = self.func(x)

209 loop = True

210 while loop:

211 self._l += 1

212 x_ln = x - self.step_size**self._l * grad_eval / (xp.linalg.norm(grad_eval))

213 y_ln = self.func(x_ln)

214 if y_ln <= func_eval:

215 return x_ln

216 return x_ln

217

218 def pre_step(self):

219 """

220 Resets the power series after n steps.

221

222 Returns

223 -------

224 None

225 """

226 if self._k <= 0:

227 return

228 # possibly restart the power series

229 if self._k % self.n_restart == 0:

230 self._l = self._k // self.n_restart