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

130 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2025-02-05 16:47 +0100

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

2from abc import ABC 

3from typing import List 

4import numpy as np 

5import numpy.typing as npt 

6 

7from scipy import sparse 

8 

9from suppy.utils import Bounds 

10from suppy.utils import LinearMapping 

11from suppy.utils import ensure_float_array 

12from suppy.projections._projections import Projection 

13 

14try: 

15 import cupy as cp 

16 

17 NO_GPU = False 

18 

19except ImportError: 

20 NO_GPU = True 

21 cp = np 

22 

23 

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

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

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

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

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