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

158 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 HalfspaceFeasibility 

16from suppy.utils import LinearMapping 

17 

18 

19class HalfspaceAMSAlgorithm(HalfspaceFeasibility, ABC): 

20 """ 

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

22 set of linear inequalities. 

23 

24 Parameters 

25 ---------- 

26 A : npt.NDArray 

27 The matrix representing the coefficients of the linear inequalities. 

28 b : npt.NDArray 

29 Bound for linear inequalities 

30 algorithmic_relaxation : npt.NDArray or float, optional 

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

32 relaxation : float, optional 

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

34 proximity_flag : bool, optional 

35 A flag indicating whether to use proximity in the algorithm, by default True. 

36 """ 

37 

38 def __init__( 

39 self, 

40 A: npt.NDArray, 

41 b: npt.NDArray, 

42 algorithmic_relaxation: npt.NDArray | float = 1, 

43 relaxation: float = 1, 

44 proximity_flag: bool = True, 

45 ): 

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

47 

48 

49class SequentialAMSHalfspace(HalfspaceAMSAlgorithm): 

50 """ 

51 SequentialAMS class for sequentially applying the AMS algorithm. 

52 

53 Parameters 

54 ---------- 

55 A : npt.NDArray 

56 The matrix A used in the AMS algorithm. 

57 b : npt.NDArray 

58 Bound for linear inequalities 

59 algorithmic_relaxation : npt.NDArray or float, optional 

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

61 relaxation : float, optional 

62 The relaxation parameter, by default 1. 

63 cs : None or List[int], optional 

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

65 proximity_flag : bool, optional 

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

67 

68 Attributes 

69 ---------- 

70 """ 

71 

72 def __init__( 

73 self, 

74 A: npt.NDArray, 

75 b: npt.NDArray, 

76 algorithmic_relaxation: npt.NDArray | float = 1, 

77 relaxation: float = 1, 

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

79 proximity_flag: bool = True, 

80 ): 

81 

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

83 xp = cp if self._use_gpu else np 

84 if cs is None: 

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

86 else: 

87 self.cs = cs 

88 

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

90 """ 

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

92 constraints. 

93 

94 Parameters 

95 ---------- 

96 x : npt.NDArray 

97 The input array to be projected. 

98 

99 Returns 

100 ------- 

101 npt.NDArray 

102 The projected array. 

103 """ 

104 

105 for i in self.cs: 

106 p_i = self.single_map(x, i) 

107 res = self.b[i] - p_i 

108 if res < 0: 

109 self.A.update_step( 

110 x, self.algorithmic_relaxation * self.inverse_row_norm[i] * res, i 

111 ) 

112 return x 

113 

114 

115class SequentialWeightedAMSHalfspace(SequentialAMSHalfspace): 

116 """ 

117 Parameters 

118 ---------- 

119 A : npt.NDArray 

120 The constraint matrix. 

121 b : npt.NDArray 

122 Bound for linear inequalities 

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

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

125 used. 

126 algorithmic_relaxation : npt.NDArray or float, optional 

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

128 relaxation : float, optional 

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

130 weight_decay : float, optional 

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

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

133 cs : None or list of int, optional 

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

135 proximity_flag : bool, optional 

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

137 

138 Attributes 

139 ---------- 

140 weights : npt.NDArray 

141 The weights assigned to each constraint. 

142 weight_decay : float 

143 Decay rate for the weights. 

144 temp_weight_decay : float 

145 Initial value for weight decay. 

146 """ 

147 

148 def __init__( 

149 self, 

150 A: npt.NDArray, 

151 b: npt.NDArray, 

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

153 algorithmic_relaxation: npt.NDArray | float = 1, 

154 relaxation: float = 1, 

155 weight_decay: float = 1, 

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

157 proximity_flag: bool = True, 

158 ): 

159 

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

161 xp = cp if self._use_gpu else np 

162 self.weight_decay = weight_decay # decay rate 

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

164 

165 if weights is None: 

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

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

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

169 self.weights = weights 

170 

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

172 """ 

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

174 constraints. 

175 

176 Parameters 

177 ---------- 

178 x : npt.NDArray 

179 The input array to be projected. 

180 

181 Returns 

182 ------- 

183 npt.NDArray 

184 The projected array. 

185 

186 Notes 

187 ----- 

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

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

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

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

192 to the temporary weight decay after each iteration. 

193 """ 

194 

195 weighted_relaxation = self.algorithmic_relaxation * self.temp_weight_decay 

196 

197 for i in self.cs: 

198 p_i = self.single_map(x, i) 

199 res = self.b[i] - p_i 

200 if res < 0: 

201 self.A.update_step( 

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

203 ) 

204 

205 self.temp_weight_decay *= self.weight_decay 

206 return x 

207 

208 

209class SimultaneousAMSHalfspace(HalfspaceAMSAlgorithm): 

210 """ 

211 SimultaneousAMS is an implementation of the AMS (Alternating 

212 Minimization Scheme) algorithm 

213 that performs simultaneous projections and proximity calculations. 

214 

215 Parameters 

216 ---------- 

217 A : npt.NDArray 

218 The matrix representing the constraints. 

219 b : npt.NDArray 

220 Bound for linear inequalities 

221 algorithmic_relaxation : npt.NDArray or float, optional 

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

223 relaxation : float, optional 

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

225 weights : None or List[float], optional 

226 The weights for the constraints, by default None. 

227 proximity_flag : bool, optional 

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

229 """ 

230 

231 def __init__( 

232 self, 

233 A: npt.NDArray, 

234 b: npt.NDArray, 

235 algorithmic_relaxation: npt.NDArray | float = 1, 

236 relaxation: float = 1, 

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

238 proximity_flag: bool = True, 

239 ): 

