Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\projections\_projection_methods.py: 70%

172 statements  

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

1""" 

2General implementation for sequential, simultaneous, block iterative and 

3string averaged projection methods. 

4""" 

5from abc import ABC 

6from typing import List 

7import numpy as np 

8import numpy.typing as npt 

9 

10try: 

11 import cupy as cp 

12 

13 NO_GPU = False 

14except ImportError: 

15 cp = np 

16 NO_GPU = True 

17 

18from suppy.projections._projections import Projection, BasicProjection 

19from suppy.utils import ensure_float_array 

20 

21 

22class ProjectionMethod(Projection, ABC): 

23 """ 

24 A class used to represent methods for projecting a point onto multiple 

25 sets. 

26 

27 Parameters 

28 ---------- 

29 projections : List[Projection] 

30 A list of Projection objects to be used in the projection method. 

31 relaxation : int, optional 

32 A relaxation parameter for the projection method (default is 1). 

33 proximity_flag : bool 

34 Flag to indicate whether to take this object into account when calculating proximity, by default True. 

35 

36 Attributes 

37 ---------- 

38 projections : List[Projection] 

39 The list of Projection objects used in the projection method. 

40 all_x : array-like or None 

41 Storage for all x values if storage is enabled during solve. 

42 proximities : list 

43 A list to store proximity values during the solve process. 

44 relaxation : float 

45 Relaxation parameter for the projection. 

46 proximity_flag : bool 

47 Flag to indicate whether to take this object into account when calculating proximity. 

48 """ 

49 

50 def __init__(self, projections: List[Projection], relaxation=1, proximity_flag=True): 

51 # if all([proj._use_gpu == projections[0]._use_gpu for proj in projections]): 

52 # self._use_gpu = projections[0]._use_gpu 

53 # else: 

54 # raise ValueError("Projections do not have the same gpu flag!") 

55 super().__init__(relaxation, proximity_flag) 

56 self.projections = projections 

57 self.all_x = None 

58 self.proximities = [] 

59 

60 def visualize(self, ax): 

61 """ 

62 Visualizes all projection objects (if applicable) on the given 

63 matplotlib axis. 

64 

65 Parameters 

66 ---------- 

67 ax : matplotlib.axes.Axes 

68 The matplotlib axis on which to visualize the projections. 

69 """ 

70 for proj in self.projections: 

71 proj.visualize(ax) 

72 

73 @ensure_float_array 

74 def solve( 

75 self, 

76 x: npt.NDArray, 

77 max_iter: int = 500, 

78 storage: bool = False, 

79 constr_tol: float = 1e-6, 

80 proximity_measures: List | None = None, 

81 ) -> npt.NDArray: 

82 """ 

83 Solves the optimization problem using an iterative approach. 

84 

85 Parameters 

86 ---------- 

87 x : npt.NDArray 

88 Initial guess for the solution. 

89 max_iter : int 

90 Maximum number of iterations to perform. 

91 storage : bool, optional 

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

93 constr_tol : float, optional 

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

95 proximity_measures : List, optional 

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

97 

98 Returns 

99 ------- 

100 npt.NDArray 

101 The solution after the iterative process. 

102 """ 

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

104 if proximity_measures is None: 

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

106 else: 

107 # TODO: Check if the proximity measures are valid 

108 _ = None 

109 

110 self.proximities = [] 

111 i = 0 

112 feasible = False 

113 

114 if storage is True: 

115 self.all_x = [] 

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

117 

118 while i < max_iter and not feasible: 

119 x = self.project(x) 

120 if storage is True: 

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

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

123 

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

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

126 

127 feasible = True 

128 i += 1 

129 if self.all_x is not None: 

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

131 return x 

132 

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

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

135 proxs = xp.array( 

136 [xp.array(proj.proximity(x, proximity_measures)) for proj in self.projections] 

137 ) 

138 measures = [] 

139 for i, measure in enumerate(proximity_measures): 

140 if isinstance(measure, tuple): 

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

142 measures.append((proxs[:, i]).mean()) 

143 else: 

144 raise ValueError("Invalid proximity measure") 

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

146 measures.append(proxs[:, i].max()) 

147 else: 

148 raise ValueError("Invalid proximity measure") 

149 return measures 

150 

151 

152class SequentialProjection(ProjectionMethod): 

