Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\feasibility\_halfspaces\_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 HalfspaceFeasibility

16 from suppy.utils import LinearMapping

17

18

19 class HalfspaceAMSAlgorithm(HalfspaceFeasibility, ABC):

20 """

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

22 set of linear inequalities.

23

24 Parameters

25 ----------

26 A : npt.NDArray

27 The matrix representing the coefficients of the linear inequalities.

28 b : npt.NDArray

29 Bound for linear inequalities

30 algorithmic_relaxation : npt.NDArray or float, optional

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

32 relaxation : float, optional

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

34 proximity_flag : bool, optional

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

36 """

37

38 def __init__(

39 self,

40 A: npt.NDArray,

41 b: npt.NDArray,

42 algorithmic_relaxation: npt.NDArray | float = 1,

43 relaxation: float = 1,

44 proximity_flag: bool = True,

45 ):

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

47

48

49 class SequentialAMSHalfspace(HalfspaceAMSAlgorithm):

50 """

51 SequentialAMS class for sequentially applying the AMS algorithm.

52

53 Parameters

54 ----------

55 A : npt.NDArray

56 The matrix A used in the AMS algorithm.

57 b : npt.NDArray

58 Bound for linear inequalities

59 algorithmic_relaxation : npt.NDArray or float, optional

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

61 relaxation : float, optional

62 The relaxation parameter, by default 1.

63 cs : None or List[int], optional

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

65 proximity_flag : bool, optional

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

67

68 Attributes

69 ----------

70 """

71

72 def __init__(

73 self,

74 A: npt.NDArray,

75 b: npt.NDArray,

76 algorithmic_relaxation: npt.NDArray | float = 1,

77 relaxation: float = 1,

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

79 proximity_flag: bool = True,

80 ):

81

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

83 xp = cp if self._use_gpu else np

84 if cs is None:

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

86 else:

87 self.cs = cs

88

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

90 """

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

92 constraints.

93

94 Parameters

95 ----------

96 x : npt.NDArray

97 The input array to be projected.

98

99 Returns

100 -------

101 npt.NDArray

102 The projected array.

103 """

104

105 for i in self.cs:

106 p_i = self.single_map(x, i)

107 res = self.b[i] - p_i

108 if res < 0:

109 self.A.update_step(

110 x, self.algorithmic_relaxation * self.inverse_row_norm[i] * res, i

111 )

112 return x

113

114

115 class SequentialWeightedAMSHalfspace(SequentialAMSHalfspace):

116 """

117 Parameters

118 ----------

119 A : npt.NDArray

120 The constraint matrix.

121 b : npt.NDArray

122 Bound for linear inequalities

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

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

125 used.

126 algorithmic_relaxation : npt.NDArray or float, optional

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

128 relaxation : float, optional

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

130 weight_decay : float, optional

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

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

133 cs : None or list of int, optional

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

135 proximity_flag : bool, optional

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

137

138 Attributes

139 ----------

140 weights : npt.NDArray

141 The weights assigned to each constraint.

142 weight_decay : float

143 Decay rate for the weights.

144 temp_weight_decay : float

145 Initial value for weight decay.

146 """

147

148 def __init__(

149 self,

150 A: npt.NDArray,

151 b: npt.NDArray,

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

153 algorithmic_relaxation: npt.NDArray | float = 1,

154 relaxation: float = 1,

155 weight_decay: float = 1,

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

157 proximity_flag: bool = True,

158 ):

159

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

161 xp = cp if self._use_gpu else np

162 self.weight_decay = weight_decay # decay rate

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

164

165 if weights is None:

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

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

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

169 self.weights = weights

170

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

172 """

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

174 constraints.

175

176 Parameters

177 ----------

178 x : npt.NDArray

179 The input array to be projected.

180

181 Returns

182 -------

183 npt.NDArray

184 The projected array.

185

186 Notes

187 -----

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

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

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

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

192 to the temporary weight decay after each iteration.

193 """

194

195 weighted_relaxation = self.algorithmic_relaxation * self.temp_weight_decay

196

197 for i in self.cs:

198 p_i = self.single_map(x, i)

199 res = self.b[i] - p_i

200 if res < 0:

201 self.A.update_step(

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

203 )

204

205 self.temp_weight_decay *= self.weight_decay

206 return x

207

208

209 class SimultaneousAMSHalfspace(HalfspaceAMSAlgorithm):

210 """

211 SimultaneousAMS is an implementation of the AMS (Alternating

212 Minimization Scheme) algorithm

213 that performs simultaneous projections and proximity calculations.

214

215 Parameters

216 ----------

217 A : npt.NDArray

218 The matrix representing the constraints.

219 b : npt.NDArray

220 Bound for linear inequalities

221 algorithmic_relaxation : npt.NDArray or float, optional

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

223 relaxation : float, optional

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

225 weights : None or List[float], optional

226 The weights for the constraints, by default None.

227 proximity_flag : bool, optional

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

229 """

230

231 def __init__(

232 self,

233 A: npt.NDArray,

234 b: npt.NDArray,

235 algorithmic_relaxation: npt.NDArray | float = 1,

236 relaxation: float = 1,

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

238 proximity_flag: bool = True,

239 ):

