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

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 

7 

8try: 

9 import cupy as cp 

10except ImportError: 

11 cp = np 

12 

13from suppy.utils import LinearMapping 

14from suppy.utils import ensure_float_array 

15from suppy.projections._projections import Projection 

16 

17# from ._algorithms import Feasibility 

18from suppy.feasibility._linear_algorithms import Feasibility 

19 

20 

21class SplitFeasibility(Feasibility, ABC): 

22 """ 

23 Abstract base class used to represent split feasibility problems. 

24 

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. 

33 

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 """ 

47 

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 ): 

55 

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 

61 

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. 

73 

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. 

88 

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 

100 

101 if storage is True: 

102 self.all_x = [] 

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

104 

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)) 

110 

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

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

113 

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 

119 

120 def project(self, x: npt.NDArray, y: npt.NDArray | None = None) -> npt.NDArray: 

121 """ 

122 Projects the input array onto the feasible set. 

123 

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). 

130 

131 Returns 

132 ------- 

133 npt.NDArray 

134 The projected array. 

135 """ 

136 

137 return self._project(x, y) 

138 

139 @abstractmethod 

140 def _project(self, x: npt.NDArray, y: npt.NDArray | None = None) -> npt.NDArray: 

141 pass 

142 

143 def map(self, x: npt.NDArray) -> npt.NDArray: 

144 """ 

145 Maps the input space array to the target space via matrix 

146 multiplication. 

147 

148 Parameters 

149 ---------- 

150 x : npt.NDArray 

151 The input space array to be map. 

152 

153 Returns 

154 ------- 

155 npt.NDArray 

156 The corresponding target space array. 

157 """ 

158 

159 return self.A @ x 

160 

161 def map_back(self, y: npt.NDArray) -> npt.NDArray: 

162 """ 

163 Transposed map of the target space array to the input space. 

164 

165 Parameters 

166 ---------- 

167 y : npt.NDArray 

168 The target space array to map. 

169 

170 Returns 

171 ------- 

172 npt.NDArray 

173 The corresponding array in input space. 

174 """ 

175 

176 return self.A.T @ y 

177 

178 

179class CQAlgorithm(SplitFeasibility): 

180 """ 

181 Implementation for the CQ algorithm to solve split feasibility problems. 

182 

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. 

197 

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 """ 

215 

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 ): 

225 

226 super().__init__(A, algorithmic_relaxation, proximity_flag, use_gpu) 

227 self.c_projection = C_projection 

228 self.q_projection = Q_projection 

229 

230 def _project(self, x: npt.NDArray, y: npt.NDArray | None = None) -> npt.NDArray: 

231 """ 

232 Perform one step of the CQ algorithm. 

233 

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. 

242 

243 Returns 

244 ------- 

245 npt.NDArray 

246 """ 

247 if y is None: 

248 y = self.map(x) 

249 

250 y_p = self.q_projection.project(y.copy()) 

251 x = x - self.algorithmic_relaxation * self.map_back(y - y_p) 

252 

253 return self.c_projection.project(x), y_p 

254 

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? 

259 

260 

261# class LinearExtrapolatedLandweber(SplitFeasibility): 

262# """ 

263# Implementation for a linear extrapolated Landweber algorithm to solve split feasibility problems. 

264 

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# """ 

278 

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# ): 

287 

288# super().__init__(A, algorithmic_relaxation, proximity_flag) 

289# self.bounds = Bounds(lb, ub) 

290 

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. 

294 

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) 

305 

306# x -= self.algorithmic_relaxation * 

307 

308 

309# def _proximity(self, x: npt.NDArray) -> float: 

310# """ 

311# Calculate the proximity of a point to the set Q. 

312 

313# Parameters 

314# ---------- 

315# x : npt.NDArray 

316# The point in the input space. 

317 

318# Returns 

319# ------- 

320# float 

321# The proximity measure. 

322# """ 

323# p = self.map(x) 

324# return self.q_projection.proximity(p) 

325 

326 

327class ProductSpaceAlgorithm(SplitFeasibility): 

328 

329 """ 

330 Implementation for a product space algorithm to solve split feasibility 

331 problems. 

332 

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 """ 

346 

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 ): 

355 

356 super().__init__(A, algorithmic_relaxation, proximity_flag) 

357 self.c_projections = C_projections 

358 self.q_projections = Q_projections 

359 

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) 

363 

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 = [] 

369 

370 def _project(self, x: npt.NDArray, y: npt.NDArray | None = None) -> npt.NDArray: 

371 """ 

372 Perform one step of the product space algorithm. 

373 

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. 

381 

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):] 

396 

397 def _proximity(self, x): 

398 raise NotImplementedError("Proximity not implemented for ProductSpaceAlgorithm.")