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

1"""

2 General implementation for sequential, simultaneous, block iterative and

3 string averaged projection methods.

4"""

5 from abc import ABC

6 from typing import List

7 import numpy as np

8 import numpy.typing as npt

9

10 try:

11 import cupy as cp

12

13 NO_GPU = False

14 except ImportError:

15 cp = np

16 NO_GPU = True

17

18 from suppy.projections._projections import Projection, BasicProjection

19 from suppy.utils import ensure_float_array

20

21

22 class 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

152 class 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

218 class 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

304 class 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

384 class 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

479 class 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

505 class 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

526 class 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