Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\feasibility\_bands\_arm_algorithms.py: 69%

128 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2025-02-05 17:36 +0100

1from abc import ABC 

2from typing import List 

3import numpy as np 

4import numpy.typing as npt 

5from suppy.utils import LinearMapping 

6from suppy.feasibility._linear_algorithms import HyperslabFeasibility 

7 

8try: 

9 import cupy as cp 

10 

11 NO_GPU = False 

12 

13except ImportError: 

14 NO_GPU = True 

15 cp = np 

16 

17 

18class ARMAlgorithm(HyperslabFeasibility, ABC): 

19 """ 

20 ARMAlgorithm class for handling feasibility problems with additional 

21 algorithmic relaxation. 

22 

23 Parameters 

24 ---------- 

25 A : npt.NDArray 

26 The matrix representing the linear mapping. 

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 specific to the algorithm, by default 1. 

33 relaxation : float, optional 

34 The general relaxation parameter, by default 1. 

35 proximity_flag : bool, optional 

36 Flag to indicate if proximity constraints should be considered, 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 

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

50 

51 

52class SequentialARM(ARMAlgorithm): 

53 """ 

54 SequentialARM is a class that implements a sequential algorithm for 

55 Adaptive Relaxation Method (ARM). 

56 

57 Parameters 

58 ---------- 

59 A : npt.NDArray 

60 The matrix A used in the ARM algorithm. 

61 lb : npt.NDArray 

62 The lower bounds for the variables. 

63 ub : npt.NDArray 

64 The upper bounds for the variables. 

65 algorithmic_relaxation : npt.NDArray or float, optional 

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

67 relaxation : float, optional 

68 The relaxation parameter, by default 1. 

69 cs : None or List[int], optional 

70 The list of indices for the constraints, by default None. 

71 proximity_flag : bool, optional 

72 Flag to indicate if proximity should be considered, by default True. 

73 """ 

74 

75 def __init__( 

76 self, 

77 A: npt.NDArray, 

78 lb: npt.NDArray, 

79 ub: npt.NDArray, 

80 algorithmic_relaxation: npt.NDArray | float = 1, 

81 relaxation: float = 1, 

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

83 proximity_flag=True, 

84 ): 

85 

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

87 xp = cp if self._use_gpu else np 

88 self._k = 0 # relaxation power 

89 if cs is None: 

90 self.cs = xp.arange(self.A.shape[0]) 

91 else: 

92 self.cs = cs 

93 

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

95 xp = cp if self._use_gpu else np 

96 

97 for i in self.cs: 

98 p_i = self.single_map(x, i) 

99 d = p_i - self.bounds.center[i] 

100 psi = (self.bounds.u[i] - self.bounds.l[i]) / 2 

101 if xp.abs(d) > psi: 

102 self.A.update_step( 

103 x, 

104 -1 

105 * self.algorithmic_relaxation**self._k 

106 / 2 

107 * self.inverse_row_norm[i] 

108 * ((d**2 - psi**2) / d), 

109 i, 

110 ) 

111 return x 

112 

113 

114class SimultaneousARM(ARMAlgorithm): 

115 """ 

116 SimultaneousARM is a class that implements an ARM (Adaptive Relaxation 

117 Method) algorithm 

118 for solving feasibility problems. 

119 

120 Parameters 

121 ---------- 

122 A : npt.NDArray 

123 The matrix representing the constraints. 

124 lb : npt.NDArray 

125 The lower bounds for the constraints. 

126 ub : npt.NDArray 

127 The upper bounds for the constraints. 

128 algorithmic_relaxation : npt.NDArray or float, optional 

129 The relaxation parameter for the algorithm. Default is 1. 

130 relaxation : float, optional 

131 The relaxation parameter for the constraints. Default is 1. 

132 weights : None, List[float], or npt.NDArray, optional 

133 The weights for the constraints. If None, weights are set to be uniform. Default is None. 

134 proximity_flag : bool, optional 

