Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\feasibility\_hyperplanes\_ams_algorithms.py: 75%

148 statements  

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

1from abc import ABC 

2from typing import List 

3import numpy as np 

4import numpy.typing as npt 

5 

6try: 

7 import cupy as cp 

8 

9 NO_GPU = False 

10 

11except ImportError: 

12 NO_GPU = True 

13 cp = np 

14 

15from suppy.feasibility._linear_algorithms import HyperplaneFeasibility 

16from suppy.utils import LinearMapping 

17 

18 

19class HyperplaneAMSAlgorithm(HyperplaneFeasibility, ABC): 

20 """ 

21 The HyperplaneAMSAlgorithm class is used to find a feasible solution to 

22 a 

23 set of linear inequalities. 

24 

25 Parameters 

26 ---------- 

27 A : npt.NDArray 

28 The matrix representing the coefficients of the linear inequalities. 

29 b : npt.NDArray 

30 Bound for linear inequalities 

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 A flag indicating whether to use proximity in the algorithm, by default True. 

37 """ 

38 

39 def __init__( 

40 self, 

41 A: npt.NDArray, 

42 b: npt.NDArray, 

43 algorithmic_relaxation: npt.NDArray | float = 1, 

44 relaxation: float = 1, 

45 proximity_flag: bool = True, 

46 ): 

47 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag) 

48 

49 

50class SequentialAMSHyperplane(HyperplaneAMSAlgorithm): 

51 """ 

52 SequentialAMS class for sequentially applying the AMS algorithm. 

53 

54 Parameters 

55 ---------- 

56 A : npt.NDArray 

57 The matrix A used in the AMS algorithm. 

58 b : npt.NDArray 

59 Bound for linear inequalities 

60 algorithmic_relaxation : npt.NDArray or float, optional 

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

62 relaxation : float, optional 

63 The relaxation parameter, by default 1. 

64 cs : None or List[int], optional 

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

66 proximity_flag : bool, optional 

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

68 

69 Attributes 

70 ---------- 

71 """ 

72 

73 def __init__( 

74 self, 

75 A: npt.NDArray, 

76 b: npt.NDArray, 

77 algorithmic_relaxation: npt.NDArray | float = 1, 

78 relaxation: float = 1, 

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

80 proximity_flag: bool = True, 

81 ): 

82 

83 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag) 

84 xp = cp if self._use_gpu else np 

85 if cs is None: 

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

87 else: 

88 self.cs = cs 

89 

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

91 """ 

92 Projects the input array `x` onto the feasible region defined by the 

93 constraints. 

94 

95 Parameters 

96 ---------- 

97 x : npt.NDArray 

98 The input array to be projected. 

99 

100 Returns 

101 ------- 

102 npt.NDArray 

103 The projected array. 

104 """ 

105 

106 for i in self.cs: 

107 p_i = self.single_map(x, i) 

108 res = self.b[i] - p_i 

109 self.A.update_step(x, self.algorithmic_relaxation * self.inverse_row_norm[i] * res, i) 

110 return x 

111 

112 

113class SequentialWeightedAMSHyperplane(SequentialAMSHyperplane): 

114 """ 

115 Parameters 

116 ---------- 

117 A : npt.NDArray 

118 The constraint matrix. 

119 b : npt.NDArray 

120 Bound for linear inequalities 

121 weights : None, list of float, or npt.NDArray, optional 

122 The weights assigned to each constraint. If None, default weights are 

123 used. 

124 algorithmic_relaxation : npt.NDArray or float, optional 

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

126 relaxation : float, optional 

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

128 weight_decay : float, optional 

129 Parameter that determines the rate at which the weights are reduced 

130 after each phase (weights * weight_decay). Default is 1. 

131 cs : None or list of int, optional 

132 The indices of the constraints to be considered. Default is None. 

133 proximity_flag : bool, optional 

134 Flag to indicate if proximity should be considered. Default is True. 

135 

136 Attributes 

137 ---------- 

138 weights : npt.NDArray 

139 The weights assigned to each constraint. 

140 weight_decay : float 

141 Decay rate for the weights. 

142 temp_weight_decay : float 

143 Initial value for weight decay. 

144 """ 

145 

146 def __init__( 

147 self, 

148 A: npt.NDArray, 

149 b: npt.NDArray, 

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

151 algorithmic_relaxation: npt.NDArray | float = 1, 

152 relaxation: float = 1, 

153 weight_decay: float = 1, 

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

155 proximity_flag: bool = True, 

156 ): 

157 

158 super().__init__(A, b, algorithmic_relaxation, relaxation, cs, proximity_flag) 

