Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\feasibility\_linear_algorithms.py: 88%

1"""Base classes for linear feasibility problems."""

2 from abc import ABC

3 from typing import List

4 import numpy as np

5 import numpy.typing as npt

6

7 from scipy import sparse

8

9 from suppy.utils import Bounds

10 from suppy.utils import LinearMapping

11 from suppy.utils import ensure_float_array

12 from suppy.projections._projections import Projection

13

14 try:

15 import cupy as cp

16

17 NO_GPU = False

18

19 except ImportError:

20 NO_GPU = True

21 cp = np

22

23

24 class Feasibility(Projection, ABC):

25 """

26 Parameters

27 ----------

28 algorithmic_relaxation : npt.NDArray or float, optional

29 The relaxation parameter for the algorithm, by default 1.0.

30 relaxation : float, optional

31 The relaxation parameter for the projection, by default 1.0.

32 proximity_flag : bool, optional

33 A flag indicating whether to use this object for proximity

34 calculations, by default True.

35

36 Attributes

37 ----------

38 algorithmic_relaxation : npt.NDArray or float, optional

39 The relaxation parameter for the algorithm, by default 1.0.

40 relaxation : float, optional

41 The relaxation parameter for the projection, by default 1.0.

42 proximity_flag : bool, optional

43 Flag to indicate whether to calculate proximity, by default True.

44 _use_gpu : bool, optional

45 Flag to indicate whether to use GPU for computations, by default False.

46 """

47

48 def __init__(

49 self,

50 algorithmic_relaxation: npt.NDArray | float = 1.0,

51 relaxation: float = 1.0,

52 proximity_flag: bool = True,

53 _use_gpu: bool = False,

54 ):

55 super().__init__(relaxation, proximity_flag, _use_gpu)

56 self.algorithmic_relaxation = algorithmic_relaxation

57 self.all_x = None

58 self.proximities = None

59

60 @ensure_float_array

61 def solve(

62 self,

63 x: npt.NDArray,

64 max_iter: int = 500,

65 constr_tol: float = 1e-6,

66 storage: bool = False,

67 proximity_measures: List | None = None,

68 ) -> npt.NDArray:

69 """

70 Solves the optimization problem using an iterative approach.

71

72 Parameters

73 ----------

74 x : npt.NDArray

75 Initial guess for the solution.

76 max_iter : int, optional

77 Maximum number of iterations to perform.

78 storage : bool, optional

79 Flag indicating whether to store the intermediate solutions, by default False.

80 constr_tol : float, optional

81 The tolerance for the constraints, by default 1e-6.

82 proximity_measures : List, optional

83 The proximity measures to calculate, by default None. Right now only the first in the list is used to check the feasibility.

84

85 Returns

86 -------

87 npt.NDArray

88 The solution after the iterative process.

89 """

90 xp = cp if isinstance(x, cp.ndarray) else np

91 if proximity_measures is None:

92 proximity_measures = [("p_norm", 2)]

93 else:

94 # TODO: Check if the proximity measures are valid

95 _ = None

96