135 Flag to indicate whether to use proximity in the algorithm. Default is True. 

136 

137 Methods 

138 ------- 

139 _project(x) 

140 Performs the simultaneous projection of the input vector x. 

141 _proximity(x) 

142 Computes the proximity measure of the input vector x. 

143 """ 

144 

145 def __init__( 

146 self, 

147 A: npt.NDArray, 

148 lb: npt.NDArray, 

149 ub: npt.NDArray, 

150 algorithmic_relaxation: npt.NDArray | float = 1, 

151 relaxation: float = 1, 

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

153 proximity_flag=True, 

154 ): 

155 

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

157 self._k = 0 

158 xp = cp if self._use_gpu else np 

159 

160 if weights is None: 

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

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

163 print("Weights do not add up to 1! Renormalizing to 1...") 

164 self.weights = weights / weights.sum() 

165 else: 

166 self.weights = weights 

167 

168 def _project(self, x): 

169 xp = cp if self._use_gpu else np 

170 # simultaneous projection 

171 p = self.map(x) 

172 d = p - self.bounds.center 

173 psi = self.bounds.half_distance 

174 d_idx = xp.abs(d) > psi 

175 x -= ( 

176 self.algorithmic_relaxation**self._k 

177 / 2 

178 * ( 

179 self.weights[d_idx] 

180 * self.inverse_row_norm[d_idx] 

181 * (d[d_idx] - (psi[d_idx] ** 2) / d[d_idx]) 

182 ) 

183 @ self.A[d_idx, :] 

184 ) 

185 

186 self._k += 1 

187 return x 

188 

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

190 p = self.map(x) 

191 # residuals are positive if constraints are met 

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

193 res_u[res_u > 0] = 0 

194 res_l[res_l > 0] = 0 

195 res = -res_u - res_l 

196 measures = [] 

197 for measure in proximity_measures: 

198 if isinstance(measure, tuple): 

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

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

201 else: 

202 raise ValueError("Invalid proximity measure") 

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

204 measures.append(res.max()) 

205 else: 

206 raise ValueError("Invalid proximity measure)") 

207 return measures 

208 

209 

210class BIPARM(ARMAlgorithm): 

211 """ 

212 BIPARM Algorithm for feasibility problems. 

213 

214 Parameters 

215 ---------- 

216 A : npt.NDArray 

217 The matrix representing the constraints. 

218 lb : npt.NDArray 

219 The lower bounds for the constraints. 

220 ub : npt.NDArray 

221 The upper bounds for the constraints. 

222 algorithmic_relaxation : npt.NDArray or float, optional 

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

224 relaxation : float, optional 

225 The relaxation parameter for the projections, by default 1. 

226 weights : None, List[float], or npt.NDArray, optional 

227 The weights for the constraints, by default None. 

228 proximity_flag : bool, optional 

229 Flag to indicate if proximity should be considered, by default True. 

230 

231 Methods 

232 ------- 

233 _project(x) 

234 Perform the simultaneous projection of x. 

235 _proximity(x) 

236 Calculate the proximity measure for x. 