153 """ 

154 Class to represent a sequential projection. 

155 

156 Parameters 

157 ---------- 

158 projections : List[Projection] 

159 A list of projection methods to be applied sequentially. 

160 relaxation : float, optional 

161 A relaxation parameter for the projection methods, by default 1. 

162 control_seq : None, numpy.typing.ArrayLike, or List[int], optional 

163 An optional sequence that determines the order in which the projections are applied. 

164 If None, the projections are applied in the order they are provided, by default None. 

165 proximity_flag : bool 

166 Flag to indicate whether to take this object into account when calculating proximity, by default True. 

167 

168 Attributes 

169 ---------- 

170 projections : List[Projection] 

171 The list of Projection objects used in the projection method. 

172 all_x : array-like or None 

173 Storage for all x values if storage is enabled during solve. 

174 relaxation : float 

175 Relaxation parameter for the projection. 

176 proximity_flag : bool 

177 Flag to indicate whether to take this object into account when calculating proximity. 

178 control_seq : npt.NDArray or List[int] 

179 The sequence in which the projections are applied. 

180 """ 

181 

182 def __init__( 

183 self, 

184 projections: List[Projection], 

185 relaxation: float = 1, 

186 control_seq: None | npt.NDArray | List[int] = None, 

187 proximity_flag=True, 

188 ): 

189 

190 # TODO: optional: assign order in which projections are applied 

191 super().__init__(projections, relaxation, proximity_flag) 

192 if control_seq is None: 

193 self.control_seq = np.arange(len(projections)) 

194 else: 

195 self.control_seq = control_seq 

196 

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

198 """ 

199 Sequentially projects the input array `x` using the control 

200 sequence. 

201 

202 Parameters 

203 ---------- 

204 x : npt.NDArray 

205 The input array to be projected. 

206 

207 Returns 

208 ------- 

209 npt.NDArray 

210 The projected array after applying all projection methods in the control sequence. 

211 """ 

212 

213 for i in self.control_seq: 

214 x = self.projections[i].project(x) 

215 return x 

216 

217 

218class SimultaneousProjection(ProjectionMethod): 

219 """ 

220 Class to represent a simultaneous projection. 

221 

222 Parameters 

223 ---------- 

224 projections : List[Projection] 

225 A list of projection methods to be applied. 

226 weights : npt.NDArray or None, optional 

227 An array of weights for each projection method. If None, equal weights 

228 are assigned to each projection. Weights are normalized to sum up to 1. Default is None. 

229 relaxation : float, optional 

230 A relaxation parameter for the projection methods. Default is 1. 

231 proximity_flag : bool, optional 

232 A flag indicating whether to use proximity in the projection methods. 

233 Default is True. 

234 

235 Attributes 

236 ---------- 

237 projections : List[Projection] 

238 The list of Projection objects used in the projection method. 

239 all_x : array-like or None 

240 Storage for all x values if storage is enabled during solve. 

241 relaxation : float 

242 Relaxation parameter for the projection. 

243 proximity_flag : bool 

244 Flag to indicate whether to take this object into account when calculating proximity. 

245 weights : npt.NDArray 

246 The weights assigned to each projection method. 

247 

248 Notes 

249 ----- 

250 While the simultaneous projection is performed simultaneously mathematically, the actual computation right now is sequential. 

251 """ 

252 

253 def __init__( 

254 self, 

255 projections: List[Projection], 

256 weights: npt.NDArray | None = None, 

257 relaxation: float = 1, 

258 proximity_flag=True, 

259 ): 

260 

261 super().__init__(projections, relaxation, proximity_flag) 

262 if weights is None: 

263 weights = np.ones(len(projections)) / len(projections) 

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

265 

266 def _project(self, x: float) -> float: 

267 """ 

268 Simultaneously projects the input array `x`. 

269 

270 Parameters 

271 ---------- 

272 x : npt.NDArray 

273 The input array to be projected. 

274 

275 Returns 

276 ------- 

277 npt.NDArray 

278 The projected array. 

279 """ 

280 x_new = 0 

281 for proj, weight in zip(self.projections, self.weights): 

282 x_new = x_new + weight * proj.project(x.copy()) 

283 return x_new 

284 

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

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

287 proxs = xp.array( 

288 [xp.array(proj.proximity(x, proximity_measures)) for proj in self.projections] 

289 ) 

