Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\feasibility\_halfspaces\_ams_algorithms.py: 75%
158 statements
« prev ^ index » next coverage.py v7.6.4, created at 2025-02-05 10:12 +0100
« prev ^ index » next coverage.py v7.6.4, created at 2025-02-05 10:12 +0100
1from abc import ABC
2from typing import List
3import numpy as np
4import numpy.typing as npt
6try:
7 import cupy as cp
9 NO_GPU = False
11except ImportError:
12 NO_GPU = True
13 cp = np
15from suppy.feasibility._linear_algorithms import HalfspaceFeasibility
16from suppy.utils import LinearMapping
19class HalfspaceAMSAlgorithm(HalfspaceFeasibility, ABC):
20 """
21 The HalfspaceAMSAlgorithm class is used to find a feasible solution to a
22 set of linear inequalities.
24 Parameters
25 ----------
26 A : npt.NDArray
27 The matrix representing the coefficients of the linear inequalities.
28 b : npt.NDArray
29 Bound for linear inequalities
30 algorithmic_relaxation : npt.NDArray or float, optional
31 The relaxation parameter for the algorithm, by default 1.
32 relaxation : float, optional
33 The relaxation parameter for the feasibility problem, by default 1.
34 proximity_flag : bool, optional
35 A flag indicating whether to use proximity in the algorithm, by default True.
36 """
38 def __init__(
39 self,
40 A: npt.NDArray,
41 b: npt.NDArray,
42 algorithmic_relaxation: npt.NDArray | float = 1,
43 relaxation: float = 1,
44 proximity_flag: bool = True,
45 ):
46 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag)
49class SequentialAMSHalfspace(HalfspaceAMSAlgorithm):
50 """
51 SequentialAMS class for sequentially applying the AMS algorithm.
53 Parameters
54 ----------
55 A : npt.NDArray
56 The matrix A used in the AMS algorithm.
57 b : npt.NDArray
58 Bound for linear inequalities
59 algorithmic_relaxation : npt.NDArray or float, optional
60 The relaxation parameter for the algorithm, by default 1.
61 relaxation : float, optional
62 The relaxation parameter, by default 1.
63 cs : None or List[int], optional
64 The list of indices for the constraints, by default None.
65 proximity_flag : bool, optional
66 Flag to indicate if proximity should be considered, by default True.
68 Attributes
69 ----------
70 """
72 def __init__(
73 self,
74 A: npt.NDArray,
75 b: npt.NDArray,
76 algorithmic_relaxation: npt.NDArray | float = 1,
77 relaxation: float = 1,
78 cs: None | List[int] = None,
79 proximity_flag: bool = True,
80 ):
82 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag)
83 xp = cp if self._use_gpu else np
84 if cs is None:
85 self.cs = xp.arange(self.A.shape[0])
86 else:
87 self.cs = cs
89 def _project(self, x: npt.NDArray) -> npt.NDArray:
90 """
91 Projects the input array `x` onto the feasible region defined by the
92 constraints.
94 Parameters
95 ----------
96 x : npt.NDArray
97 The input array to be projected.
99 Returns
100 -------
101 npt.NDArray
102 The projected array.
103 """
105 for i in self.cs:
106 p_i = self.single_map(x, i)
107 res = self.b[i] - p_i
108 if res < 0:
109 self.A.update_step(
110 x, self.algorithmic_relaxation * self.inverse_row_norm[i] * res, i
111 )
112 return x
115class SequentialWeightedAMSHalfspace(SequentialAMSHalfspace):
116 """
117 Parameters
118 ----------
119 A : npt.NDArray
120 The constraint matrix.
121 b : npt.NDArray
122 Bound for linear inequalities
123 weights : None, list of float, or npt.NDArray, optional
124 The weights assigned to each constraint. If None, default weights are
125 used.
126 algorithmic_relaxation : npt.NDArray or float, optional
127 The relaxation parameter for the algorithm. Default is 1.
128 relaxation : float, optional
129 The relaxation parameter for the algorithm. Default is 1.
130 weight_decay : float, optional
131 Parameter that determines the rate at which the weights are reduced
132 after each phase (weights * weight_decay). Default is 1.
133 cs : None or list of int, optional
134 The indices of the constraints to be considered. Default is None.
135 proximity_flag : bool, optional
136 Flag to indicate if proximity should be considered. Default is True.
138 Attributes
139 ----------
140 weights : npt.NDArray
141 The weights assigned to each constraint.
142 weight_decay : float
143 Decay rate for the weights.
144 temp_weight_decay : float
145 Initial value for weight decay.
146 """
148 def __init__(
149 self,
150 A: npt.NDArray,
151 b: npt.NDArray,
152 weights: None | List[float] | npt.NDArray = None,
153 algorithmic_relaxation: npt.NDArray | float = 1,
154 relaxation: float = 1,
155 weight_decay: float = 1,
156 cs: None | List[int] = None,
157 proximity_flag: bool = True,
158 ):
160 super().__init__(A, b, algorithmic_relaxation, relaxation, cs, proximity_flag)
161 xp = cp if self._use_gpu else np
162 self.weight_decay = weight_decay # decay rate
163 self.temp_weight_decay = 1 # initial value for weight decay
165 if weights is None:
166 self.weights = xp.ones(self.A.shape[0])
167 elif xp.abs((weights.sum() - 1)) > 1e-10:
168 print("Weights do not add up to 1! Renormalizing to 1...")
169 self.weights = weights
171 def _project(self, x: npt.NDArray) -> npt.NDArray:
172 """
173 Projects the input array `x` onto a feasible region defined by the
174 constraints.
176 Parameters
177 ----------
178 x : npt.NDArray
179 The input array to be projected.
181 Returns
182 -------
183 npt.NDArray
184 The projected array.
186 Notes
187 -----
188 This method iteratively adjusts the input array `x` based on the constraints
189 defined in `self.cs`. For each constraint, it computes the projection and
190 checks if the constraints are violated. If a constraint is violated, it updates
191 the array `x` using a weighted relaxation factor. The weight decay is applied
192 to the temporary weight decay after each iteration.
193 """
195 weighted_relaxation = self.algorithmic_relaxation * self.temp_weight_decay
197 for i in self.cs:
198 p_i = self.single_map(x, i)
199 res = self.b[i] - p_i
200 if res < 0:
201 self.A.update_step(
202 x, weighted_relaxation * self.weights[i] * self.inverse_row_norm[i] * res, i
203 )
205 self.temp_weight_decay *= self.weight_decay
206 return x
209class SimultaneousAMSHalfspace(HalfspaceAMSAlgorithm):
210 """
211 SimultaneousAMS is an implementation of the AMS (Alternating
212 Minimization Scheme) algorithm
213 that performs simultaneous projections and proximity calculations.
215 Parameters
216 ----------
217 A : npt.NDArray
218 The matrix representing the constraints.
219 b : npt.NDArray
220 Bound for linear inequalities
221 algorithmic_relaxation : npt.NDArray or float, optional
222 The relaxation parameter for the algorithm, by default 1.
223 relaxation : float, optional
224 The relaxation parameter for the projections, by default 1.
225 weights : None or List[float], optional
226 The weights for the constraints, by default None.
227 proximity_flag : bool, optional
228 Flag to indicate if proximity calculations should be performed, by default True.
229 """
231 def __init__(
232 self,
233 A: npt.NDArray,
234 b: npt.NDArray,
235 algorithmic_relaxation: npt.NDArray | float = 1,
236 relaxation: float = 1,
237 weights: None | List[float] = None,
238 proximity_flag: bool = True,
239 ):
241 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag)
243 xp = cp if self._use_gpu else np
245 if weights is None:
246 self.weights = xp.ones(self.A.shape[0]) / self.A.shape[0]
247 elif xp.abs((weights.sum() - 1)) > 1e-10:
248 print("Weights do not add up to 1! Renormalizing to 1...")
249 self.weights = weights / weights.sum()
250 else:
251 self.weights = weights
253 def _project(self, x):
254 # simultaneous projection
255 p = self.map(x)
256 res = self.b - p
257 res_idx = res < 0
258 x += self.algorithmic_relaxation * (
259 self.weights[res_idx]
260 * self.inverse_row_norm[res_idx]
261 * res[res_idx]
262 @ self.A[res_idx, :]
263 )
264 return x
266 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> float:
267 p = self.map(x)
268 # residuals are positive if constraints are met
269 res = self.b - p
270 res[res > 0] = 0
271 res = -res
273 measures = []
274 for measure in proximity_measures:
275 if isinstance(measure, tuple):
276 if measure[0] == "p_norm":
277 measures.append(self.weights @ (res ** measure[1]))
278 else:
279 raise ValueError("Invalid proximity measure")
280 elif isinstance(measure, str) and measure == "max_norm":
281 measures.append(res.max())
282 else:
283 raise ValueError("Invalid proximity measure)")
284 return measures
287class ExtrapolatedLandweberHalfspace(SimultaneousAMSHalfspace):
288 def __init__(
289 self, A, b, algorithmic_relaxation=1, relaxation=1, weights=None, proximity_flag=True
290 ):
291 super().__init__(A, b, algorithmic_relaxation, relaxation, weights, proximity_flag)
292 self.a_i = self.A.row_norm(2, 2)
293 self.weight_norm = self.weights / self.a_i
294 self.sigmas = []
296 def _project(self, x):
297 p = self.map(x)
298 res = self.b - p
299 res_idx = res < 0
300 if not (np.any(res_idx)):
301 self.sigmas.append(0)
302 return x
303 t = self.weight_norm[res_idx] * res[res_idx]
304 t_2 = t @ self.A[res_idx, :]
305 sig = (res[res_idx] @ t) / (t_2 @ t_2)
306 self.sigmas.append(sig)
307 x += sig * t_2
309 return x
312class BlockIterativeAMSHalfspace(HalfspaceAMSAlgorithm):
313 """
314 Block Iterative AMS Algorithm.
315 This class implements a block iterative version of the AMS (Alternating
316 Minimization Scheme) algorithm.
317 It is designed to handle constraints and weights in a block-wise manner.
319 Parameters
320 ----------
321 A : npt.NDArray
322 The matrix representing the linear constraints.
323 b : npt.NDArray
324 Bound for linear inequalities
325 weights : List[List[float]] or List[npt.NDArray]
326 A list of lists or arrays representing the weights for each block. Each list/array should sum to 1.
327 algorithmic_relaxation : npt.NDArray or float, optional
328 The relaxation parameter for the algorithm, by default 1.
329 relaxation : float, optional
330 The relaxation parameter for the constraints, by default 1.
331 proximity_flag : bool, optional
332 A flag indicating whether to use proximity measures, by default True.
334 Raises
335 ------
336 ValueError
337 If any of the weight lists do not sum to 1.
338 """
340 def __init__(
341 self,
342 A: npt.NDArray,
343 b: npt.NDArray,
344 weights: List[List[float]] | List[npt.NDArray],
345 algorithmic_relaxation: npt.NDArray | float = 1,
346 relaxation: float = 1,
347 proximity_flag: bool = True,
348 ):
350 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag)
352 xp = cp if self._use_gpu else np
354 # check that weights is a list of lists that add up to 1 each
355 for el in weights:
356 if xp.abs((xp.sum(el) - 1)) > 1e-10:
357 raise ValueError("Weights do not add up to 1!")
359 self.weights = []
360 self.block_idxs = [
361 xp.where(xp.array(el) > 0)[0] for el in weights
362 ] # get idxs that meet requirements
364 # assemble a list of general weights
365 self.total_weights = xp.zeros_like(weights[0])
366 for el in weights:
367 el = xp.asarray(el)
368 self.weights.append(el[xp.array(el) > 0]) # remove non zero weights
369 self.total_weights += el / len(weights)
371 def _project(self, x):
372 # simultaneous projection
374 for el, block_idx in zip(self.weights, self.block_idxs): # get mask and associated weights
375 p = self.indexed_map(x, block_idx)
376 res = self.b[block_idx] - p
378 res_idx = res < 0
379 full_idx = block_idx[res_idx]
381 x += self.algorithmic_relaxation * (
382 el[res_idx] * self.inverse_row_norm[full_idx] * res[res_idx] @ self.A[full_idx, :]
383 )
385 return x
387 def _proximity(self, x: npt.NDArray, proximity_measures: List) -> float:
388 p = self.map(x)
389 # residuals are positive if constraints are met
390 res = self.b - p
391 res[res > 0] = 0
392 res = -res
394 measures = []
395 for measure in proximity_measures:
396 if isinstance(measure, tuple):
397 if measure[0] == "p_norm":
398 measures.append(self.total_weights @ (res ** measure[1]))
399 else:
400 raise ValueError("Invalid proximity measure")
401 elif isinstance(measure, str) and measure == "max_norm":
402 measures.append(res.max())
403 else:
404 raise ValueError("Invalid proximity measure)")
405 return measures
408class StringAveragedAMSHalfspace(HalfspaceAMSAlgorithm):
409 """
410 StringAveragedAMS is an implementation of the HalfspaceAMSAlgorithm that
411 performs
412 string averaged projections.
414 Parameters
415 ----------
416 A : npt.NDArray
417 The matrix A used in the algorithm.
418 b : npt.NDArray
419 Bound for linear inequalities
420 strings : List[List[int]]
421 A list of lists, where each inner list represents a string of indices.
422 algorithmic_relaxation : npt.NDArray or float, optional
423 The relaxation parameter for the algorithm, by default 1.
424 relaxation : float, optional
425 The relaxation parameter for the projection, by default 1.
426 weights : None or List[float], optional
427 The weights for each string, by default None. If None, equal weights are assigned.
428 proximity_flag : bool, optional
429 A flag indicating whether to use proximity, by default True.
430 """
432 def __init__(
433 self,
434 A: npt.NDArray,
435 b: npt.NDArray,
436 strings: List[List[int]],
437 algorithmic_relaxation: npt.NDArray | float = 1,
438 relaxation: float = 1,
439 weights: None | List[float] = None,
440 proximity_flag: bool = True,
441 ):
443 super().__init__(A, b, algorithmic_relaxation, relaxation, proximity_flag)
444 xp = cp if self._use_gpu else np
445 self.strings = strings
446 if weights is None:
447 self.weights = xp.ones(len(strings)) / len(strings)
449 # if check_weight_validity(weights):
450 # self.weights = weights
451 else:
452 if len(weights) != len(self.strings):
453 raise ValueError("The number of weights must be equal to the number of strings.")
455 self.weights = weights
456 # print('Choosing default weight vector...')
457 # self.weights = np.ones(self.A.shape[0])/self.A.shape[0]
459 def _project(self, x):
460 # string averaged projection
461 x_c = x.copy() # create a general copy of x
462 x -= x # reset x is this viable?
463 for string, weight in zip(self.strings, self.weights):
464 x_s = x_c.copy() # generate a copy for individual strings
465 for i in string:
466 p_i = self.single_map(x_s, i)
467 res_i = self.b[i] - p_i
468 if res_i < 0:
469 self.A.update_step(
470 x_s, self.algorithmic_relaxation * self.inverse_row_norm[i] * res_i, i
471 )
472 x += weight * x_s
473 return x