240 

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

242 

243 xp = cp if self._use_gpu else np 

244 

245 if weights is None: 

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

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

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

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

250 else: 

251 self.weights = weights 

252 

253 def _project(self, x): 

254 # simultaneous projection 

255 p = self.map(x) 

256 res = self.b - p 

257 res_idx = res < 0 

258 x += self.algorithmic_relaxation * ( 

259 self.weights[res_idx] 

260 * self.inverse_row_norm[res_idx] 

261 * res[res_idx] 

262 @ self.A[res_idx, :] 

263 ) 

264 return x 

265 

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

267 p = self.map(x) 

268 # residuals are positive if constraints are met 

269 res = self.b - p 

270 res[res > 0] = 0 

271 res = -res 

272 

273 measures = [] 

274 for measure in proximity_measures: 

275 if isinstance(measure, tuple): 

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

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

278 else: 

279 raise ValueError("Invalid proximity measure") 

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

281 measures.append(res.max()) 

282 else: 

283 raise ValueError("Invalid proximity measure)") 

284 return measures 

285 

286 

287class ExtrapolatedLandweberHalfspace(SimultaneousAMSHalfspace): 

288 def __init__( 

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

290 ): 

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

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

293 self.weight_norm = self.weights / self.a_i 

294 self.sigmas = [] 

295 

296 def _project(self, x): 

297 p = self.map(x) 

298 res = self.b - p 

299 res_idx = res < 0 

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

301 self.sigmas.append(0) 

302 return x 

303 t = self.weight_norm[res_idx] * res[res_idx] 

304 t_2 = t @ self.A[res_idx, :] 

305 sig = (res[res_idx] @ t) / (t_2 @ t_2) 

306 self.sigmas.append(sig) 

307 x += sig * t_2 

308 

309 return x 

310 

311 

312class BlockIterativeAMSHalfspace(HalfspaceAMSAlgorithm): 

313 """ 

314 Block Iterative AMS Algorithm. 

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

316 Minimization Scheme) algorithm. 

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

318 

319 Parameters 

320 ---------- 

321 A : npt.NDArray 

322 The matrix representing the linear constraints. 

323 b : npt.NDArray 

324 Bound for linear inequalities 

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

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

327 algorithmic_relaxation : npt.NDArray or float, optional 

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

329 relaxation : float, optional 

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

331 proximity_flag : bool, optional 

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

333 

334 Raises 

335 ------ 

336 ValueError 

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

338 """ 

339 

340 def __init__( 

341 self, 

342 A: npt.NDArray, 

343 b: npt.NDArray, 

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

345 algorithmic_relaxation: npt.NDArray | float = 1, 

346 relaxation: float = 1, 

347 proximity_flag: bool = True, 

348 ): 

349 

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

351 

352 xp = cp if self._use_gpu else np 

353 

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

355 for el in weights: 

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

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

358 

359 self.weights = [] 

360 self.block_idxs = [ 

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

362 ] # get idxs that meet requirements 

363 

364 # assemble a list of general weights 

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

366 for el in weights: 

367 el = xp.asarray(el) 

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

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

370 

371 def _project(self, x): 

372 # simultaneous projection 

373 

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

375 p = self.indexed_map(x, block_idx) 

376 res = self.b[block_idx] - p 

377 

378 res_idx = res < 0 

379 full_idx = block_idx[res_idx] 

380 

381 x += self.algorithmic_relaxation * ( 

382 el[res_idx] * self.inverse_row_norm[full_idx] * res[res_idx] @ self.A[full_idx, :] 

383 ) 

384 

385 return x 

386 

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

388 p = self.map(x) 

389 # residuals are positive if constraints are met 

390 res = self.b - p 

391 res[res > 0] = 0 

392 res = -res 

393 

394 measures = [] 

395 for measure in proximity_measures: 

396 if isinstance(measure, tuple): 

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

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

399 else: 

400 raise ValueError("Invalid proximity measure") 

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

402 measures.append(res.max()) 

403 else: 

404 raise ValueError("Invalid proximity measure)") 

405 return measures 

406 

407 

408class StringAveragedAMSHalfspace(HalfspaceAMSAlgorithm): 

409 """ 

410 StringAveragedAMS is an implementation of the HalfspaceAMSAlgorithm that 

411 performs 

412 string averaged projections. 

413 

414 Parameters 

415 ---------- 

416 A : npt.NDArray 

417 The matrix A used in the algorithm. 

418 b : npt.NDArray 

419 Bound for linear inequalities 

420 strings : List[List[int]] 

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

422 algorithmic_relaxation : npt.NDArray or float, optional 

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

424 relaxation : float, optional 

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

426 weights : None or List[float], optional 

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

428 proximity_flag : bool, optional 

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

430 """ 

431 

432 def __init__( 

433 self, 

434 A: npt.NDArray, 

435 b: npt.NDArray, 

436 strings: List[List[int]], 

437 algorithmic_relaxation: npt.NDArray | float = 1, 

438 relaxation: float = 1, 

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

440 proximity_flag: bool = True, 

441 ): 

442 

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

444 xp = cp if self._use_gpu else np 

445 self.strings = strings 

446 if weights is None: 

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

448 

449 # if check_weight_validity(weights): 

450 # self.weights = weights 

451 else: 

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

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

454 

455 self.weights = weights 

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

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

458 

459 def _project(self, x): 

460 # string averaged projection 

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

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

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

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

465 for i in string: 

466 p_i = self.single_map(x_s, i) 

467 res_i = self.b[i] - p_i 

468 if res_i < 0: 

469 self.A.update_step( 

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

471 ) 

472 x += weight * x_s 

473 return x