Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\feasibility\_bands\_arm_algorithms.py: 69%
128 statements
« prev ^ index » next coverage.py v7.6.4, created at 2025-02-05 17:36 +0100
« prev ^ index » next coverage.py v7.6.4, created at 2025-02-05 17:36 +0100
1from abc import ABC
2from typing import List
3import numpy as np
4import numpy.typing as npt
5from suppy.utils import LinearMapping
6from suppy.feasibility._linear_algorithms import HyperslabFeasibility
8try:
9 import cupy as cp
11 NO_GPU = False
13except ImportError:
14 NO_GPU = True
15 cp = np
18class ARMAlgorithm(HyperslabFeasibility, ABC):
19 """
20 ARMAlgorithm class for handling feasibility problems with additional
21 algorithmic relaxation.
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 """
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 ):
49 super().__init__(A, lb, ub, algorithmic_relaxation, relaxation, proximity_flag)
52class SequentialARM(ARMAlgorithm):
53 """
54 SequentialARM is a class that implements a sequential algorithm for
55 Adaptive Relaxation Method (ARM).
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 """
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 ):
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
94 def _project(self, x: npt.NDArray) -> npt.NDArray:
95 xp = cp if self._use_gpu else np
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
114class SimultaneousARM(ARMAlgorithm):
115 """
116 SimultaneousARM is a class that implements an ARM (Adaptive Relaxation
117 Method) algorithm
118 for solving feasibility problems.
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.
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 """
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 ):
156 super().__init__(A, lb, ub, algorithmic_relaxation, relaxation, proximity_flag)
157 self._k = 0
158 xp = cp if self._use_gpu else np
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
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 )
186 self._k += 1
187 return x
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
210class BIPARM(ARMAlgorithm):
211 """
212 BIPARM Algorithm for feasibility problems.
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.
231 Methods
232 -------
233 _project(x)
234 Perform the simultaneous projection of x.
235 _proximity(x)
236 Calculate the proximity measure for x.
237 """
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 ):
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]
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
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 )
281 self._k += 1
282 return x
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
305class 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.
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.
331 Methods
332 -------
333 _project(x)
334 Projects the input vector x using the string averaged projection method.
336 Raises
337 ------
338 ValueError
339 If the number of weights does not match the number of strings.
340 """
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 ):
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
359 if weights is None:
360 self.weights = xp.ones(len(strings)) / len(strings)
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.")
368 self.weights = weights
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