Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\feasibility\_bands\_arm_algorithms.py: 69%

1 from abc import ABC

2 from typing import List

3 import numpy as np

4 import numpy.typing as npt

5 from suppy.utils import LinearMapping

6 from suppy.feasibility._linear_algorithms import HyperslabFeasibility

7

8 try:

9 import cupy as cp

10

11 NO_GPU = False

12

13 except ImportError:

14 NO_GPU = True

15 cp = np

16

17

18 class ARMAlgorithm(HyperslabFeasibility, ABC):

19 """

20 ARMAlgorithm class for handling feasibility problems with additional

21 algorithmic relaxation.

22

23 Parameters

24 ----------

25 A : npt.NDArray

26 The matrix representing the linear mapping.

27 lb : npt.NDArray

28 The lower bounds for the feasibility problem.

29 ub : npt.NDArray

30 The upper bounds for the feasibility problem.

31 algorithmic_relaxation : npt.NDArray or float, optional

32 The relaxation parameter specific to the algorithm, by default 1.

33 relaxation : float, optional

34 The general relaxation parameter, by default 1.

35 proximity_flag : bool, optional

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

37 """

38

39 def __init__(

40 self,

41 A: npt.NDArray,

42 lb: npt.NDArray,

43 ub: npt.NDArray,

44 algorithmic_relaxation: npt.NDArray | float = 1,

45 relaxation: float = 1,

46 proximity_flag=True,

47 ):

48

49 super().__init__(A, lb, ub, algorithmic_relaxation, relaxation, proximity_flag)

50

51

52 class SequentialARM(ARMAlgorithm):

53 """

54 SequentialARM is a class that implements a sequential algorithm for

55 Adaptive Relaxation Method (ARM).

56

57 Parameters

58 ----------

59 A : npt.NDArray

60 The matrix A used in the ARM algorithm.

61 lb : npt.NDArray

62 The lower bounds for the variables.

63 ub : npt.NDArray

64 The upper bounds for the variables.

65 algorithmic_relaxation : npt.NDArray or float, optional

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

67 relaxation : float, optional

68 The relaxation parameter, by default 1.

69 cs : None or List[int], optional

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

71 proximity_flag : bool, optional

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

73 """

74

75 def __init__(

76 self,

77 A: npt.NDArray,

78 lb: npt.NDArray,

79 ub: npt.NDArray,

80 algorithmic_relaxation: npt.NDArray | float = 1,

81 relaxation: float = 1,

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

83 proximity_flag=True,

84 ):

85

86 super().__init__(A, lb, ub, algorithmic_relaxation, relaxation, proximity_flag)

87 xp = cp if self._use_gpu else np

88 self._k = 0 # relaxation power

89 if cs is None:

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

91 else:

92 self.cs = cs

93

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

95 xp = cp if self._use_gpu else np

96

97 for i in self.cs:

98 p_i = self.single_map(x, i)

99 d = p_i - self.bounds.center[i]

100 psi = (self.bounds.u[i] - self.bounds.l[i]) / 2

101 if xp.abs(d) > psi:

102 self.A.update_step(

103 x,

104 -1

105 * self.algorithmic_relaxation**self._k

106 / 2

107 * self.inverse_row_norm[i]

108 * ((d**2 - psi**2) / d),

109 i,

110 )

111 return x

112

113

114 class SimultaneousARM(ARMAlgorithm):

115 """

116 SimultaneousARM is a class that implements an ARM (Adaptive Relaxation

117 Method) algorithm

118 for solving feasibility problems.

119

120 Parameters

121 ----------

122 A : npt.NDArray

123 The matrix representing the constraints.

124 lb : npt.NDArray

125 The lower bounds for the constraints.

126 ub : npt.NDArray

127 The upper bounds for the constraints.

128 algorithmic_relaxation : npt.NDArray or float, optional

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

130 relaxation : float, optional

131 The relaxation parameter for the constraints. Default is 1.

132 weights : None, List[float], or npt.NDArray, optional

133 The weights for the constraints. If None, weights are set to be uniform. Default is None.

134 proximity_flag : bool, optional

135 Flag to indicate whether to use proximity in the algorithm. Default is True.

136

137 Methods

138 -------

139 _project(x)

140 Performs the simultaneous projection of the input vector x.

141 _proximity(x)

142 Computes the proximity measure of the input vector x.

143 """

144

145 def __init__(

146 self,

147 A: npt.NDArray,

148 lb: npt.NDArray,

149 ub: npt.NDArray,

150 algorithmic_relaxation: npt.NDArray | float = 1,

151 relaxation: float = 1,

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

153 proximity_flag=True,

154 ):

155

156 super().__init__(A, lb, ub, algorithmic_relaxation, relaxation, proximity_flag)

157 self._k = 0

158 xp = cp if self._use_gpu else np

159

160 if weights is None:

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

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

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

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

165 else:

166 self.weights = weights

167

168 def _project(self, x):

169 xp = cp if self._use_gpu else np

170 # simultaneous projection

171 p = self.map(x)

172 d = p - self.bounds.center

173 psi = self.bounds.half_distance

174 d_idx = xp.abs(d) > psi

175 x -= (

176 self.algorithmic_relaxation**self._k

177 / 2

178 * (

179 self.weights[d_idx]

180 * self.inverse_row_norm[d_idx]

181 * (d[d_idx] - (psi[d_idx] ** 2) / d[d_idx])

182 )

183 @ self.A[d_idx, :]

184 )

185

186 self._k += 1

187 return x

188

189 def _proximity(self, x: npt.NDArray, proximity_measures: List[str]) -> float:

190 p = self.map(x)

