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

1from abc import ABC 

2from typing import List 

3import numpy as np 

4import numpy.typing as npt 

5 

6try: 

7 import cupy as cp 

8 

9 NO_GPU = False 

10 

11except ImportError: 

12 NO_GPU = True 

13 cp = np 

14 

15from suppy.utils import ensure_float_array 

16from suppy.feasibility._linear_algorithms import HyperslabFeasibility 

17 

18 

19class ART3plusAlgorithm(HyperslabFeasibility, ABC): 

20 """ 

21 ART3plusAlgorithm class for implementing the ART3+ algorithm. 

22 

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 """ 

38 

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) 

49 

50 

51class SequentialART3plus(ART3plusAlgorithm): 

52 """ 

53 SequentialART3plus is an implementation of the ART3plus algorithm for 

54 solving feasibility problems. 

55 

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. 

68 

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. 

77 

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 """ 

85 

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 ): 

94 

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 

101 

102 self.cs = self.initial_cs.copy() 

103 self._feasible = True 

104 

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 

118 

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 

126 

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 

136 

137 else: # constraint is already met 

138 to_remove.append(i) 

139 

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 

143 

144 

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. 

156 

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. 

169 

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 

182 

183 self.proximities = [] 

184 i = 0 

185 feasible = False 

186 

187 if storage is True: 

188 self.all_x = [] 

189 self.all_x.append(x.copy()) 

190 

191 while i < max_iter and not feasible: 

192 

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 

199 

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)) 

204 

205 # TODO: If proximity changes x some potential issues! 

206 if self.proximities[-1][0] < constr_tol: 

207 

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 

213 

214 

215class SimultaneousART3plus(ART3plusAlgorithm): 

216 """ 

217 SimultaneousART3plus is an implementation of the ART3plus algorithm for 

218 solving feasibility problems. 

219 

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. 

232 

233 Attributes 

234 ---------- 

235 weights : npt.NDArray 

236 The weights for the constraints. 

237 

238 """ 

239 

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 ): 

248 

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 

258 

259 self._feasible = True 

260 self._not_met = xp.arange(self.A.shape[0]) 

261 

262 self._not_met_init = self._not_met.copy() 

263 self._feasible = True 

264 

265 def _project(self, x: npt.NDArray) -> npt.NDArray: 

266 """ 

267 Perform one step of the ART3plus algorithm. 

268 

269 Args: 

270 x (npt.NDArray): The point to be projected. 

271 

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] 

279 

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 

286 

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 ) 

310 

311 # remove constraints that were already met before 

312 self._not_met = self._not_met[(idx_1 | idx_2)] 

313 

314 if idx_1.sum() > 0 or idx_2.sum() > 0: 

315 self._feasible = False 

316 

317 return x 

318 

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 

338