Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2Routines for removing redundant (linearly dependent) equations from linear 

3programming equality constraints. 

4""" 

5# Author: Matt Haberland 

6 

7import numpy as np 

8from scipy.linalg import svd 

9import scipy 

10from scipy.linalg.blas import dtrsm 

11 

12 

13def _row_count(A): 

14 """ 

15 Counts the number of nonzeros in each row of input array A. 

16 Nonzeros are defined as any element with absolute value greater than 

17 tol = 1e-13. This value should probably be an input to the function. 

18 

19 Parameters 

20 ---------- 

21 A : 2-D array 

22 An array representing a matrix 

23 

24 Returns 

25 ------- 

26 rowcount : 1-D array 

27 Number of nonzeros in each row of A 

28 

29 """ 

30 tol = 1e-13 

31 return np.array((abs(A) > tol).sum(axis=1)).flatten() 

32 

33 

34def _get_densest(A, eligibleRows): 

35 """ 

36 Returns the index of the densest row of A. Ignores rows that are not 

37 eligible for consideration. 

38 

39 Parameters 

40 ---------- 

41 A : 2-D array 

42 An array representing a matrix 

43 eligibleRows : 1-D logical array 

44 Values indicate whether the corresponding row of A is eligible 

45 to be considered 

46 

47 Returns 

48 ------- 

49 i_densest : int 

50 Index of the densest row in A eligible for consideration 

51 

52 """ 

53 rowCounts = _row_count(A) 

54 return np.argmax(rowCounts * eligibleRows) 

55 

56 

57def _remove_zero_rows(A, b): 

58 """ 

59 Eliminates trivial equations from system of equations defined by Ax = b 

60 and identifies trivial infeasibilities 

61 

62 Parameters 

63 ---------- 

64 A : 2-D array 

65 An array representing the left-hand side of a system of equations 

66 b : 1-D array 

67 An array representing the right-hand side of a system of equations 

68 

69 Returns 

70 ------- 

71 A : 2-D array 

72 An array representing the left-hand side of a system of equations 

73 b : 1-D array 

74 An array representing the right-hand side of a system of equations 

75 status: int 

76 An integer indicating the status of the removal operation 

77 0: No infeasibility identified 

78 2: Trivially infeasible 

79 message : str 

80 A string descriptor of the exit status of the optimization. 

81 

82 """ 

83 status = 0 

84 message = "" 

85 i_zero = _row_count(A) == 0 

86 A = A[np.logical_not(i_zero), :] 

87 if not(np.allclose(b[i_zero], 0)): 

88 status = 2 

89 message = "There is a zero row in A_eq with a nonzero corresponding " \ 

90 "entry in b_eq. The problem is infeasible." 

91 b = b[np.logical_not(i_zero)] 

92 return A, b, status, message 

93 

94 

95def bg_update_dense(plu, perm_r, v, j): 

96 LU, p = plu 

97 

98 vperm = v[perm_r] 

99 u = dtrsm(1, LU, vperm, lower=1, diag=1) 

100 LU[:j+1, j] = u[:j+1] 

101 l = u[j+1:] 

102 piv = LU[j, j] 

103 LU[j+1:, j] += (l/piv) 

104 return LU, p 

105 

106 

107def _remove_redundancy_dense(A, rhs, true_rank=None): 

108 """ 

109 Eliminates redundant equations from system of equations defined by Ax = b 

110 and identifies infeasibilities. 

111 

112 Parameters 

113 ---------- 

114 A : 2-D sparse matrix 

115 An matrix representing the left-hand side of a system of equations 

116 rhs : 1-D array 

117 An array representing the right-hand side of a system of equations 

118 

119 Returns 

120 ---------- 

121 A : 2-D sparse matrix 

122 A matrix representing the left-hand side of a system of equations 

123 rhs : 1-D array 

124 An array representing the right-hand side of a system of equations 

125 status: int 

126 An integer indicating the status of the system 

127 0: No infeasibility identified 

128 2: Trivially infeasible 

129 message : str 

130 A string descriptor of the exit status of the optimization. 

131 

132 References 

133 ---------- 

134 .. [2] Andersen, Erling D. "Finding all linearly dependent rows in 

135 large-scale linear programming." Optimization Methods and Software 

136 6.3 (1995): 219-227. 