240

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

242

243 xp = cp if self._use_gpu else np

244

245 if weights is None:

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

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

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

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

250 else:

251 self.weights = weights

252

253 def _project(self, x):

254 # simultaneous projection

255 p = self.map(x)

256 res = self.b - p

257 res_idx = res < 0

258 x += self.algorithmic_relaxation * (

259 self.weights[res_idx]

260 * self.inverse_row_norm[res_idx]

261 * res[res_idx]

262 @ self.A[res_idx, :]

263 )

264 return x

265

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

267 p = self.map(x)

268 # residuals are positive if constraints are met

269 res = self.b - p

270 res[res > 0] = 0

271 res = -res

272

273 measures = []

274 for measure in proximity_measures:

275 if isinstance(measure, tuple):

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

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

278 else:

279 raise ValueError("Invalid proximity measure")

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

281 measures.append(res.max())

282 else:

283 raise ValueError("Invalid proximity measure)")

284 return measures

285

286

287 class ExtrapolatedLandweberHalfspace(SimultaneousAMSHalfspace):

288 def __init__(

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

290 ):

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

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

293 self.weight_norm = self.weights / self.a_i

294 self.sigmas = []

295

296 def _project(self, x):

297 p = self.map(x)

298 res = self.b - p

299 res_idx = res < 0

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

301 self.sigmas.append(0)

302 return x

303 t = self.weight_norm[res_idx] * res[res_idx]

304 t_2 = t @ self.A[res_idx, :]

305 sig = (res[res_idx] @ t) / (t_2 @ t_2)

306 self.sigmas.append(sig)

307 x += sig * t_2

308

309 return x

310

311

312 class BlockIterativeAMSHalfspace(HalfspaceAMSAlgorithm):

313 """

314 Block Iterative AMS Algorithm.

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

316 Minimization Scheme) algorithm.

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

318

319 Parameters

320 ----------

321 A : npt.NDArray

322 The matrix representing the linear constraints.

323 b : npt.NDArray

324 Bound for linear inequalities

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

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

327 algorithmic_relaxation : npt.NDArray or float, optional

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

329 relaxation : float, optional

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

331 proximity_flag : bool, optional

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

333

334 Raises

335 ------

336 ValueError

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

338 """

339

340 def __init__(

341 self,

342 A: npt.NDArray,

343 b: npt.NDArray,

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

345 algorithmic_relaxation: npt.NDArray | float = 1,

346 relaxation: float = 1,

347 proximity_flag: bool = True,

348 ):

349

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

351

352 xp = cp if self._use_gpu else np

353

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

355 for el in weights:

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

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

358

359 self.weights = []

360 self.block_idxs = [

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

362 ] # get idxs that meet requirements

363

364 # assemble a list of general weights

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

366 for el in weights:

367 el = xp.asarray(el)

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

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

370

371 def _project(self, x):

372 # simultaneous projection

373

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

375 p = self.indexed_map(x, block_idx)

376 res = self.b[block_idx] - p

377

378 res_idx = res < 0

379 full_idx = block_idx[res_idx]

380

381 x += self.algorithmic_relaxation * (

382 el[res_idx] * self.inverse_row_norm[full_idx] * res[res_idx] @ self.A[full_idx, :]

383 )

384

385 return x

386

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

388 p = self.map(x)

389 # residuals are positive if constraints are met

390 res = self.b - p

391 res[res > 0] = 0

392 res = -res

393

394 measures = []

395 for measure in proximity_measures:

396 if isinstance(measure, tuple):

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

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

399 else:

400 raise ValueError("Invalid proximity measure")

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

402 measures.append(res.max())

403 else:

404 raise ValueError("Invalid proximity measure)")

405 return measures

406

407

408 class StringAveragedAMSHalfspace(HalfspaceAMSAlgorithm):

409 """

410 StringAveragedAMS is an implementation of the HalfspaceAMSAlgorithm that

411 performs

412 string averaged projections.

413

414 Parameters

415 ----------

416 A : npt.NDArray

417 The matrix A used in the algorithm.

418 b : npt.NDArray

419 Bound for linear inequalities

420 strings : List[List[int]]

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

422 algorithmic_relaxation : npt.NDArray or float, optional

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

424 relaxation : float, optional

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

426 weights : None or List[float], optional

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

428 proximity_flag : bool, optional

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

430 """

431

432 def __init__(

433 self,

434 A: npt.NDArray,

435 b: npt.NDArray,

436 strings: List[List[int]],

437 algorithmic_relaxation: npt.NDArray | float = 1,

438 relaxation: float = 1,

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

440 proximity_flag: bool = True,

441 ):

442

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

444 xp = cp if self._use_gpu else np

445 self.strings = strings

446 if weights is None:

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

448

449 # if check_weight_validity(weights):

450 # self.weights = weights

451 else:

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

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

454

455 self.weights = weights

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

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

458

459 def _project(self, x):

460 # string averaged projection

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

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

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

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

465 for i in string:

466 p_i = self.single_map(x_s, i)

467 res_i = self.b[i] - p_i

468 if res_i < 0:

469 self.A.update_step(

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

471 )

472 x += weight * x_s

473 return x