290 measures = [] 

291 for i, measure in enumerate(proximity_measures): 

292 if isinstance(measure, tuple): 

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

294 measures.append(self.weights @ (proxs[:, i])) 

295 else: 

296 raise ValueError("Invalid proximity measure") 

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

298 measures.append(proxs[:, i].max()) 

299 else: 

300 raise ValueError("Invalid proximity measure") 

301 return measures 

302 

303 

304class StringAveragedProjection(ProjectionMethod): 

305 """ 

306 Class to represent a string averaged projection. 

307 

308 Parameters 

309 ---------- 

310 projections : List[Projection] 

311 A list of projection methods to be applied. 

312 strings : List[List] 

313 A list of strings, where each string is a list of indices of the projection methods to be applied. 

314 weights : npt.NDArray or None, optional 

315 An array of weights for each strings. If None, equal weights 

316 are assigned to each string. Weights are normalized to sum up to 1. Default is None. 

317 relaxation : float, optional 

318 A relaxation parameter for the projection methods. Default is 1. 

319 proximity_flag : bool, optional 

320 A flag indicating whether to use proximity in the projection methods. 

321 Default is True. 

322 

323 Attributes 

324 ---------- 

325 projections : List[Projection] 

326 The list of Projection objects used in the projection method. 

327 all_x : array-like or None 

328 Storage for all x values if storage is enabled during solve. 

329 relaxation : float 

330 Relaxation parameter for the projection. 

331 proximity_flag : bool 

332 Flag to indicate whether to take this object into account when calculating proximity. 

333 strings : List[List] 

334 A list of strings, where each string is a list of indices of the projection methods to be applied. 

335 weights : npt.NDArray 

336 The weights assigned to each projection method. 

337 

338 Notes 

339 ----- 

340 While the string projections are performed simultaneously mathematically, the actual computation right now is sequential. 

341 """ 

342 

343 def __init__( 

344 self, 

345 projections: List[Projection], 

346 strings: List[List], 

347 weights: npt.NDArray | None = None, 

348 relaxation: float = 1, 

349 proximity_flag=True, 

350 ): 

351 

352 super().__init__(projections, relaxation, proximity_flag) 

353 if weights is None: 

354 weights = np.ones(len(strings)) / len(strings) # assign uniform weights 

355 else: 

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

357 self.strings = strings 

358 

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

360 """ 

361 String averaged projection of the input array `x`. 

362 

363 Parameters 

364 ---------- 

365 x : npt.NDArray 

366 The input array to be projected. 

367 

368 Returns 

369 ------- 

370 npt.NDArray 

371 The projected array after applying all projection methods in the control sequence. 

372 """ 

373 x_new = 0 

374 # TODO: Can this be parallelized? 

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

376 # run over all individual strings 

377 x_s = x.copy() # create a copy for 

378 for el in string: # run over all elements in the string sequentially 

379 x_s = self.projections[el].project(x_s) 

380 x_new += weight * x_s 

381 return x_new 

382 

383 

384class BlockIterativeProjection(ProjectionMethod): 

385 """ 

386 Class to represent a block iterative projection. 

387 

388 Parameters 

389 ---------- 

390 projections : List[Projection] 

391 A list of projection methods to be applied. 

392 weights : List[List[float]] | List[npt.NDArray] 

393 A List of weights for each block of projection methods. 

394 relaxation : float, optional 

395 A relaxation parameter for the projection methods. Default is 1. 

396 proximity_flag : bool, optional 

397 A flag indicating whether to use proximity in the projection methods. 

398 Default is True. 

399 

400 Attributes 

401 ---------- 

402 projections : List[Projection] 

403 The list of Projection objects used in the projection method. 

404 all_x : array-like or None 

405 Storage for all x values if storage is enabled during solve. 

406 relaxation : float 

407 Relaxation parameter for the projection. 

408 proximity_flag : bool 

409 Flag to indicate whether to take this object into account when calculating proximity. 

410 weights : List[npt.NDArray] 

411 The weights assigned to each block of projection methods. 

412 

413 Notes 

414 ----- 

415 While the individual block projections are performed simultaneously mathematically, the actual computation right now is sequential. 

416 """ 

417 

