Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\feasibility\_split_algorithms.py: 70%
88 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"""Algorithms for split feasibility problem."""
2from abc import ABC, abstractmethod
3from typing import List
4import numpy as np
5import numpy.typing as npt
6from scipy import sparse
8try:
9 import cupy as cp
10except ImportError:
11 cp = np
13from suppy.utils import LinearMapping
14from suppy.utils import ensure_float_array
15from suppy.projections._projections import Projection
17# from ._algorithms import Feasibility
18from suppy.feasibility._linear_algorithms import Feasibility
21class SplitFeasibility(Feasibility, ABC):
22 """
23 Abstract base class used to represent split feasibility problems.
25 Parameters
26 ----------
27 A : npt.NDArray
28 Matrix connecting input and target space.
29 algorithmic_relaxation : npt.NDArray or float, optional
30 Relaxation applied to the entire solution of the projection step, by default 1.
31 proximity_flag : bool, optional
32 A flag indicating whether to use this object for proximity calculations, by default True.
34 Attributes
35 ----------
36 A : LinearMapping
37 Linear mapping between input and target space.
38 proximities : list
39 A list to store proximity values during the solve process.
40 algorithmic_relaxation : float
41 Relaxation applied to the entire solution of the projection step.
42 proximity_flag : bool, optional
43 A flag indicating whether to use this object for proximity calculations.
44 relaxation : float, optional
45 The relaxation parameter for the projection, by default 1.0.
46 """
48 def __init__(
49 self,
50 A: npt.NDArray | sparse.sparray,
51 algorithmic_relaxation: npt.NDArray | float = 1.0,
52 proximity_flag: bool = True,
53 _use_gpu: bool = False,
54 ):
56 _, _use_gpu = LinearMapping.get_flags(A)
57 super().__init__(algorithmic_relaxation, 1, proximity_flag, _use_gpu=_use_gpu)
58 self.A = LinearMapping(A)
59 self.proximities = []
60 self.all_x = None
62 @ensure_float_array
63 def solve(
64 self,
65 x: npt.NDArray,
66 max_iter: int = 10,
67 constr_tol: float = 1e-6,
68 storage: bool = False,
69 proximity_measures: List | None = None,
70 ) -> npt.NDArray:
71 """
72 Solves the split feasibility problem for a given input array.
74 Parameters
75 ----------
76 x : npt.NDArray
77 Starting point for the algorithm.
78 max_iter : int, optional
79 The maximum number of iterations (default is 10).
80 constr_tol : float, optional
81 Stopping criterium for the feasibility seeking algorithm.
82 Solution deemed feasible if the proximity drops below this value (default is 1e-6).
83 storage : bool, optional
84 A flag indicating whether to store all intermediate solutions (default is False).
85 proximity_measures : List, optional
86 The proximity measures to calculate, by default None.
87 Right now only the first in the list is used to check the feasibility.
89 Returns
90 -------
91 npt.NDArray
92 The solution after applying the feasibility seeking algorithm.
93 """
94 if proximity_measures is None:
95 proximity_measures = [("p_norm", 2)]
96 xp = cp if isinstance(x, cp.ndarray) else np
97 self.proximities = [self.proximity(x, proximity_measures)]
98 i = 0
99 feasible = False
101 if storage is True:
102 self.all_x = []
103 self.all_x.append(x.copy())
105 while i < max_iter and not feasible:
106 x, _ = self.step(x)
107 if storage is True:
108 self.all_x.append(x.copy())
109 self.proximities.append(self.proximity(x, proximity_measures))
111 # TODO: If proximity changes x some potential issues!
112 if self.proximities[-1][0] < constr_tol:
114 feasible = True
115 i += 1
116 if self.all_x is not None:
117 self.all_x = xp.array(self.all_x)
118 return x
120 def project(self, x: npt.NDArray, y: npt.NDArray | None = None) -> npt.NDArray:
121 """
122 Projects the input array onto the feasible set.
124 Parameters
125 ----------
126 x : npt.NDArray
127 The input array to project.
128 y : npt.NDArray, optional
129 An optional array for projection (default is None).
131 Returns
132 -------
133 npt.NDArray
134 The projected array.
135 """
137 return self._project(x, y)
139 @abstractmethod
140 def _project(self, x: npt.NDArray, y: npt.NDArray | None = None) -> npt.NDArray:
141 pass
143 def map(self, x: npt.NDArray) -> npt.NDArray:
144 """
145 Maps the input space array to the target space via matrix
146 multiplication.
148 Parameters
149 ----------
150 x : npt.NDArray
151 The input space array to be map.
153 Returns
154 -------
155 npt.NDArray
156 The corresponding target space array.
157 """
159 return self.A @ x
161 def map_back(self, y: npt.NDArray) -> npt.NDArray:
162 """
163 Transposed map of the target space array to the input space.
165 Parameters
166 ----------
167 y : npt.NDArray
168 The target space array to map.
170 Returns
171 -------
172 npt.NDArray
173 The corresponding array in input space.
174 """
176 return self.A.T @ y
179class CQAlgorithm(SplitFeasibility):
180 """
181 Implementation for the CQ algorithm to solve split feasibility problems.
183 Parameters
184 ----------
185 A : npt.NDArray
186 Matrix connecting input and target space.
187 C_projection : Projection
188 The projection operator onto the set C.
189 Q_projection : Projection
190 The projection operator onto the set Q.
191 algorithmic_relaxation : npt.NDArray or float, optional
192 Relaxation applied to the entire solution of the projection step, by default 1.
193 proximity_flag : bool, optional
194 A flag indicating whether to use this object for proximity calculations, by default True.
195 use_gpu : bool, optional
196 A flag indicating whether to use GPU for computations, by default False.
198 Attributes
199 ----------
200 A : LinearMapping
201 Linear mapping between input and target space.
202 C_projection : Projection
203 The projection operator onto the set C.
204 Q_projection : Projection
205 The projection operator onto the set Q.
206 proximities : list
207 A list to store proximity values during the solve process.
208 algorithmic_relaxation : float
209 Relaxation applied to the entire solution of the projection step.
210 proximity_flag : bool
211 A flag indicating whether to use this object for proximity calculations.
212 relaxation : float, optional
213 The relaxation parameter for the projection, by default 1.0.
214 """
216 def __init__(
217 self,
218 A: npt.NDArray | sparse.sparray,
219 C_projection: Projection,
220 Q_projection: Projection,
221 algorithmic_relaxation: float = 1,
222 proximity_flag=True,
223 use_gpu=False,
224 ):
226 super().__init__(A, algorithmic_relaxation, proximity_flag, use_gpu)
227 self.c_projection = C_projection
228 self.q_projection = Q_projection
230 def _project(self, x: npt.NDArray, y: npt.NDArray | None = None) -> npt.NDArray:
231 """
232 Perform one step of the CQ algorithm.
234 Parameters
235 ----------
236 x : npt.NDArray
237 The point in the input space to be projected.
238 y : npt.NDArray or None, optional
239 The point in the target space to be projected,
240 obtained through e.g. a perturbation step.
241 If None, it is calculated from x.
243 Returns
244 -------
245 npt.NDArray
246 """
247 if y is None:
248 y = self.map(x)
250 y_p = self.q_projection.project(y.copy())
251 x = x - self.algorithmic_relaxation * self.map_back(y - y_p)
253 return self.c_projection.project(x), y_p
255 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> float:
256 p = self.map(x)
257 return self.q_projection.proximity(p, proximity_measures)
258 # TODO: correct?
261# class LinearExtrapolatedLandweber(SplitFeasibility):
262# """
263# Implementation for a linear extrapolated Landweber algorithm to solve split feasibility problems.
265# Parameters
266# ----------
267# A : npt.NDArray
268# Matrix connecting input and target space.
269# lb: npt.NDArray
270# Lower bounds for the target space.
271# ub: npt.NDArray
272# Upper bounds for the target space.
273# algorithmic_relaxation : npt.NDArray or float, optional
274# Relaxation applied to the entire solution of the projection step, by default 1.
275# proximity_flag : bool, optional
276# A flag indicating whether to use this object for proximity calculations, by default True.
277# """
279# def __init__(
280# self,
281# A: npt.NDArray | sparse.sparray,
282# lb: npt.NDArray,
283# ub: npt.NDArray,
284# algorithmic_relaxation: npt.NDArray | float = 1,
285# proximity_flag=True,
286# ):
288# super().__init__(A, algorithmic_relaxation, proximity_flag)
289# self.bounds = Bounds(lb, ub)
291# def _project(self, x: npt.NDArray, y: npt.NDArray | None = None) -> npt.NDArray:
292# """
293# Perform one step of the linear extrapolated Landweber algorithm.
295# Parameters
296# ----------
297# x : npt.NDArray
298# The point in the input space to be projected.
299# Returns
300# -------
301# npt.NDArray
302# """
303# p = self.map(x)
304# (res_u, res_l) = self.bounds.residual(p)
306# x -= self.algorithmic_relaxation *
309# def _proximity(self, x: npt.NDArray) -> float:
310# """
311# Calculate the proximity of a point to the set Q.
313# Parameters
314# ----------
315# x : npt.NDArray
316# The point in the input space.
318# Returns
319# -------
320# float
321# The proximity measure.
322# """
323# p = self.map(x)
324# return self.q_projection.proximity(p)
327class ProductSpaceAlgorithm(SplitFeasibility):
329 """
330 Implementation for a product space algorithm to solve split feasibility
331 problems.
333 Parameters
334 ----------
335 A : npt.NDArray
336 Matrix connecting input and target space.
337 C_projection : Projection
338 The projection operator onto the set C.
339 Q_projection : Projection
340 The projection operator onto the set Q.
341 algorithmic_relaxation : npt.NDArray or float, optional
342 Relaxation applied to the entire solution of the projection step, by default 1.
343 proximity_flag : bool, optional
344 A flag indicating whether to use this object for proximity calculations, by default True.
345 """
347 def __init__(
348 self,
349 A: npt.NDArray | sparse.sparray,
350 C_projections: List[Projection],
351 Q_projections: List[Projection],
352 algorithmic_relaxation: npt.NDArray | float = 1,
353 proximity_flag=True,
354 ):
356 super().__init__(A, algorithmic_relaxation, proximity_flag)
357 self.c_projections = C_projections
358 self.q_projections = Q_projections
360 # calculate projection back into Ax=b space
361 Z = np.concatenate([A, -1 * np.eye(A.shape[0])], axis=1)
362 self.Pv = np.eye(Z.shape[1]) - LinearMapping(Z.T @ (np.linalg.inv(Z @ Z.T)) @ Z)
364 print(
365 "Warning! This algorithm is only suitable for small scale problems. Use the CQAlgorithm for larger problems."
366 )
367 self.xs = []
368 self.ys = []
370 def _project(self, x: npt.NDArray, y: npt.NDArray | None = None) -> npt.NDArray:
371 """
372 Perform one step of the product space algorithm.
374 Parameters
375 ----------
376 x : npt.NDArray
377 The point in the input space to be projected.
378 y : npt.NDArray or None, optional
379 The point in the target space to be projected, obtained through e.g. a perturbation step.
380 If None, it is calculated from x.
382 Returns
383 -------
384 npt.NDArray
385 """
386 if y is None:
387 y = self.map(x)
388 for el in self.c_projections:
389 x = el.project(x)
390 for el in self.q_projections:
391 y = el.project(y)
392 xy = self.Pv @ np.concatenate([x, y])
393 self.xs.append(xy[: len(x)].copy())
394 self.ys.append(xy[len(x) :].copy())
395 return xy[: len(x)] # ,xy[len(x):]
397 def _proximity(self, x):
398 raise NotImplementedError("Proximity not implemented for ProductSpaceAlgorithm.")