Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\feasibility\_hyperplanes\_ams_algorithms.py: 75%

1 from abc import ABC

2 from typing import List

3 import numpy as np

4 import numpy.typing as npt

5

6 try:

7 import cupy as cp

8

9 NO_GPU = False

10

11 except ImportError:

12 NO_GPU = True

13 cp = np

14

15 from suppy.feasibility._linear_algorithms import HyperplaneFeasibility

16 from suppy.utils import LinearMapping

17

18

19 class HyperplaneAMSAlgorithm(HyperplaneFeasibility, ABC):

20 """

21 The HyperplaneAMSAlgorithm class is used to find a feasible solution to

22 a

23 set of linear inequalities.

24

25 Parameters

26 ----------

27 A : npt.NDArray

28 The matrix representing the coefficients of the linear inequalities.

29 b : npt.NDArray

30 Bound for linear inequalities

31 algorithmic_relaxation : npt.NDArray or float, optional

32 The relaxation parameter for the algorithm, by default 1.

33 relaxation : float, optional

34 The relaxation parameter for the feasibility problem, by default 1.

35 proximity_flag : bool, optional

36 A flag indicating whether to use proximity in the algorithm, by default True.

37 """

38

39 def __init__(

40 self,

41 A: npt.NDArray,

42 b: npt.NDArray,

43 algorithmic_relaxation: npt.NDArray | float = 1,

44 relaxation: float = 1,

45 proximity_flag: bool = True,

46 ):

47 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag)

48

49

50 class SequentialAMSHyperplane(HyperplaneAMSAlgorithm):

51 """

52 SequentialAMS class for sequentially applying the AMS algorithm.

53

54 Parameters

55 ----------

56 A : npt.NDArray

57 The matrix A used in the AMS algorithm.

58 b : npt.NDArray

59 Bound for linear inequalities

60 algorithmic_relaxation : npt.NDArray or float, optional

61 The relaxation parameter for the algorithm, by default 1.

62 relaxation : float, optional

63 The relaxation parameter, by default 1.

64 cs : None or List[int], optional

65 The list of indices for the constraints, by default None.

66 proximity_flag : bool, optional

67 Flag to indicate if proximity should be considered, by default True.

68

69 Attributes

70 ----------

71 """

72

73 def __init__(

74 self,

75 A: npt.NDArray,

76 b: npt.NDArray,

77 algorithmic_relaxation: npt.NDArray | float = 1,

78 relaxation: float = 1,

79 cs: None | List[int] = None,

80 proximity_flag: bool = True,

81 ):

82

83 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag)

84 xp = cp if self._use_gpu else np

85 if cs is None:

86 self.cs = xp.arange(self.A.shape[0])

87 else:

88 self.cs = cs

89

90 def _project(self, x: npt.NDArray) -> npt.NDArray:

91 """

92 Projects the input array `x` onto the feasible region defined by the

93 constraints.

94

95 Parameters

96 ----------

97 x : npt.NDArray

98 The input array to be projected.

99

100 Returns

101 -------

102 npt.NDArray

103 The projected array.

104 """

105

106 for i in self.cs:

107 p_i = self.single_map(x, i)

108 res = self.b[i] - p_i

109 self.A.update_step(x, self.algorithmic_relaxation * self.inverse_row_norm[i] * res, i)

110 return x

111

112

113 class SequentialWeightedAMSHyperplane(SequentialAMSHyperplane):

114 """

115 Parameters

116 ----------

117 A : npt.NDArray

118 The constraint matrix.

119 b : npt.NDArray

120 Bound for linear inequalities

121 weights : None, list of float, or npt.NDArray, optional

122 The weights assigned to each constraint. If None, default weights are

123 used.

124 algorithmic_relaxation : npt.NDArray or float, optional

125 The relaxation parameter for the algorithm. Default is 1.

126 relaxation : float, optional

127 The relaxation parameter for the algorithm. Default is 1.

128 weight_decay : float, optional

129 Parameter that determines the rate at which the weights are reduced

130 after each phase (weights * weight_decay). Default is 1.

131 cs : None or list of int, optional

132 The indices of the constraints to be considered. Default is None.

133 proximity_flag : bool, optional

134 Flag to indicate if proximity should be considered. Default is True.

135

136 Attributes

137 ----------

138 weights : npt.NDArray

139 The weights assigned to each constraint.

140 weight_decay : float

141 Decay rate for the weights.

142 temp_weight_decay : float

143 Initial value for weight decay.

144 """

145

146 def __init__(

147 self,

148 A: npt.NDArray,

149 b: npt.NDArray,

150 weights: None | List[float] | npt.NDArray = None,

151 algorithmic_relaxation: npt.NDArray | float = 1,

152 relaxation: float = 1,

153 weight_decay: float = 1,

154 cs: None | List[int] = None,

155 proximity_flag: bool = True,

156 ):

157

158 super().__init__(A, b, algorithmic_relaxation, relaxation, cs, proximity_flag)

159 xp = cp if self._use_gpu else np