137 

138 """ 

139 tolapiv = 1e-8 

140 tolprimal = 1e-8 

141 status = 0 

142 message = "" 

143 inconsistent = ("There is a linear combination of rows of A_eq that " 

144 "results in zero, suggesting a redundant constraint. " 

145 "However the same linear combination of b_eq is " 

146 "nonzero, suggesting that the constraints conflict " 

147 "and the problem is infeasible.") 

148 A, rhs, status, message = _remove_zero_rows(A, rhs) 

149 

150 if status != 0: 

151 return A, rhs, status, message 

152 

153 m, n = A.shape 

154 

155 v = list(range(m)) # Artificial column indices. 

156 b = list(v) # Basis column indices. 

157 # This is better as a list than a set because column order of basis matrix 

158 # needs to be consistent. 

159 d = [] # Indices of dependent rows 

160 perm_r = None 

161 

162 A_orig = A 

163 A = np.zeros((m, m + n), order='F') 

164 np.fill_diagonal(A, 1) 

165 A[:, m:] = A_orig 

166 e = np.zeros(m) 

167 

168 js_candidates = np.arange(m, m+n, dtype=int) # candidate columns for basis 

169 # manual masking was faster than masked array 

170 js_mask = np.ones(js_candidates.shape, dtype=bool) 

171 

172 # Implements basic algorithm from [2] 

173 # Uses some of the suggested improvements (removing zero rows and 

174 # Bartels-Golub update idea). 

175 # Removing column singletons would be easy, but it is not as important 

176 # because the procedure is performed only on the equality constraint 

177 # matrix from the original problem - not on the canonical form matrix, 

178 # which would have many more column singletons due to slack variables 

179 # from the inequality constraints. 

180 # The thoughts on "crashing" the initial basis are only really useful if 

181 # the matrix is sparse. 

182 

183 lu = np.eye(m, order='F'), np.arange(m) # initial LU is trivial 

184 perm_r = lu[1] 

185 for i in v: 

186 

187 e[i] = 1 

188 if i > 0: 

189 e[i-1] = 0 

190 

191 try: # fails for i==0 and any time it gets ill-conditioned 

192 j = b[i-1] 

193 lu = bg_update_dense(lu, perm_r, A[:, j], i-1) 

194 except Exception: 

195 lu = scipy.linalg.lu_factor(A[:, b]) 

196 LU, p = lu 

197 perm_r = list(range(m)) 

198 for i1, i2 in enumerate(p): 

199 perm_r[i1], perm_r[i2] = perm_r[i2], perm_r[i1] 

200 

201 pi = scipy.linalg.lu_solve(lu, e, trans=1) 

202 

203 js = js_candidates[js_mask] 

204 batch = 50 

205 

206 # This is a tiny bit faster than looping over columns indivually, 

207 # like for j in js: if abs(A[:,j].transpose().dot(pi)) > tolapiv: 

208 for j_index in range(0, len(js), batch): 

209 j_indices = js[j_index: min(j_index+batch, len(js))] 

210 

211 c = abs(A[:, j_indices].transpose().dot(pi)) 

212 if (c > tolapiv).any(): 

213 j = js[j_index + np.argmax(c)] # very independent column 

214 b[i] = j 

215 js_mask[j-m] = False 

216 break 

217 else: 

218 bibar = pi.T.dot(rhs.reshape(-1, 1)) 

219 bnorm = np.linalg.norm(rhs) 

220 if abs(bibar)/(1+bnorm) > tolprimal: # inconsistent 

221 status = 2 

222 message = inconsistent 

223 return A_orig, rhs, status, message 

224 else: # dependent 

225 d.append(i) 

226 if true_rank is not None and len(d) == m - true_rank: 

227 break # found all redundancies 

228 

229 keep = set(range(m)) 

230 keep = list(keep - set(d)) 

231 return A_orig[keep, :], rhs[keep], status, message 

232 

233 

234def _remove_redundancy_sparse(A, rhs): 

235 """ 

236 Eliminates redundant equations from system of equations defined by Ax = b 

237 and identifies infeasibilities. 

238 

239 Parameters 

240 ---------- 

241 A : 2-D sparse matrix 

242 An matrix representing the left-hand side of a system of equations 

243 rhs : 1-D array 