159 xp = cp if self._use_gpu else np 

160 self.weight_decay = weight_decay # decay rate 

161 self.temp_weight_decay = 1 # initial value for weight decay 

162 

163 if weights is None: 

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

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

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

167 self.weights = weights 

168 

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

170 """ 

171 Projects the input array `x` onto a feasible region defined by the 

172 constraints. 

173 

174 Parameters 

175 ---------- 

176 x : npt.NDArray 

177 The input array to be projected. 

178 

179 Returns 

180 ------- 

181 npt.NDArray 

182 The projected array. 

183 

184 Notes 

185 ----- 

186 This method iteratively adjusts the input array `x` based on the constraints 

187 defined in `self.cs`. For each constraint, it computes the projection and 

188 checks if the constraints are violated. If a constraint is violated, it updates 

189 the array `x` using a weighted relaxation factor. The weight decay is applied 

190 to the temporary weight decay after each iteration. 

191 """ 

192 

193 weighted_relaxation = self.algorithmic_relaxation * self.temp_weight_decay 

194 

195 for i in self.cs: 

196 p_i = self.single_map(x, i) 

197 res = self.b[i] - p_i 

198 self.A.update_step( 

199 x, weighted_relaxation * self.weights[i] * self.inverse_row_norm[i] * res, i 

200 ) 

201 

202 self.temp_weight_decay *= self.weight_decay 

203 return x 

204 

205 

206class SimultaneousAMSHyperplane(HyperplaneAMSAlgorithm): 

207 """ 

208 SimultaneousAMS is an implementation of the AMS (Alternating 

209 Minimization Scheme) algorithm 

210 that performs simultaneous projections and proximity calculations. 

211 

212 Parameters 

213 ---------- 

214 A : npt.NDArray 

215 The matrix representing the constraints. 

216 b : npt.NDArray 

217 Bound for linear inequalities 

218 algorithmic_relaxation : npt.NDArray or float, optional 

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

220 relaxation : float, optional 

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

222 weights : None or List[float], optional 

223 The weights for the constraints, by default None. 

224 proximity_flag : bool, optional 

225 Flag to indicate if proximity calculations should be performed, by default True. 

226 """ 

227 

228 def __init__( 

229 self, 

230 A: npt.NDArray, 

231 b: npt.NDArray, 

232 algorithmic_relaxation: npt.NDArray | float = 1, 

233 relaxation: float = 1, 

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

235 proximity_flag: bool = True, 

236 ): 

237 

238 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag) 

239 

240 xp = cp if self._use_gpu else np 

241 

242 if weights is None: 

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

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

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

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

247 else: 

248 self.weights = weights 

249 

250 def _project(self, x): 

251 # simultaneous projection 

252 p = self.map(x) 

253 res = self.b - p 

254 x += self.algorithmic_relaxation * (self.weights * self.inverse_row_norm * res @ self.A) 

255 return x 

256 

257 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> float: 

258 p = self.map(x) 

259 # residuals are positive if constraints are met 

260 res = abs(self.b - p) 

261 measures = [] 

262 for measure in proximity_measures: 

263 if isinstance(measure, tuple): 

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

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

266 else: 

267 raise ValueError("Invalid proximity measure") 

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

269 measures.append(res.max()) 

270 else: 

271 raise ValueError("Invalid proximity measure") 

272 return measures 

273 

274 

275class ExtrapolatedLandweberHyperplane(SimultaneousAMSHyperplane): 

276 def __init__( 

277 self, A, b, algorithmic_relaxation=1, relaxation=1, weights=None, proximity_flag=True 

278 ): 

279 super().__init__(A, b, algorithmic_relaxation, relaxation, weights, proximity_flag) 

280 self.a_i = self.A.row_norm(2, 2) 

281 self.weight_norm = self.weights / self.a_i 

282 self.sigmas = [] 

283 

284 def _project(self, x): 

285 p = self.map(x) 

286 res = self.b - p 

287 res_idx = res != 0 

288 if not (np.any(res_idx)): 

289 self.sigmas.append(0) 

290 return x 

291 t = self.weight_norm * res 

292 t_2 = t @ self.A 

293 sig = (res @ t) / (t_2 @ t_2) 

294 self.sigmas.append(sig) 

295 x += sig * t_2 

296 

297 return x 

298 

299 

300class BlockIterativeAMSHyperplane(HyperplaneAMSAlgorithm): 