160 self.weight_decay = weight_decay # decay rate

161 self.temp_weight_decay = 1 # initial value for weight decay

162

163 if weights is None:

164 self.weights = xp.ones(self.A.shape[0])

165 elif xp.abs((weights.sum() - 1)) > 1e-10:

166 print("Weights do not add up to 1! Renormalizing to 1...")

167 self.weights = weights

168

169 def _project(self, x: npt.NDArray) -> npt.NDArray:

170 """

171 Projects the input array `x` onto a feasible region defined by the

172 constraints.

173

174 Parameters

175 ----------

176 x : npt.NDArray

177 The input array to be projected.

178

179 Returns

180 -------

181 npt.NDArray

182 The projected array.

183

184 Notes

185 -----

186 This method iteratively adjusts the input array `x` based on the constraints

187 defined in `self.cs`. For each constraint, it computes the projection and

188 checks if the constraints are violated. If a constraint is violated, it updates

189 the array `x` using a weighted relaxation factor. The weight decay is applied

190 to the temporary weight decay after each iteration.

191 """

192

193 weighted_relaxation = self.algorithmic_relaxation * self.temp_weight_decay

194

195 for i in self.cs:

196 p_i = self.single_map(x, i)

197 res = self.b[i] - p_i

198 self.A.update_step(

199 x, weighted_relaxation * self.weights[i] * self.inverse_row_norm[i] * res, i

200 )

201

202 self.temp_weight_decay *= self.weight_decay

203 return x

204

205

206 class SimultaneousAMSHyperplane(HyperplaneAMSAlgorithm):

207 """

208 SimultaneousAMS is an implementation of the AMS (Alternating

209 Minimization Scheme) algorithm

210 that performs simultaneous projections and proximity calculations.

211

212 Parameters

213 ----------

214 A : npt.NDArray

215 The matrix representing the constraints.

216 b : npt.NDArray

217 Bound for linear inequalities

218 algorithmic_relaxation : npt.NDArray or float, optional

219 The relaxation parameter for the algorithm, by default 1.

220 relaxation : float, optional

221 The relaxation parameter for the projections, by default 1.

222 weights : None or List[float], optional

223 The weights for the constraints, by default None.

224 proximity_flag : bool, optional

225 Flag to indicate if proximity calculations should be performed, by default True.

226 """

227

228 def __init__(

229 self,

230 A: npt.NDArray,

231 b: npt.NDArray,

232 algorithmic_relaxation: npt.NDArray | float = 1,

233 relaxation: float = 1,

234 weights: None | List[float] = None,

235 proximity_flag: bool = True,

236 ):

237

238 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag)

239

240 xp = cp if self._use_gpu else np

241

242 if weights is None:

243 self.weights = xp.ones(self.A.shape[0]) / self.A.shape[0]

244 elif xp.abs((weights.sum() - 1)) > 1e-10:

245 print("Weights do not add up to 1! Renormalizing to 1...")

246 self.weights = weights / weights.sum()

247 else:

248 self.weights = weights

249

250 def _project(self, x):

251 # simultaneous projection

252 p = self.map(x)

253 res = self.b - p

254 x += self.algorithmic_relaxation * (self.weights * self.inverse_row_norm * res @ self.A)

255 return x

256

257 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> float:

258 p = self.map(x)

259 # residuals are positive if constraints are met

260 res = abs(self.b - p)

261 measures = []

262 for measure in proximity_measures:

263 if isinstance(measure, tuple):

264 if measure[0] == "p_norm":

265 measures.append(self.weights @ (res ** measure[1]))

266 else:

267 raise ValueError("Invalid proximity measure")

268 elif isinstance(measure, str) and measure == "max_norm":

269 measures.append(res.max())

270 else:

271 raise ValueError("Invalid proximity measure")

272 return measures

273

274

275 class ExtrapolatedLandweberHyperplane(SimultaneousAMSHyperplane):

276 def __init__(

277 self, A, b, algorithmic_relaxation=1, relaxation=1, weights=None, proximity_flag=True

278 ):

279 super().__init__(A, b, algorithmic_relaxation, relaxation, weights, proximity_flag)

280 self.a_i = self.A.row_norm(2, 2)

281 self.weight_norm = self.weights / self.a_i

282 self.sigmas = []

283

284 def _project(self, x):

285 p = self.map(x)

286 res = self.b - p

287 res_idx = res != 0

288 if not (np.any(res_idx)):

289 self.sigmas.append(0)

290 return x

291 t = self.weight_norm * res

292 t_2 = t @ self.A

293 sig = (res @ t) / (t_2 @ t_2)

294 self.sigmas.append(sig)

295 x += sig * t_2

296

297 return x

298

299

300 class BlockIterativeAMSHyperplane(HyperplaneAMSAlgorithm):

