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
« 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
7from scipy import sparse
9from suppy.utils import Bounds
10from suppy.utils import LinearMapping
11from suppy.utils import ensure_float_array
12from suppy.projections._projections import Projection
14try:
15 import cupy as cp
17 NO_GPU = False
19except ImportError:
20 NO_GPU = True
21 cp = np
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.
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 """
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
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.
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.
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
97 self.proximities = []
98 i = 0
99 feasible = False
101 if storage is True:
102 self.all_x = []
103 self.all_x.append(x.copy())
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))
111 # TODO: If proximity changes x some potential issues!
112 if self.proximities[-1][0] < constr_tol:
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
121class LinearFeasibility(Feasibility, ABC):
122 """
123 LinearFeasibility class for handling linear feasibility problems.
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.
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 """
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)
162 def map(self, x: npt.NDArray) -> npt.NDArray:
163 """
164 Applies the linear mapping to the input array x.
166 Parameters
167 ----------
168 x : npt.NDArray
169 The input array to which the linear mapping is applied.
171 Returns
172 -------
173 npt.NDArray
174 The result of applying the linear mapping to the input array.
175 """
176 return self.A @ x
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.
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.
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)
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.
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.
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)
217 # @abstractmethodpass
218 # def project(self, x: npt.NDArray) -> npt.NDArray:
219 #
222class HyperplaneFeasibility(LinearFeasibility, ABC):
223 """
224 HyperplaneFeasibility class for solving halfspace feasibility problems.
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.
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 """
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
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
294class HalfspaceFeasibility(LinearFeasibility, ABC):
295 """
296 HalfspaceFeasibility class for solving halfspace feasibility problems.
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.
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 """
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
348 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> float:
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
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
370class HyperslabFeasibility(LinearFeasibility, ABC):
371 """
372 A class used to for solving feasibility problems for hyperslabs.
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.
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 """
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.")
419 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> float:
421 p = self.map(x)
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
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