237 """ 

238 

239 def __init__( 

240 self, 

241 A: npt.NDArray, 

242 lb: npt.NDArray, 

243 ub: npt.NDArray, 

244 algorithmic_relaxation: npt.NDArray | float = 1, 

245 relaxation: float = 1, 

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

247 proximity_flag=True, 

248 ): 

249 

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

251 xp = cp if self._use_gpu else np 

252 self._k = 0 

253 if weights is None: 

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

255 

256 # if check_weight_validity(weights): 

257 # self.weights = weights 

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

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

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

261 else: 

262 self.weights = weights 

263 

264 def _project(self, x): 

265 # simultaneous projection 

266 p = self.map(x) 

267 d = p - self.bounds.center 

268 psi = self.bounds.half_distance 

269 d_idx = abs(d) > psi 

270 x -= ( 

271 self.algorithmic_relaxation**self._k 

272 / 2 

273 * ( 

274 self.weights[d_idx] 

275 * self.inverse_row_norm[d_idx] 

276 * (d[d_idx] - (psi[d_idx] ** 2) / d[d_idx]) 

277 ) 

278 @ self.A[d_idx, :] 

279 ) 

280 

281 self._k += 1 

282 return x 

283 

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

285 p = self.map(x) 

286 # residuals are positive if constraints are met 

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

288 res_u[res_u > 0] = 0 

289 res_l[res_l > 0] = 0 

290 res = -res_u - res_l 

291 measures = [] 

292 for measure in proximity_measures: 

293 if isinstance(measure, tuple): 

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

295 measures.append(1 / len(res) * self.total_weights @ (res ** measure[1])) 

296 else: 

297 raise ValueError("Invalid proximity measure") 

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

299 measures.append(res.max()) 

300 else: 

301 raise ValueError("Invalid proximity measure)") 

302 return measures 

303 

304 

305class StringAveragedARM(ARMAlgorithm): 

306 """ 

307 String Averaged ARM Algorithm. 

308 This class implements the String Averaged ARM (Adaptive Relaxation Method) 

309 algorithm, 

310 which is used for feasibility problems involving strings of indices. 

311 

312 Parameters 

313 ---------- 

314 A : npt.NDArray 

315 The matrix A involved in the feasibility problem. 

316 lb : npt.NDArray 

317 The lower bounds for the feasibility problem. 

318 ub : npt.NDArray 

319 The upper bounds for the feasibility problem. 

320 strings : List[List[int]] 

321 A list of lists, where each inner list represents a string of indices. 

322 algorithmic_relaxation : npt.NDArray or float, optional 

323 The algorithmic relaxation parameter, by default 1. 

324 relaxation : float, optional 

325 The relaxation parameter, by default 1. 

326 weights : None or List[float], optional 

327 The weights for each string, by default None. If None, equal weights are assigned. 

328 proximity_flag : bool, optional 

329 A flag indicating whether to use proximity, by default True. 

330 

331 Methods 

332 ------- 

333 _project(x) 

334 Projects the input vector x using the string averaged projection method. 

335 

336 Raises 

337 ------ 

338 ValueError 

339 If the number of weights does not match the number of strings. 

340 """ 

341 

342 def __init__( 

343 self, 

344 A: npt.NDArray, 

345 lb: npt.NDArray, 

346 ub: npt.NDArray, 

347 strings: List[List[int]], 

348 algorithmic_relaxation: npt.NDArray | float = 1, 

349 relaxation: float = 1, 

350 weights: None | List[float] = None, 

351 proximity_flag=True, 

352 ): 

353 

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

355 xp = cp if self._use_gpu else np 

356 self._k = 0 

357 self.strings = strings 

358 

359 if weights is None: 

360 self.weights = xp.ones(len(strings)) / len(strings) 

361 

362 # if check_weight_validity(weights): 

363 # self.weights = weights 

364 else: 

365 if len(weights) != len(self.strings): 

366 raise ValueError("The number of weights must be equal to the number of strings.") 

367 

368 self.weights = weights 

369 

370 def _project(self, x): 

371 xp = cp if self._use_gpu else np 

372 # string averaged projection 

373 x_c = x.copy() # create a general copy of x 

374 x -= x # reset x is this viable? 

375 for string, weight in zip(self.strings, self.weights): 

376 x_s = x_c.copy() # generate a copy for individual strings 

377 for i in string: 

378 p_i = self.single_map(x_s, i) 

379 d = p_i - self.bounds.center[i] 

380 psi = (self.bounds.u[i] - self.bounds.l[i]) / 2 

381 if xp.abs(d) > psi: 

382 self.A.update_step( 

383 x_s, 

384 -1 

385 * self.algorithmic_relaxation**self._k 

386 / 2 

387 * ((d**2 - psi**2) / d) 

388 * self.inverse_row_norm[i], 

389 i, 

390 ) 

391 x += weight * x_s 

392 return x