Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\feasibility\_bands\_art3_algorithms.py: 55%

1 from abc import ABC

2 from typing import List

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

11 except ImportError:

12 NO_GPU = True

13 cp = np

14

15 from suppy.utils import ensure_float_array

16 from suppy.feasibility._linear_algorithms import HyperslabFeasibility

17

18

19 class ART3plusAlgorithm(HyperslabFeasibility, ABC):

20 """

21 ART3plusAlgorithm class for implementing the ART3+ algorithm.

22

23 Parameters

24 ----------

25 A : npt.NDArray

26 The matrix A involved in the feasibility problem.

27 lb : npt.NDArray

28 The lower bounds for the feasibility problem.

29 ub : npt.NDArray

30 The upper bounds for the feasibility problem.

31 algorithmic_relaxation : npt.NDArray or float, optional

32 The relaxation parameter for the algorithm, by default 1.

33 relaxation : float, optional

34 The relaxation parameter for the feasibility problem, by default 1.

35 proximity_flag : bool, optional

36 Flag to indicate whether to use proximity in the algorithm, by default True.

37 """

38

39 def __init__(

40 self,

41 A: npt.NDArray,

42 lb: npt.NDArray,

43 ub: npt.NDArray,

44 algorithmic_relaxation: npt.NDArray | float = 1,

45 relaxation: float = 1,

46 proximity_flag=True,

47 ):

48 super().__init__(A, lb, ub, algorithmic_relaxation, relaxation, proximity_flag)

49

50

51 class SequentialART3plus(ART3plusAlgorithm):

52 """

53 SequentialART3plus is an implementation of the ART3plus algorithm for

54 solving feasibility problems.

55

56 Parameters

57 ----------

58 A : npt.NDArray

59 The matrix representing the system of linear inequalities.

60 lb : npt.NDArray

61 The lower bounds for the variables.

62 ub : npt.NDArray

63 The upper bounds for the variables.

64 cs : None or List[int], optional

65 The control sequence for the algorithm. If None, it will be initialized to the range of the number of rows in A.

66 proximity_flag : bool, optional

67 A flag indicating whether to use proximity in the algorithm. Default is True.

68

69 Attributes

70 ----------

71 initial_cs : List[int]

72 The initial control sequence.

73 cs : List[int]

74 The current control sequence.

75 _feasible : bool

76 A flag indicating whether the current solution is feasible.

77

78 Methods

79 -------

80 _project(x)

81 Projects the point x onto the feasible region defined by the constraints.

82 solve(x, max_iter)

83 Solves the feasibility problem using the ART3plus algorithm.

84 """

85

86 def __init__(

87 self,

88 A: npt.NDArray,

89 lb: npt.NDArray,

90 ub: npt.NDArray,

91 cs: None | List[int] = None,

92 proximity_flag=True,

93 ):

94

95 super().__init__(A, lb, ub, 1, 1, proximity_flag)

96 xp = cp if self.A.gpu else np

97 if cs is None:

98 self.initial_cs = xp.arange(self.A.shape[0])

99 else:

100 self.initial_cs = cs

101

102 self.cs = self.initial_cs.copy()

103 self._feasible = True

104

105 def _project(self, x: npt.NDArray) -> npt.NDArray:

106 to_remove = []

107 for i in self.cs:

108 # TODO: add a boolean variable that skips this if the projection did not move the point?

109 p_i = self.single_map(x, i)

110 # should be precomputed

111 if (

112 3 / 2 * self.bounds.l[i] - 1 / 2 * self.bounds.u[i] <= p_i < self.bounds.l[i]

113 ): # lowe bound reflection

114 self.A.update_step(

115 x, 2 * self.inverse_row_norm[i] * (self.bounds.l[i] - p_i), i

116 ) # reflection

117 self._feasible = False

118

119 elif (

120 self.bounds.u[i] < p_i <= 3 / 2 * self.bounds.u[i] - 1 / 2 * self.bounds.l[i]

121 ): # upper bound reflection

122 self.A.update_step(

123 x, 2 * self.inverse_row_norm[i] * (self.bounds.u[i] - p_i), i

124 ) # reflection

125 self._feasible = False

126

127 elif self.bounds.u[i] - self.bounds.l[i] < abs(

128 p_i - (self.bounds.l[i] + self.bounds.u[i]) / 2

129 ):

130 self.A.update_step(

131 x,

132 self.inverse_row_norm[i] * ((self.bounds.l[i] + self.bounds.u[i]) / 2 - p_i),

133 i,

134 ) # projection onto center of hyperslab

135 self._feasible = False

136

137 else: # constraint is already met

138 to_remove.append(i)

139

140 # after loop remove constraints that are already met

141 self.cs = [i for i in self.cs if i not in to_remove] # is this fast?

142 return x

143

144

145 @ensure_float_array

146 def solve(

147 self,

148 x: npt.NDArray,

149 max_iter: int = 500,

150 constr_tol: float = 1e-6,

151 storage: bool = False,

152 proximity_measures: List | None = None,

153 ) -> npt.NDArray:

154 """

155 Solves the optimization problem using an iterative approach.

156

157 Parameters

158 ----------

159 x : npt.NDArray

160 Initial guess for the solution.

161 max_iter : int, optional

162 Maximum number of iterations to perform.

163 storage : bool, optional

164 Flag indicating whether to store the intermediate solutions, by default False.

165 constr_tol : float, optional

166 The tolerance for the constraints, by default 1e-6.

167 proximity_measures : List, optional

168 The proximity measures to calculate, by default None. Right now only the first in the list is used to check the feasibility.

169

170 Returns

171 -------

172 npt.NDArray

173 The solution after the iterative process.

174 """