244 An array representing the right-hand side of a system of equations 

245 

246 Returns 

247 ------- 

248 A : 2-D sparse matrix 

249 A matrix representing the left-hand side of a system of equations 

250 rhs : 1-D array 

251 An array representing the right-hand side of a system of equations 

252 status: int 

253 An integer indicating the status of the system 

254 0: No infeasibility identified 

255 2: Trivially infeasible 

256 message : str 

257 A string descriptor of the exit status of the optimization. 

258 

259 References 

260 ---------- 

261 .. [2] Andersen, Erling D. "Finding all linearly dependent rows in 

262 large-scale linear programming." Optimization Methods and Software 

263 6.3 (1995): 219-227. 

264 

265 """ 

266 

267 tolapiv = 1e-8 

268 tolprimal = 1e-8 

269 status = 0 

270 message = "" 

271 inconsistent = ("There is a linear combination of rows of A_eq that " 

272 "results in zero, suggesting a redundant constraint. " 

273 "However the same linear combination of b_eq is " 

274 "nonzero, suggesting that the constraints conflict " 

275 "and the problem is infeasible.") 

276 A, rhs, status, message = _remove_zero_rows(A, rhs) 

277 

278 if status != 0: 

279 return A, rhs, status, message 

280 

281 m, n = A.shape 

282 

283 v = list(range(m)) # Artificial column indices. 

284 b = list(v) # Basis column indices. 

285 # This is better as a list than a set because column order of basis matrix 

286 # needs to be consistent. 

287 k = set(range(m, m+n)) # Structural column indices. 

288 d = [] # Indices of dependent rows 

289 

290 A_orig = A 

291 A = scipy.sparse.hstack((scipy.sparse.eye(m), A)).tocsc() 

292 e = np.zeros(m) 

293 

294 # Implements basic algorithm from [2] 

295 # Uses only one of the suggested improvements (removing zero rows). 

296 # Removing column singletons would be easy, but it is not as important 

297 # because the procedure is performed only on the equality constraint 

298 # matrix from the original problem - not on the canonical form matrix, 

299 # which would have many more column singletons due to slack variables 

300 # from the inequality constraints. 

301 # The thoughts on "crashing" the initial basis sound useful, but the 

302 # description of the procedure seems to assume a lot of familiarity with 

303 # the subject; it is not very explicit. I already went through enough 

304 # trouble getting the basic algorithm working, so I was not interested in 

305 # trying to decipher this, too. (Overall, the paper is fraught with 

306 # mistakes and ambiguities - which is strange, because the rest of 

307 # Andersen's papers are quite good.) 

308 # I tried and tried and tried to improve performance using the 

309 # Bartels-Golub update. It works, but it's only practical if the LU 

310 # factorization can be specialized as described, and that is not possible 

311 # until the SciPy SuperLU interface permits control over column 

312 # permutation - see issue #7700. 

313 

314 for i in v: 

315 B = A[:, b] 

316 

317 e[i] = 1 

318 if i > 0: 

319 e[i-1] = 0 

320 

321 pi = scipy.sparse.linalg.spsolve(B.transpose(), e).reshape(-1, 1) 

322 

323 js = list(k-set(b)) # not efficient, but this is not the time sink... 

324 

325 # Due to overhead, it tends to be faster (for problems tested) to 

326 # compute the full matrix-vector product rather than individual 

327 # vector-vector products (with the chance of terminating as soon 

328 # as any are nonzero). For very large matrices, it might be worth 

329 # it to compute, say, 100 or 1000 at a time and stop when a nonzero 

330 # is found. 

331 

332 c = (np.abs(A[:, js].transpose().dot(pi)) > tolapiv).nonzero()[0] 

333 if len(c) > 0: # independent 

334 j = js[c[0]] 

335 # in a previous commit, the previous line was changed to choose 

336 # index j corresponding with the maximum dot product. 

337 # While this avoided issues with almost 

338 # singular matrices, it slowed the routine in most NETLIB tests. 

339 # I think this is because these columns were denser than the 

340 # first column with nonzero dot product (c[0]). 

341 # It would be nice to have a heuristic that balances sparsity with 

342 # high dot product, but I don't think it's worth the time to 

343 # develop one right now. Bartels-Golub update is a much higher 

344 # priority. 

345 b[i] = j # replace artificial column 

346 else: 

347 bibar = pi.T.dot(rhs.reshape(-1, 1)) 

348 bnorm = np.linalg.norm(rhs) 

349 if abs(bibar)/(1 + bnorm) > tolprimal: 

350 status = 2 

351 message = inconsistent 

352 return A_orig, rhs, status, message 

353 else: # dependent 

354 d.append(i) 

355 

356 keep = set(range(m)) 

357 keep = list(keep - set(d)) 

358 return A_orig[keep, :], rhs[keep], status, message 

359 

360 

361def _remove_redundancy(A, b): 

362 """ 