191 # residuals are positive if constraints are met

192 (res_l, res_u) = self.bounds.residual(p)

193 res_u[res_u > 0] = 0

194 res_l[res_l > 0] = 0

195 res = -res_u - res_l

196 measures = []

197 for measure in proximity_measures:

198 if isinstance(measure, tuple):

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

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

201 else:

202 raise ValueError("Invalid proximity measure")

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

204 measures.append(res.max())

205 else:

206 raise ValueError("Invalid proximity measure)")

207 return measures

208

209

210 class BIPARM(ARMAlgorithm):

211 """

212 BIPARM Algorithm for feasibility problems.

213

214 Parameters

215 ----------

216 A : npt.NDArray

217 The matrix representing the constraints.

218 lb : npt.NDArray

219 The lower bounds for the constraints.

220 ub : npt.NDArray

221 The upper bounds for the constraints.

222 algorithmic_relaxation : npt.NDArray or float, optional

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

224 relaxation : float, optional

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

226 weights : None, List[float], or npt.NDArray, optional

227 The weights for the constraints, by default None.

228 proximity_flag : bool, optional

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

230

231 Methods

232 -------

233 _project(x)

234 Perform the simultaneous projection of x.

235 _proximity(x)

236 Calculate the proximity measure for x.

237 """

238

239 def __init__(

240 self,

241 A: npt.NDArray,

242 lb: npt.NDArray,

243 ub: npt.NDArray,

244 algorithmic_relaxation: npt.NDArray | float = 1,

245 relaxation: float = 1,

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

247 proximity_flag=True,

248 ):

249

250 super().__init__(A, lb, ub, algorithmic_relaxation, relaxation, proximity_flag)

251 xp = cp if self._use_gpu else np

252 self._k = 0

253 if weights is None:

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

255

256 # if check_weight_validity(weights):

257 # self.weights = weights

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

259 print("Weights do not add up to 1! Choosing default weight vector...")

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

261 else:

262 self.weights = weights

263

264 def _project(self, x):

265 # simultaneous projection

266 p = self.map(x)

267 d = p - self.bounds.center

268 psi = self.bounds.half_distance

269 d_idx = abs(d) > psi

270 x -= (

271 self.algorithmic_relaxation**self._k

272 / 2

273 * (

274 self.weights[d_idx]

275 * self.inverse_row_norm[d_idx]

276 * (d[d_idx] - (psi[d_idx] ** 2) / d[d_idx])

277 )

278 @ self.A[d_idx, :]

279 )

280

281 self._k += 1

282 return x

283

284 def _proximity(self, x: npt.NDArray, proximity_measures: List[str]) -> float:

285 p = self.map(x)

286 # residuals are positive if constraints are met

287 (res_l, res_u) = self.bounds.residual(p)

288 res_u[res_u > 0] = 0

289 res_l[res_l > 0] = 0

290 res = -res_u - res_l

291 measures = []

292 for measure in proximity_measures:

293 if isinstance(measure, tuple):

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

295 measures.append(1 / len(res) * self.total_weights @ (res ** measure[1]))

296 else:

297 raise ValueError("Invalid proximity measure")

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

299 measures.append(res.max())

300 else:

301 raise ValueError("Invalid proximity measure)")

302 return measures

303

304

305 class StringAveragedARM(ARMAlgorithm):

306 """

307 String Averaged ARM Algorithm.

308 This class implements the String Averaged ARM (Adaptive Relaxation Method)

309 algorithm,

310 which is used for feasibility problems involving strings of indices.

311

312 Parameters

313 ----------

314 A : npt.NDArray

315 The matrix A involved in the feasibility problem.

316 lb : npt.NDArray

317 The lower bounds for the feasibility problem.

318 ub : npt.NDArray

319 The upper bounds for the feasibility problem.

320 strings : List[List[int]]

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

322 algorithmic_relaxation : npt.NDArray or float, optional

323 The algorithmic relaxation parameter, by default 1.

324 relaxation : float, optional

325 The relaxation parameter, by default 1.

326 weights : None or List[float], optional

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

328 proximity_flag : bool, optional

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

330

331 Methods

332 -------

333 _project(x)

334 Projects the input vector x using the string averaged projection method.

335

336 Raises

337 ------

338 ValueError

339 If the number of weights does not match the number of strings.

340 """

341

342 def __init__(

343 self,

344 A: npt.NDArray,

345 lb: npt.NDArray,

346 ub: npt.NDArray,

347 strings: List[List[int]],

348 algorithmic_relaxation: npt.NDArray | float = 1,

349 relaxation: float = 1,

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

351 proximity_flag=True,

352 ):

353

354 super().__init__(A, lb, ub, algorithmic_relaxation, relaxation, proximity_flag)

355 xp = cp if self._use_gpu else np

356 self._k = 0

357 self.strings = strings

358

359 if weights is None:

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

361

362 # if check_weight_validity(weights):

363 # self.weights = weights

364 else:

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

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

367

368 self.weights = weights

369

370 def _project(self, x):

371 xp = cp if self._use_gpu else np

372 # string averaged projection

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

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

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

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

377 for i in string:

378 p_i = self.single_map(x_s, i)

379 d = p_i - self.bounds.center[i]

380 psi = (self.bounds.u[i] - self.bounds.l[i]) / 2

381 if xp.abs(d) > psi:

382 self.A.update_step(

383 x_s,

384 -1

385 * self.algorithmic_relaxation**self._k

386 / 2

387 * ((d**2 - psi**2) / d)

388 * self.inverse_row_norm[i],

389 i,

390 )

391 x += weight * x_s

392 return x