418 def __init__( 

419 self, 

420 projections: List[Projection], 

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

422 relaxation: float = 1, 

423 proximity_flag=True, 

424 ): 

425 

426 super().__init__(projections, relaxation, proximity_flag) 

427 xp = cp if self._use_gpu else np 

428 # check if weights has the correct format 

429 for el in weights: 

430 if len(el) != len(projections): 

431 raise ValueError("Weights do not match the number of projections!") 

432 

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

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

435 

436 self.weights = [] 

437 self.block_idxs = [ 

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

439 ] # get idxs that meet requirements 

440 

441 # assemble a list of general weights 

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

443 for el in weights: 

444 el = xp.asarray(el) 

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

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

447 

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

449 # TODO: Can this be parallelized? 

450 for weight, block_idx in zip(self.weights, self.block_idxs): 

451 x_new = 0 # for simultaneous projection, later replaces x 

452 

453 i = 0 

454 for el in block_idx: 

455 x_new += weight[i] * self.projections[el].project(x.copy()) 

456 i += 1 

457 x = x_new 

458 return x 

459 

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

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

462 proxs = xp.array( 

463 [xp.array(proj.proximity(x, proximity_measures)) for proj in self.projections] 

464 ) 

465 measures = [] 

466 for i, measure in enumerate(proximity_measures): 

467 if isinstance(measure, tuple): 

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

469 measures.append(self.total_weights @ (proxs[:, i])) 

470 else: 

471 raise ValueError("Invalid proximity measure") 

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

473 measures.append(proxs[:, i].max()) 

474 else: 

475 raise ValueError("Invalid proximity measure") 

476 return measures 

477 

478 

479class MultiBallProjection(BasicProjection, ABC): 

480 """Projection onto multiple balls.""" 

481 

482 def __init__( 

483 self, 

484 centers: npt.NDArray, 

485 radii: npt.NDArray, 

486 relaxation: float = 1, 

487 idx: npt.NDArray | None = None, 

488 proximity_flag=True, 

489 ): 

490 try: 

491 if isinstance(centers, cp.ndarray) and isinstance(radii, cp.ndarray): 

492 _use_gpu = True 

493 elif (isinstance(centers, cp.ndarray)) != (isinstance(radii, cp.ndarray)): 

494 raise ValueError("Mismatch between input types of centers and radii") 

495 else: 

496 _use_gpu = False 

497 except ModuleNotFoundError: 

498 _use_gpu = False 

499 

500 super().__init__(relaxation, idx, proximity_flag, _use_gpu) 

501 self.centers = centers 

502 self.radii = radii 

503 

504 

505class SequentialMultiBallProjection(MultiBallProjection): 

506 """Sequential projection onto multiple balls.""" 

507 

508 # def __init__(self, 

509 # centers: npt.NDArray, 

510 # radii: npt.NDArray, 

511 # relaxation:float = 1, 

512 # idx: npt.NDArray | None = None): 

513 

514 # super().__init__(centers, radii, relaxation,idx) 

515 

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

517 

518 for i in range(len(self.centers)): 

519 if np.linalg.norm(x[self.idx] - self.centers[i]) > self.radii[i]: 

520 x[self.idx] = self.centers[i] + self.radii[i] * ( 

521 x[self.idx] - self.centers[i] 

522 ) / np.linalg.norm(x[self.idx] - self.centers[i]) 

523 return x 

524 

525 

526class SimultaneousMultiBallProjection(MultiBallProjection): 

527 """Simultaneous projection onto multiple balls.""" 

528 

529 def __init__( 

530 self, 

531 centers: npt.NDArray, 

532 radii: npt.NDArray, 

533 weights: npt.NDArray, 

534 relaxation: float = 1, 

535 idx: npt.NDArray | None = None, 

536 proximity_flag=True, 

537 ): 

538 

539 super().__init__(centers, radii, relaxation, idx, proximity_flag) 

540 self.weights = weights 

541 

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

543 # get all indices 

544 dists = np.linalg.norm(x[self.idx] - self.centers, axis=1) 

545 idx = (dists - self.radii) > 0 

546 # project onto halfspaces 

547 x[self.idx] = x[self.idx] - (self.weights[idx] * (1 - self.radii[idx] / dists[idx])) @ ( 

548 x[self.idx] - self.centers[idx] 

549 ) 

550 return x