Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\feasibility\_split_algorithms.py: 70%

1"""Algorithms for split feasibility problem."""

2 from abc import ABC, abstractmethod

3 from typing import List

4 import numpy as np

5 import numpy.typing as npt

6 from scipy import sparse

7

8 try:

9 import cupy as cp

10 except ImportError:

11 cp = np

12

13 from suppy.utils import LinearMapping

14 from suppy.utils import ensure_float_array

15 from suppy.projections._projections import Projection

16

17# from ._algorithms import Feasibility

18 from suppy.feasibility._linear_algorithms import Feasibility

19

20

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

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

327 class 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.")