Coverage for C:\Users\t590r\Documents\GitHub\suppy\suppy\feasibility\_bands\_art3_algorithms.py: 55%
118 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
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.utils import ensure_float_array
16from suppy.feasibility._linear_algorithms import HyperslabFeasibility
19class ART3plusAlgorithm(HyperslabFeasibility, ABC):
20 """
21 ART3plusAlgorithm class for implementing the ART3+ algorithm.
23 Parameters
24 ----------
25 A : npt.NDArray
26 The matrix A involved in the feasibility problem.
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 for the algorithm, by default 1.
33 relaxation : float, optional
34 The relaxation parameter for the feasibility problem, by default 1.
35 proximity_flag : bool, optional
36 Flag to indicate whether to use proximity in the algorithm, 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 ):
48 super().__init__(A, lb, ub, algorithmic_relaxation, relaxation, proximity_flag)
51class SequentialART3plus(ART3plusAlgorithm):
52 """
53 SequentialART3plus is an implementation of the ART3plus algorithm for
54 solving feasibility problems.
56 Parameters
57 ----------
58 A : npt.NDArray
59 The matrix representing the system of linear inequalities.
60 lb : npt.NDArray
61 The lower bounds for the variables.
62 ub : npt.NDArray
63 The upper bounds for the variables.
64 cs : None or List[int], optional
65 The control sequence for the algorithm. If None, it will be initialized to the range of the number of rows in A.
66 proximity_flag : bool, optional
67 A flag indicating whether to use proximity in the algorithm. Default is True.
69 Attributes
70 ----------
71 initial_cs : List[int]
72 The initial control sequence.
73 cs : List[int]
74 The current control sequence.
75 _feasible : bool
76 A flag indicating whether the current solution is feasible.
78 Methods
79 -------
80 _project(x)
81 Projects the point x onto the feasible region defined by the constraints.
82 solve(x, max_iter)
83 Solves the feasibility problem using the ART3plus algorithm.
84 """
86 def __init__(
87 self,
88 A: npt.NDArray,
89 lb: npt.NDArray,
90 ub: npt.NDArray,
91 cs: None | List[int] = None,
92 proximity_flag=True,
93 ):
95 super().__init__(A, lb, ub, 1, 1, proximity_flag)
96 xp = cp if self.A.gpu else np
97 if cs is None:
98 self.initial_cs = xp.arange(self.A.shape[0])
99 else:
100 self.initial_cs = cs
102 self.cs = self.initial_cs.copy()
103 self._feasible = True
105 def _project(self, x: npt.NDArray) -> npt.NDArray:
106 to_remove = []
107 for i in self.cs:
108 # TODO: add a boolean variable that skips this if the projection did not move the point?
109 p_i = self.single_map(x, i)
110 # should be precomputed
111 if (
112 3 / 2 * self.bounds.l[i] - 1 / 2 * self.bounds.u[i] <= p_i < self.bounds.l[i]
113 ): # lowe bound reflection
114 self.A.update_step(
115 x, 2 * self.inverse_row_norm[i] * (self.bounds.l[i] - p_i), i
116 ) # reflection
117 self._feasible = False
119 elif (
120 self.bounds.u[i] < p_i <= 3 / 2 * self.bounds.u[i] - 1 / 2 * self.bounds.l[i]
121 ): # upper bound reflection
122 self.A.update_step(
123 x, 2 * self.inverse_row_norm[i] * (self.bounds.u[i] - p_i), i
124 ) # reflection
125 self._feasible = False
127 elif self.bounds.u[i] - self.bounds.l[i] < abs(
128 p_i - (self.bounds.l[i] + self.bounds.u[i]) / 2
129 ):
130 self.A.update_step(
131 x,
132 self.inverse_row_norm[i] * ((self.bounds.l[i] + self.bounds.u[i]) / 2 - p_i),
133 i,
134 ) # projection onto center of hyperslab
135 self._feasible = False
137 else: # constraint is already met
138 to_remove.append(i)
140 # after loop remove constraints that are already met
141 self.cs = [i for i in self.cs if i not in to_remove] # is this fast?
142 return x
145 @ensure_float_array
146 def solve(
147 self,
148 x: npt.NDArray,
149 max_iter: int = 500,
150 constr_tol: float = 1e-6,
151 storage: bool = False,
152 proximity_measures: List | None = None,
153 ) -> npt.NDArray:
154 """
155 Solves the optimization problem using an iterative approach.
157 Parameters
158 ----------
159 x : npt.NDArray
160 Initial guess for the solution.
161 max_iter : int, optional
162 Maximum number of iterations to perform.
163 storage : bool, optional
164 Flag indicating whether to store the intermediate solutions, by default False.
165 constr_tol : float, optional
166 The tolerance for the constraints, by default 1e-6.
167 proximity_measures : List, optional
168 The proximity measures to calculate, by default None. Right now only the first in the list is used to check the feasibility.
170 Returns
171 -------
172 npt.NDArray
173 The solution after the iterative process.
174 """
175 self.cs = self.initial_cs.copy()
176 xp = cp if isinstance(x, cp.ndarray) else np
177 if proximity_measures is None:
178 proximity_measures = [("p_norm", 2)]
179 else:
180 # TODO: Check if the proximity measures are valid
181 _ = None
183 self.proximities = []
184 i = 0
185 feasible = False
187 if storage is True:
188 self.all_x = []
189 self.all_x.append(x.copy())
191 while i < max_iter and not feasible:
193 if len(self.cs) == 0:
194 if self._feasible: # ran over all constraints and still feasible
195 return x
196 else:
197 self.cs = self.initial_cs.copy()
198 self._feasible = True
200 x = self.project(x)
201 if storage is True:
202 self.all_x.append(x.copy())
203 self.proximities.append(self.proximity(x, proximity_measures))
205 # TODO: If proximity changes x some potential issues!
206 if self.proximities[-1][0] < constr_tol:
208 feasible = True
209 i += 1
210 if self.all_x is not None:
211 self.all_x = xp.array(self.all_x)
212 return x
215class SimultaneousART3plus(ART3plusAlgorithm):
216 """
217 SimultaneousART3plus is an implementation of the ART3plus algorithm for
218 solving feasibility problems.
220 Parameters
221 ----------
222 A : npt.NDArray
223 The matrix representing the system of linear inequalities.
224 lb : npt.NDArray
225 The lower bounds for the variables.
226 ub : npt.NDArray
227 The upper bounds for the variables.
228 weights : None | List[float] | npt.NDArray, optional
229 The weights for the constraints. If None, default weights are used. Default is None.
230 proximity_flag : bool, optional
231 Flag to indicate whether to use proximity measure. Default is True.
233 Attributes
234 ----------
235 weights : npt.NDArray
236 The weights for the constraints.
238 """
240 def __init__(
241 self,
242 A: npt.NDArray,
243 lb: npt.NDArray,
244 ub: npt.NDArray,
245 weights: None | List[float] | npt.NDArray = None,
246 proximity_flag=True,
247 ):
249 super().__init__(A, lb, ub, 1, 1, proximity_flag)
250 xp = cp if self.A.gpu else np
251 if weights is None:
252 self.weights = xp.ones(self.A.shape[0]) / self.A.shape[0]
253 elif abs((weights.sum() - 1)) > 1e-10:
254 print("Weights do not add up to 1! Choosing default weight vector...")
255 self.weights = xp.ones(self.A.shape[0]) / self.A.shape[0]
256 else:
257 self.weights = weights
259 self._feasible = True
260 self._not_met = xp.arange(self.A.shape[0])
262 self._not_met_init = self._not_met.copy()
263 self._feasible = True
265 def _project(self, x: npt.NDArray) -> npt.NDArray:
266 """
267 Perform one step of the ART3plus algorithm.
269 Args:
270 x (npt.NDArray): The point to be projected.
272 Returns:
273 npt.NDArray: The projected point.
274 """
275 p = self.map(x)
276 p = p[self._not_met]
277 l_redux = self.bounds.l[self._not_met]
278 u_redux = self.bounds.u[self._not_met]
280 # following calculations are performed on subarrays
281 # assign different subsets
282 idx_1 = p < l_redux
283 idx_2 = p > u_redux
284 idx_3 = p < l_redux - (u_redux - l_redux) / 2
285 idx_4 = p > u_redux + (u_redux - l_redux) / 2
287 # sets on subarrays
288 set_1 = idx_1 & (not idx_3) # idxs for lower bound reflection
289 set_2 = idx_2 & (not idx_4) # idxs for upper bound reflection
290 set_3 = idx_3 | idx_4 # idxs for projections
291 # there should be no overlap between the different regions here!
292 x += (
293 self.weights[self._not_met][set_1]
294 * self.inverse_row_norm[self._not_met][set_1]
295 * (2 * (l_redux - p))[set_1]
296 @ self.A[self._not_met][set_1, :]
297 )
298 x += (
299 self.weights[self._not_met][set_2]
300 * self.inverse_row_norm[self._not_met][set_2]
301 * (2 * (u_redux - p))[set_2]
302 @ self.A[self._not_met][set_2, :]
303 )
304 x += (
305 self.weights[self._not_met][set_3]
306 * self.inverse_row_norm[self._not_met][set_3]
307 * ((l_redux + u_redux) / 2 - p)[set_3]
308 @ self.A[self._not_met][set_3, :]
309 )
311 # remove constraints that were already met before
312 self._not_met = self._not_met[(idx_1 | idx_2)]
314 if idx_1.sum() > 0 or idx_2.sum() > 0:
315 self._feasible = False
317 return x
319 def _proximity(self, x: npt.NDArray, proximity_measures: List[str]) -> float:
320 p = self.map(x)
321 # residuals are positive if constraints are met
322 (res_l, res_u) = self.bounds.residual(p)
323 res_u[res_u > 0] = 0
324 res_l[res_l > 0] = 0
325 res = -res_u - res_l
326 measures = []
327 for measure in proximity_measures:
328 if isinstance(measure, tuple):
329 if measure[0] == "p_norm":
330 measures.append(self.weights @ (res ** measure[1]))
331 else:
332 raise ValueError("Invalid proximity measure")
333 elif isinstance(measure, str) and measure == "max_norm":
334 measures.append(res.max())
335 else:
336 raise ValueError("Invalid proximity measure)")
337 return measures