301 """

302 Block Iterative AMS Algorithm.

303 This class implements a block iterative version of the AMS (Alternating

304 Minimization Scheme) algorithm.

305 It is designed to handle constraints and weights in a block-wise manner.

306

307 Parameters

308 ----------

309 A : npt.NDArray

310 The matrix representing the linear constraints.

311 b : npt.NDArray

312 Bound for linear inequalities

313 weights : List[List[float]] or List[npt.NDArray]

314 A list of lists or arrays representing the weights for each block. Each list/array should sum to 1.

315 algorithmic_relaxation : npt.NDArray or float, optional

316 The relaxation parameter for the algorithm, by default 1.

317 relaxation : float, optional

318 The relaxation parameter for the constraints, by default 1.

319 proximity_flag : bool, optional

320 A flag indicating whether to use proximity measures, by default True.

321

322 Raises

323 ------

324 ValueError

325 If any of the weight lists do not sum to 1.

326 """

327

328 def __init__(

329 self,

330 A: npt.NDArray,

331 b: npt.NDArray,

332 weights: List[List[float]] | List[npt.NDArray],

333 algorithmic_relaxation: npt.NDArray | float = 1,

334 relaxation: float = 1,

335 proximity_flag: bool = True,

336 ):

337

338 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag)

339

340 xp = cp if self._use_gpu else np

341

342 # check that weights is a list of lists that add up to 1 each

343 for el in weights:

344 if xp.abs((xp.sum(el) - 1)) > 1e-10:

345 raise ValueError("Weights do not add up to 1!")

346

347 self.weights = []

348 self.block_idxs = [

349 xp.where(xp.array(el) > 0)[0] for el in weights

350 ] # get idxs that meet requirements

351

352 # assemble a list of general weights

353 self.total_weights = xp.zeros_like(weights[0])

354 for el in weights:

355 el = xp.asarray(el)

356 self.weights.append(el[xp.array(el) > 0]) # remove non zero weights

357 self.total_weights += el / len(weights)

358

359 def _project(self, x):

360 # simultaneous projection

361

362 for el, block_idx in zip(self.weights, self.block_idxs): # get mask and associated weights

363 p = self.indexed_map(x, block_idx)

364 res = self.b[block_idx] - p

365

366 x += self.algorithmic_relaxation * (

367 el * self.inverse_row_norm[block_idx] * res @ self.A[block_idx, :]

368 )

369 return x

370

371 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> float:

372 p = self.map(x)

373 # residuals are positive if constraints are met

374 res = abs(self.b - p)

375 measures = []

376 for measure in proximity_measures:

377 if isinstance(measure, tuple):

378 if measure[0] == "p_norm":

379 measures.append(self.total_weights @ (res ** measure[1]))

380 else:

381 raise ValueError("Invalid proximity measure")

382 elif isinstance(measure, str) and measure == "max_norm":

383 measures.append(res.max())

384 else:

385 raise ValueError("Invalid proximity measure")

386 return measures

387

388

389 class StringAveragedAMSHyperplane(HyperplaneAMSAlgorithm):

390

391 """

392 StringAveragedAMS is an implementation of the HyperplaneAMSAlgorithm

393 that

394 performs

395 string averaged projections.

396

397 Parameters

398 ----------

399 A : npt.NDArray

400 The matrix A used in the algorithm.

401 b : npt.NDArray

402 Bound for linear inequalities

403 strings : List[List[int]]

404 A list of lists, where each inner list represents a string of indices.

405 algorithmic_relaxation : npt.NDArray or float, optional

406 The relaxation parameter for the algorithm, by default 1.

407 relaxation : float, optional

408 The relaxation parameter for the projection, by default 1.

409 weights : None or List[float], optional

410 The weights for each string, by default None. If None, equal weights are assigned.

411 proximity_flag : bool, optional

412 A flag indicating whether to use proximity, by default True.

413 """

414

415 def __init__(

416 self,

417 A: npt.NDArray,

418 b: npt.NDArray,

419 strings: List[List[int]],

420 algorithmic_relaxation: npt.NDArray | float = 1,

421 relaxation: float = 1,

422 weights: None | List[float] = None,

423 proximity_flag: bool = True,

424 ):

425

426 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag)

427 xp = cp if self._use_gpu else np

428 self.strings = strings

429 if weights is None:

430 self.weights = xp.ones(len(strings)) / len(strings)

431

432 # if check_weight_validity(weights):

433 # self.weights = weights

434 else:

435 if len(weights) != len(self.strings):

436 raise ValueError("The number of weights must be equal to the number of strings.")

437

438 self.weights = weights

439 # print('Choosing default weight vector...')

440 # self.weights = np.ones(self.A.shape[0])/self.A.shape[0]

441

442 def _project(self, x):

443 # string averaged projection

444 x_c = x.copy() # create a general copy of x

445 x -= x # reset x is this viable?

446 for string, weight in zip(self.strings, self.weights):

447 x_s = x_c.copy() # generate a copy for individual strings

448 for i in string:

449 p_i = self.single_map(x_s, i)

450 res_i = self.b[i] - p_i

451 self.A.update_step(

452 x_s, self.algorithmic_relaxation * self.inverse_row_norm[i] * res_i, i

453 )

454 x += weight * x_s

455 return x