175 self.cs = self.initial_cs.copy()

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

177 if proximity_measures is None:

178 proximity_measures = [("p_norm", 2)]

179 else:

180 # TODO: Check if the proximity measures are valid

181 _ = None

182

183 self.proximities = []

184 i = 0

185 feasible = False

186

187 if storage is True:

188 self.all_x = []

189 self.all_x.append(x.copy())

190

191 while i < max_iter and not feasible:

192

193 if len(self.cs) == 0:

194 if self._feasible: # ran over all constraints and still feasible

195 return x

196 else:

197 self.cs = self.initial_cs.copy()

198 self._feasible = True

199

200 x = self.project(x)

201 if storage is True:

202 self.all_x.append(x.copy())

203 self.proximities.append(self.proximity(x, proximity_measures))

204

205 # TODO: If proximity changes x some potential issues!

206 if self.proximities[-1][0] < constr_tol:

207

208 feasible = True

209 i += 1

210 if self.all_x is not None:

211 self.all_x = xp.array(self.all_x)

212 return x

213

214

215 class SimultaneousART3plus(ART3plusAlgorithm):

216 """

217 SimultaneousART3plus is an implementation of the ART3plus algorithm for

218 solving feasibility problems.

219

220 Parameters

221 ----------

222 A : npt.NDArray

223 The matrix representing the system of linear inequalities.

224 lb : npt.NDArray

225 The lower bounds for the variables.

226 ub : npt.NDArray

227 The upper bounds for the variables.

228 weights : None | List[float] | npt.NDArray, optional

229 The weights for the constraints. If None, default weights are used. Default is None.

230 proximity_flag : bool, optional

231 Flag to indicate whether to use proximity measure. Default is True.

232

233 Attributes

234 ----------

235 weights : npt.NDArray

236 The weights for the constraints.

237

238 """

239

240 def __init__(

241 self,

242 A: npt.NDArray,

243 lb: npt.NDArray,

244 ub: npt.NDArray,

245 weights: None | List[float] | npt.NDArray = None,

246 proximity_flag=True,

247 ):

248

249 super().__init__(A, lb, ub, 1, 1, proximity_flag)

250 xp = cp if self.A.gpu else np

251 if weights is None:

252 self.weights = xp.ones(self.A.shape[0]) / self.A.shape[0]

253 elif abs((weights.sum() - 1)) > 1e-10:

254 print("Weights do not add up to 1! Choosing default weight vector...")

255 self.weights = xp.ones(self.A.shape[0]) / self.A.shape[0]

256 else:

257 self.weights = weights

258

259 self._feasible = True

260 self._not_met = xp.arange(self.A.shape[0])

261

262 self._not_met_init = self._not_met.copy()

263 self._feasible = True

264

265 def _project(self, x: npt.NDArray) -> npt.NDArray:

266 """

267 Perform one step of the ART3plus algorithm.

268

269 Args:

270 x (npt.NDArray): The point to be projected.

271

272 Returns:

273 npt.NDArray: The projected point.

274 """

275 p = self.map(x)

276 p = p[self._not_met]

277 l_redux = self.bounds.l[self._not_met]

278 u_redux = self.bounds.u[self._not_met]

279

280 # following calculations are performed on subarrays

281 # assign different subsets

282 idx_1 = p < l_redux

283 idx_2 = p > u_redux

284 idx_3 = p < l_redux - (u_redux - l_redux) / 2

285 idx_4 = p > u_redux + (u_redux - l_redux) / 2

286

287 # sets on subarrays

288 set_1 = idx_1 & (not idx_3) # idxs for lower bound reflection

289 set_2 = idx_2 & (not idx_4) # idxs for upper bound reflection

290 set_3 = idx_3 | idx_4 # idxs for projections

291 # there should be no overlap between the different regions here!

292 x += (

293 self.weights[self._not_met][set_1]

294 * self.inverse_row_norm[self._not_met][set_1]

295 * (2 * (l_redux - p))[set_1]

296 @ self.A[self._not_met][set_1, :]

297 )

298 x += (

299 self.weights[self._not_met][set_2]

300 * self.inverse_row_norm[self._not_met][set_2]

301 * (2 * (u_redux - p))[set_2]

302 @ self.A[self._not_met][set_2, :]

303 )

304 x += (

305 self.weights[self._not_met][set_3]

306 * self.inverse_row_norm[self._not_met][set_3]

307 * ((l_redux + u_redux) / 2 - p)[set_3]

308 @ self.A[self._not_met][set_3, :]

309 )

310

311 # remove constraints that were already met before

312 self._not_met = self._not_met[(idx_1 | idx_2)]

313

314 if idx_1.sum() > 0 or idx_2.sum() > 0:

315 self._feasible = False

316

317 return x

318

319 def _proximity(self, x: npt.NDArray, proximity_measures: List[str]) -> float:

320 p = self.map(x)

321 # residuals are positive if constraints are met

322 (res_l, res_u) = self.bounds.residual(p)

323 res_u[res_u > 0] = 0

324 res_l[res_l > 0] = 0

325 res = -res_u - res_l

326 measures = []

327 for measure in proximity_measures:

328 if isinstance(measure, tuple):

329 if measure[0] == "p_norm":

330 measures.append(self.weights @ (res ** measure[1]))

331 else:

332 raise ValueError("Invalid proximity measure")

333 elif isinstance(measure, str) and measure == "max_norm":

334 measures.append(res.max())

335 else:

336 raise ValueError("Invalid proximity measure)")

337 return measures

338