301 """ 

302 Block Iterative AMS Algorithm. 

303 This class implements a block iterative version of the AMS (Alternating 

304 Minimization Scheme) algorithm. 

305 It is designed to handle constraints and weights in a block-wise manner. 

306 

307 Parameters 

308 ---------- 

309 A : npt.NDArray 

310 The matrix representing the linear constraints. 

311 b : npt.NDArray 

312 Bound for linear inequalities 

313 weights : List[List[float]] or List[npt.NDArray] 

314 A list of lists or arrays representing the weights for each block. Each list/array should sum to 1. 

315 algorithmic_relaxation : npt.NDArray or float, optional 

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

317 relaxation : float, optional 

318 The relaxation parameter for the constraints, by default 1. 

319 proximity_flag : bool, optional 

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

321 

322 Raises 

323 ------ 

324 ValueError 

325 If any of the weight lists do not sum to 1. 

326 """ 

327 

328 def __init__( 

329 self, 

330 A: npt.NDArray, 

331 b: npt.NDArray, 

332 weights: List[List[float]] | List[npt.NDArray], 

333 algorithmic_relaxation: npt.NDArray | float = 1, 

334 relaxation: float = 1, 

335 proximity_flag: bool = True, 

336 ): 

337 

338 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag) 

339 

340 xp = cp if self._use_gpu else np 

341 

342 # check that weights is a list of lists that add up to 1 each 

343 for el in weights: 

344 if xp.abs((xp.sum(el) - 1)) > 1e-10: 

345 raise ValueError("Weights do not add up to 1!") 

346 

347 self.weights = [] 

348 self.block_idxs = [ 

349 xp.where(xp.array(el) > 0)[0] for el in weights 

350 ] # get idxs that meet requirements 

351 

352 # assemble a list of general weights 

353 self.total_weights = xp.zeros_like(weights[0]) 

354 for el in weights: 

355 el = xp.asarray(el) 

356 self.weights.append(el[xp.array(el) > 0]) # remove non zero weights 

357 self.total_weights += el / len(weights) 

358 

359 def _project(self, x): 

360 # simultaneous projection 

361 

362 for el, block_idx in zip(self.weights, self.block_idxs): # get mask and associated weights 

363 p = self.indexed_map(x, block_idx) 

364 res = self.b[block_idx] - p 

365 

366 x += self.algorithmic_relaxation * ( 

367 el * self.inverse_row_norm[block_idx] * res @ self.A[block_idx, :] 

368 ) 

369 return x 

370 

371 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> float: 

372 p = self.map(x) 

373 # residuals are positive if constraints are met 

374 res = abs(self.b - p) 

375 measures = [] 

376 for measure in proximity_measures: 

377 if isinstance(measure, tuple): 

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

379 measures.append(self.total_weights @ (res ** measure[1])) 

380 else: 

381 raise ValueError("Invalid proximity measure") 

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

383 measures.append(res.max()) 

384 else: 

385 raise ValueError("Invalid proximity measure") 

386 return measures 

387 

388 

389class StringAveragedAMSHyperplane(HyperplaneAMSAlgorithm): 

390 

391 """ 

392 StringAveragedAMS is an implementation of the HyperplaneAMSAlgorithm 

393 that 

394 performs 

395 string averaged projections. 

396 

397 Parameters 

398 ---------- 

399 A : npt.NDArray 

400 The matrix A used in the algorithm. 

401 b : npt.NDArray 

402 Bound for linear inequalities 

403 strings : List[List[int]] 

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

405 algorithmic_relaxation : npt.NDArray or float, optional 

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

407 relaxation : float, optional 

408 The relaxation parameter for the projection, by default 1. 

409 weights : None or List[float], optional 

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

411 proximity_flag : bool, optional 

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

413 """ 

414 

415 def __init__( 

416 self, 

417 A: npt.NDArray, 

418 b: npt.NDArray, 

419 strings: List[List[int]], 

420 algorithmic_relaxation: npt.NDArray | float = 1, 

421 relaxation: float = 1, 

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

423 proximity_flag: bool = True, 

424 ): 

425 

426 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag) 

427 xp = cp if self._use_gpu else np 

428 self.strings = strings 

429 if weights is None: 

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

431 

432 # if check_weight_validity(weights): 

433 # self.weights = weights 

434 else: 

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

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

437 

438 self.weights = weights 

439 # print('Choosing default weight vector...') 

440 # self.weights = np.ones(self.A.shape[0])/self.A.shape[0] 

441 

442 def _project(self, x): 

443 # string averaged projection 

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

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

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

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

448 for i in string: 

449 p_i = self.single_map(x_s, i) 

450 res_i = self.b[i] - p_i 

451 self.A.update_step( 

452 x_s, self.algorithmic_relaxation * self.inverse_row_norm[i] * res_i, i 

453 ) 

454 x += weight * x_s 

455 return x