97 self.proximities = []

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.project(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

121 class LinearFeasibility(Feasibility, ABC):

122 """

123 LinearFeasibility class for handling linear feasibility problems.

124

125 Parameters

126 ----------

127 A : npt.NDArray or sparse.sparray

128 Matrix for linear inequalities

129 algorithmic_relaxation : npt.NDArray or float, optional

130 The relaxation parameter for the algorithm, by default 1.0.

131 relaxation : float, optional

132 The relaxation parameter, by default 1.0.

133 proximity_flag : bool, optional

134 Flag indicating whether to use proximity, by default True.

135

136 Attributes

137 ----------

138 A : LinearMapping

139 Matrix for linear system (stored in internal LinearMapping object).

140 algorithmic_relaxation : npt.NDArray or float, optional

141 The relaxation parameter for the algorithm, by default 1.0.

142 relaxation : float, optional

143 The relaxation parameter for the projection, by default 1.0.

144 proximity_flag : bool, optional

145 Flag to indicate whether to calculate proximity, by default True.

146 _use_gpu : bool, optional

147 Flag to indicate whether to use GPU for computations, by default False.

148 """

149

150 def __init__(

151 self,

152 A: npt.NDArray | sparse.sparray,

153 algorithmic_relaxation: npt.NDArray | float = 1.0,

154 relaxation: float = 1.0,

155 proximity_flag: bool = True,

156 ):

157 _, _use_gpu = LinearMapping.get_flags(A)

158 super().__init__(algorithmic_relaxation, relaxation, proximity_flag, _use_gpu)

159 self.A = LinearMapping(A)

160 self.inverse_row_norm = 1 / self.A.row_norm(2, 2)

161

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

163 """

164 Applies the linear mapping to the input array x.

165

166 Parameters

167 ----------

168 x : npt.NDArray

169 The input array to which the linear mapping is applied.

170

171 Returns

172 -------

173 npt.NDArray

174 The result of applying the linear mapping to the input array.

175 """

176 return self.A @ x

177

178 def single_map(self, x: npt.NDArray, i: int) -> npt.NDArray:

179 """

180 Applies the linear mapping to the input array x at a specific index

181 i.

182

183 Parameters

184 ----------

185 x : npt.NDArray

186 The input array to which the linear mapping is applied.

187 i : int

188 The specific index at which the linear mapping is applied.

189

190 Returns

191 -------

192 npt.NDArray

193 The result of applying the linear mapping to the input array at the specified index.

194 """

195 return self.A.single_map(x, i)

196

197 def indexed_map(self, x: npt.NDArray, idx: List[int] | npt.NDArray) -> npt.NDArray:

198 """

199 Applies the linear mapping to the input array x at multiple

200 specified

201 indices.

202

203 Parameters

204 ----------

205 x : npt.NDArray

206 The input array to which the linear mapping is applied.

207 idx : List[int] or npt.NDArray

208 The indices at which the linear mapping is applied.

209

210 Returns

211 -------

212 npt.NDArray

213 The result of applying the linear mapping to the input array at the specified indices.

214 """

215 return self.A.index_map(x, idx)

216

217 # @abstractmethodpass

218 # def project(self, x: npt.NDArray) -> npt.NDArray:

219 #

220

221

222 class HyperplaneFeasibility(LinearFeasibility, ABC):

223 """

224 HyperplaneFeasibility class for solving halfspace feasibility problems.

225

226 Parameters

227 ----------

228 A : npt.NDArray or sparse.sparray

229 Matrix for linear inequalities

230 b : npt.NDArray

231 Bound for linear inequalities

232 algorithmic_relaxation : npt.NDArray or float, optional

233 The relaxation parameter for the algorithm, by default 1.0.

234 relaxation : float, optional

235 The relaxation parameter, by default 1.0.

236 proximity_flag : bool, optional

237 Flag indicating whether to use proximity, by default True.

238

239 Attributes

240 ----------

241 A : LinearMapping

242 Matrix for linear system (stored in internal LinearMapping object).

243 b : npt.NDArray

244 Bound for linear inequalities

245 algorithmic_relaxation : npt.NDArray or float, optional

246 The relaxation parameter for the algorithm, by default 1.0.

247 relaxation : float, optional

248 The relaxation parameter for the projection, by default 1.0.

249 proximity_flag : bool, optional

250 Flag to indicate whether to calculate proximity, by default True.

251 _use_gpu : bool, optional

252 Flag to indicate whether to use GPU for computations, by default False.

253 """

254

255 def __init__(

256 self,

257 A: npt.NDArray | sparse.sparray,

258 b: npt.NDArray,

259 algorithmic_relaxation: npt.NDArray | float = 1.0,

260 relaxation: float = 1.0,

261 proximity_flag: bool = True,

262 ):

263 super().__init__(A, algorithmic_relaxation, relaxation, proximity_flag)

264 try:

265 len(b)

266 if A.shape[0] != len(b):

267 raise ValueError("Matrix A and vector b must have the same number of rows.")

268 except TypeError:

269 # create an array for b if it is a scalar

270 if not self.A.gpu:

271 b = np.ones(A.shape[0]) * b

272 else:

273 b = cp.ones(A.shape[0]) * b

274 self.b = b

275

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

277 p = self.map(x)

278 # residuals are positive if constraints are met

279 res = abs(self.b - p)

280 measures = []

281 for measure in proximity_measures:

282 if isinstance(measure, tuple):

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

284 measures.append(1 / len(res) * (res ** measure[1]).sum())

285 else:

286 raise ValueError("Invalid proximity measure")

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

288 measures.append(res.max())

289 else:

290 raise ValueError("Invalid proximity measure")

291 return measures

292

293

294 class HalfspaceFeasibility(LinearFeasibility, ABC):

295 """

296 HalfspaceFeasibility class for solving halfspace feasibility problems.

297

298 Parameters

299 ----------

300 A : npt.NDArray or sparse.sparray

301 Matrix for linear inequalities

302 b : npt.NDArray

303 Bound for linear inequalities

304 algorithmic_relaxation : npt.NDArray or float, optional

305 The relaxation parameter for the algorithm, by default 1.0.

306 relaxation : float, optional

307 The relaxation parameter, by default 1.0.

308 proximity_flag : bool, optional

309 Flag indicating whether to use proximity, by default True.

310

311 Attributes

312 ----------

313 A : LinearMapping

314 Matrix for linear system (stored in internal LinearMapping object).

315 b : npt.NDArray

316 Bound for linear inequalities

317 algorithmic_relaxation : npt.NDArray or float, optional

318 The relaxation parameter for the algorithm, by default 1.0.

319 relaxation : float, optional

320 The relaxation parameter for the projection, by default 1.0.

321 proximity_flag : bool, optional

322 Flag to indicate whether to calculate proximity, by default True.

323 _use_gpu : bool, optional

324 Flag to indicate whether to use GPU for computations, by default False.

325 """

326

327 def __init__(

328 self,

329 A: npt.NDArray | sparse.sparray,

330 b: npt.NDArray,

331 algorithmic_relaxation: npt.NDArray | float = 1.0,

332 relaxation: float = 1.0,

333 proximity_flag: bool = True,

334 ):

335 super().__init__(A, algorithmic_relaxation, relaxation, proximity_flag)

336 try:

337 len(b)

338 if A.shape[0] != len(b):

339 raise ValueError("Matrix A and vector b must have the same number of rows.")

340 except TypeError:

341 # create an array for b if it is a scalar

342 if not self.A.gpu:

343 b = np.ones(A.shape[0]) * b

344 else:

345 b = cp.ones(A.shape[0]) * b

346 self.b = b

347

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

349

350 p = self.map(x)

351 # residuals are positive if constraints are met

352 res = self.b - p

353 res[res > 0] = 0

354 res = -res

355

356 measures = []

357 for measure in proximity_measures:

358 if isinstance(measure, tuple):

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

360 measures.append(1 / len(res) * (res ** measure[1]).sum())

361 else:

362 raise ValueError("Invalid proximity measure")

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

364 measures.append(res.max())

365 else:

366 raise ValueError("Invalid proximity measure)")

367 return measures

368

369

370 class HyperslabFeasibility(LinearFeasibility, ABC):

371 """

372 A class used to for solving feasibility problems for hyperslabs.

373

374 Parameters

375 ----------

376 A : npt.NDArray

377 The matrix representing the linear system.

378 lb : npt.NDArray

379 The lower bounds for the hyperslab.

380 ub : npt.NDArray

381 The upper bounds for the hyperslab.

382 algorithmic_relaxation : npt.NDArray or float, optional

383 The relaxation parameter for the algorithm, by default 1.0.

384 relaxation : int, optional

385 The relaxation parameter, by default 1.

386 proximity_flag : bool, optional

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

388

389 Attributes

390 ----------

391 bounds : bounds

392 Objective for handling the upper and lower bounds of the hyperslab.

393 A : LinearMapping

394 Matrix for linear system (stored in internal LinearMapping object).

395 algorithmic_relaxation : npt.NDArray or float, optional

396 The relaxation parameter for the algorithm, by default 1.0.

397 relaxation : float, optional

398 The relaxation parameter for the projection, by default 1.0.

399 proximity_flag : bool, optional

400 Flag to indicate whether to calculate proximity, by default True.

401 _use_gpu : bool, optional

402 Flag to indicate whether to use GPU for computations, by default False.

403 """

404

405 def __init__(

406 self,

407 A: npt.NDArray,

408 lb: npt.NDArray,

409 ub: npt.NDArray,

410 algorithmic_relaxation: npt.NDArray | float = 1.0,

411 relaxation=1,

412 proximity_flag=True,

413 ):

414 super().__init__(A, algorithmic_relaxation, relaxation, proximity_flag)

415 self.bounds = Bounds(lb, ub)

416 if self.A.shape[0] != len(self.bounds.l):

417 raise ValueError("Matrix A and bound vector must have the same number of rows.")

418

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

420

421 p = self.map(x)

422

423 # residuals are positive if constraints are met

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

425 res_l[res_l > 0] = 0

426 res_u[res_u > 0] = 0

427 res = -res_l - res_u

428

429 measures = []

430 for measure in proximity_measures:

431 if isinstance(measure, tuple):

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

433 measures.append(1 / len(res) * (res ** measure[1]).sum())

434 else:

435 raise ValueError("Invalid proximity measure")

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

437 measures.append(res.max())

438 else:

439 raise ValueError("Invalid proximity measure)")

440 return measures