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
« 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
10try:
11 import cupy as cp
13 NO_GPU = False
14except ImportError:
15 cp = np
16 NO_GPU = True
18from suppy.projections._projections import Projection, BasicProjection
19from suppy.utils import ensure_float_array
22class ProjectionMethod(Projection, ABC):
23 """
24 A class used to represent methods for projecting a point onto multiple
25 sets.
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.
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 """
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 = []
60 def visualize(self, ax):
61 """
62 Visualizes all projection objects (if applicable) on the given
63 matplotlib axis.
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)
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.
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.
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
110 self.proximities = []
111 i = 0
112 feasible = False
114 if storage is True:
115 self.all_x = []
116 self.all_x.append(x.copy())
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))
124 # TODO: If proximity changes x some potential issues!
125 if self.proximities[-1][0] < constr_tol:
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
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
152class SequentialProjection(ProjectionMethod):
153 """
154 Class to represent a sequential projection.
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.
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 """
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 ):
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
197 def _project(self, x: npt.NDArray) -> npt.NDArray:
198 """
199 Sequentially projects the input array `x` using the control
200 sequence.
202 Parameters
203 ----------
204 x : npt.NDArray
205 The input array to be projected.
207 Returns
208 -------
209 npt.NDArray
210 The projected array after applying all projection methods in the control sequence.
211 """
213 for i in self.control_seq:
214 x = self.projections[i].project(x)
215 return x
218class SimultaneousProjection(ProjectionMethod):
219 """
220 Class to represent a simultaneous projection.
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.
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.
248 Notes
249 -----
250 While the simultaneous projection is performed simultaneously mathematically, the actual computation right now is sequential.
251 """
253 def __init__(
254 self,
255 projections: List[Projection],
256 weights: npt.NDArray | None = None,
257 relaxation: float = 1,
258 proximity_flag=True,
259 ):
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()
266 def _project(self, x: float) -> float:
267 """
268 Simultaneously projects the input array `x`.
270 Parameters
271 ----------
272 x : npt.NDArray
273 The input array to be projected.
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
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
304class StringAveragedProjection(ProjectionMethod):
305 """
306 Class to represent a string averaged projection.
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.
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.
338 Notes
339 -----
340 While the string projections are performed simultaneously mathematically, the actual computation right now is sequential.
341 """
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 ):
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
359 def _project(self, x: npt.NDArray) -> npt.NDArray:
360 """
361 String averaged projection of the input array `x`.
363 Parameters
364 ----------
365 x : npt.NDArray
366 The input array to be projected.
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
384class BlockIterativeProjection(ProjectionMethod):
385 """
386 Class to represent a block iterative projection.
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.
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.
413 Notes
414 -----
415 While the individual block projections are performed simultaneously mathematically, the actual computation right now is sequential.
416 """
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 ):
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!")
433 if abs((el.sum() - 1)) > 1e-10:
434 raise ValueError("Weights do not add up to 1!")
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
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)
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
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
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
479class MultiBallProjection(BasicProjection, ABC):
480 """Projection onto multiple balls."""
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
500 super().__init__(relaxation, idx, proximity_flag, _use_gpu)
501 self.centers = centers
502 self.radii = radii
505class SequentialMultiBallProjection(MultiBallProjection):
506 """Sequential projection onto multiple balls."""
508 # def __init__(self,
509 # centers: npt.NDArray,
510 # radii: npt.NDArray,
511 # relaxation:float = 1,
512 # idx: npt.NDArray | None = None):
514 # super().__init__(centers, radii, relaxation,idx)
516 def _project(self, x: npt.NDArray) -> npt.NDArray:
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
526class SimultaneousMultiBallProjection(MultiBallProjection):
527 """Simultaneous projection onto multiple balls."""
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 ):
539 super().__init__(centers, radii, relaxation, idx, proximity_flag)
540 self.weights = weights
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