363 Eliminates redundant equations from system of equations defined by Ax = b 

364 and identifies infeasibilities. 

365 

366 Parameters 

367 ---------- 

368 A : 2-D array 

369 An array representing the left-hand side of a system of equations 

370 b : 1-D array 

371 An array representing the right-hand side of a system of equations 

372 

373 Returns 

374 ------- 

375 A : 2-D array 

376 An array representing the left-hand side of a system of equations 

377 b : 1-D array 

378 An array representing the right-hand side of a system of equations 

379 status: int 

380 An integer indicating the status of the system 

381 0: No infeasibility identified 

382 2: Trivially infeasible 

383 message : str 

384 A string descriptor of the exit status of the optimization. 

385 

386 References 

387 ---------- 

388 .. [2] Andersen, Erling D. "Finding all linearly dependent rows in 

389 large-scale linear programming." Optimization Methods and Software 

390 6.3 (1995): 219-227. 

391 

392 """ 

393 

394 A, b, status, message = _remove_zero_rows(A, b) 

395 

396 if status != 0: 

397 return A, b, status, message 

398 

399 U, s, Vh = svd(A) 

400 eps = np.finfo(float).eps 

401 tol = s.max() * max(A.shape) * eps 

402 

403 m, n = A.shape 

404 s_min = s[-1] if m <= n else 0 

405 

406 # this algorithm is faster than that of [2] when the nullspace is small 

407 # but it could probably be improvement by randomized algorithms and with 

408 # a sparse implementation. 

409 # it relies on repeated singular value decomposition to find linearly 

410 # dependent rows (as identified by columns of U that correspond with zero 

411 # singular values). Unfortunately, only one row can be removed per 

412 # decomposition (I tried otherwise; doing so can cause problems.) 

413 # It would be nice if we could do truncated SVD like sp.sparse.linalg.svds 

414 # but that function is unreliable at finding singular values near zero. 

415 # Finding max eigenvalue L of A A^T, then largest eigenvalue (and 

416 # associated eigenvector) of -A A^T + L I (I is identity) via power 

417 # iteration would also work in theory, but is only efficient if the 

418 # smallest nonzero eigenvalue of A A^T is close to the largest nonzero 

419 # eigenvalue. 

420 

421 while abs(s_min) < tol: 

422 v = U[:, -1] # TODO: return these so user can eliminate from problem? 

423 # rows need to be represented in significant amount 

424 eligibleRows = np.abs(v) > tol * 10e6 

425 if not np.any(eligibleRows) or np.any(np.abs(v.dot(A)) > tol): 

426 status = 4 

427 message = ("Due to numerical issues, redundant equality " 

428 "constraints could not be removed automatically. " 

429 "Try providing your constraint matrices as sparse " 

430 "matrices to activate sparse presolve, try turning " 

431 "off redundancy removal, or try turning off presolve " 

432 "altogether.") 

433 break 

434 if np.any(np.abs(v.dot(b)) > tol * 100): # factor of 100 to fix 10038 and 10349 

435 status = 2 

436 message = ("There is a linear combination of rows of A_eq that " 

437 "results in zero, suggesting a redundant constraint. " 

438 "However the same linear combination of b_eq is " 

439 "nonzero, suggesting that the constraints conflict " 

440 "and the problem is infeasible.") 

441 break 

442 

443 i_remove = _get_densest(A, eligibleRows) 

444 A = np.delete(A, i_remove, axis=0) 

445 b = np.delete(b, i_remove) 

446 U, s, Vh = svd(A) 

447 m, n = A.shape 

448 s_min = s[-1] if m <= n else 0 

449 

450 return A, b, status, message