_remove_redundancy.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. """
  2. Routines for removing redundant (linearly dependent) equations from linear
  3. programming equality constraints.
  4. """
  5. # Author: Matt Haberland
  6. from __future__ import division, print_function, absolute_import
  7. import numpy as np
  8. from scipy.linalg import svd
  9. import scipy
  10. def _row_count(A):
  11. """
  12. Counts the number of nonzeros in each row of input array A.
  13. Nonzeros are defined as any element with absolute value greater than
  14. tol = 1e-13. This value should probably be an input to the function.
  15. Parameters
  16. ----------
  17. A : 2-D array
  18. An array representing a matrix
  19. Returns
  20. -------
  21. rowcount : 1-D array
  22. Number of nonzeros in each row of A
  23. """
  24. tol = 1e-13
  25. return np.array((abs(A) > tol).sum(axis=1)).flatten()
  26. def _get_densest(A, eligibleRows):
  27. """
  28. Returns the index of the densest row of A. Ignores rows that are not
  29. eligible for consideration.
  30. Parameters
  31. ----------
  32. A : 2-D array
  33. An array representing a matrix
  34. eligibleRows : 1-D logical array
  35. Values indicate whether the corresponding row of A is eligible
  36. to be considered
  37. Returns
  38. -------
  39. i_densest : int
  40. Index of the densest row in A eligible for consideration
  41. """
  42. rowCounts = _row_count(A)
  43. return np.argmax(rowCounts * eligibleRows)
  44. def _remove_zero_rows(A, b):
  45. """
  46. Eliminates trivial equations from system of equations defined by Ax = b
  47. and identifies trivial infeasibilities
  48. Parameters
  49. ----------
  50. A : 2-D array
  51. An array representing the left-hand side of a system of equations
  52. b : 1-D array
  53. An array representing the right-hand side of a system of equations
  54. Returns
  55. -------
  56. A : 2-D array
  57. An array representing the left-hand side of a system of equations
  58. b : 1-D array
  59. An array representing the right-hand side of a system of equations
  60. status: int
  61. An integer indicating the status of the removal operation
  62. 0: No infeasibility identified
  63. 2: Trivially infeasible
  64. message : str
  65. A string descriptor of the exit status of the optimization.
  66. """
  67. status = 0
  68. message = ""
  69. i_zero = _row_count(A) == 0
  70. A = A[np.logical_not(i_zero), :]
  71. if not(np.allclose(b[i_zero], 0)):
  72. status = 2
  73. message = "There is a zero row in A_eq with a nonzero corresponding " \
  74. "entry in b_eq. The problem is infeasible."
  75. b = b[np.logical_not(i_zero)]
  76. return A, b, status, message
  77. def bg_update_dense(plu, perm_r, v, j):
  78. LU, p = plu
  79. u = scipy.linalg.solve_triangular(LU, v[perm_r], lower=True,
  80. unit_diagonal=True)
  81. LU[:j+1, j] = u[:j+1]
  82. l = u[j+1:]
  83. piv = LU[j, j]
  84. LU[j+1:, j] += (l/piv)
  85. return LU, p
  86. def _remove_redundancy_dense(A, rhs):
  87. """
  88. Eliminates redundant equations from system of equations defined by Ax = b
  89. and identifies infeasibilities.
  90. Parameters
  91. ----------
  92. A : 2-D sparse matrix
  93. An matrix representing the left-hand side of a system of equations
  94. rhs : 1-D array
  95. An array representing the right-hand side of a system of equations
  96. Returns
  97. ----------
  98. A : 2-D sparse matrix
  99. A matrix representing the left-hand side of a system of equations
  100. rhs : 1-D array
  101. An array representing the right-hand side of a system of equations
  102. status: int
  103. An integer indicating the status of the system
  104. 0: No infeasibility identified
  105. 2: Trivially infeasible
  106. message : str
  107. A string descriptor of the exit status of the optimization.
  108. References
  109. ----------
  110. .. [2] Andersen, Erling D. "Finding all linearly dependent rows in
  111. large-scale linear programming." Optimization Methods and Software
  112. 6.3 (1995): 219-227.
  113. """
  114. tolapiv = 1e-8
  115. tolprimal = 1e-8
  116. status = 0
  117. message = ""
  118. inconsistent = ("There is a linear combination of rows of A_eq that "
  119. "results in zero, suggesting a redundant constraint. "
  120. "However the same linear combination of b_eq is "
  121. "nonzero, suggesting that the constraints conflict "
  122. "and the problem is infeasible.")
  123. A, rhs, status, message = _remove_zero_rows(A, rhs)
  124. if status != 0:
  125. return A, rhs, status, message
  126. m, n = A.shape
  127. v = list(range(m)) # Artificial column indices.
  128. b = list(v) # Basis column indices.
  129. # This is better as a list than a set because column order of basis matrix
  130. # needs to be consistent.
  131. k = set(range(m, m+n)) # Structural column indices.
  132. d = [] # Indices of dependent rows
  133. lu = None
  134. perm_r = None
  135. A_orig = A
  136. A = np.hstack((np.eye(m), A))
  137. e = np.zeros(m)
  138. # Implements basic algorithm from [2]
  139. # Uses some of the suggested improvements (removing zero rows and
  140. # Bartels-Golub update idea).
  141. # Removing column singletons would be easy, but it is not as important
  142. # because the procedure is performed only on the equality constraint
  143. # matrix from the original problem - not on the canonical form matrix,
  144. # which would have many more column singletons due to slack variables
  145. # from the inequality constraints.
  146. # The thoughts on "crashing" the initial basis sound useful, but the
  147. # description of the procedure seems to assume a lot of familiarity with
  148. # the subject; it is not very explicit. I already went through enough
  149. # trouble getting the basic algorithm working, so I was not interested in
  150. # trying to decipher this, too. (Overall, the paper is fraught with
  151. # mistakes and ambiguities - which is strange, because the rest of
  152. # Andersen's papers are quite good.)
  153. B = A[:, b]
  154. for i in v:
  155. e[i] = 1
  156. if i > 0:
  157. e[i-1] = 0
  158. try: # fails for i==0 and any time it gets ill-conditioned
  159. j = b[i-1]
  160. lu = bg_update_dense(lu, perm_r, A[:, j], i-1)
  161. except Exception:
  162. lu = scipy.linalg.lu_factor(B)
  163. LU, p = lu
  164. perm_r = list(range(m))
  165. for i1, i2 in enumerate(p):
  166. perm_r[i1], perm_r[i2] = perm_r[i2], perm_r[i1]
  167. pi = scipy.linalg.lu_solve(lu, e, trans=1)
  168. # not efficient, but this is not the time sink...
  169. js = np.array(list(k-set(b)))
  170. batch = 50
  171. dependent = True
  172. # This is a tiny bit faster than looping over columns indivually,
  173. # like for j in js: if abs(A[:,j].transpose().dot(pi)) > tolapiv:
  174. for j_index in range(0, len(js), batch):
  175. j_indices = js[np.arange(j_index, min(j_index+batch, len(js)))]
  176. c = abs(A[:, j_indices].transpose().dot(pi))
  177. if (c > tolapiv).any():
  178. j = js[j_index + np.argmax(c)] # very independent column
  179. B[:, i] = A[:, j]
  180. b[i] = j
  181. dependent = False
  182. break
  183. if dependent:
  184. bibar = pi.T.dot(rhs.reshape(-1, 1))
  185. bnorm = np.linalg.norm(rhs)
  186. if abs(bibar)/(1+bnorm) > tolprimal: # inconsistent
  187. status = 2
  188. message = inconsistent
  189. return A_orig, rhs, status, message
  190. else: # dependent
  191. d.append(i)
  192. keep = set(range(m))
  193. keep = list(keep - set(d))
  194. return A_orig[keep, :], rhs[keep], status, message
  195. def _remove_redundancy_sparse(A, rhs):
  196. """
  197. Eliminates redundant equations from system of equations defined by Ax = b
  198. and identifies infeasibilities.
  199. Parameters
  200. ----------
  201. A : 2-D sparse matrix
  202. An matrix representing the left-hand side of a system of equations
  203. rhs : 1-D array
  204. An array representing the right-hand side of a system of equations
  205. Returns
  206. -------
  207. A : 2-D sparse matrix
  208. A matrix representing the left-hand side of a system of equations
  209. rhs : 1-D array
  210. An array representing the right-hand side of a system of equations
  211. status: int
  212. An integer indicating the status of the system
  213. 0: No infeasibility identified
  214. 2: Trivially infeasible
  215. message : str
  216. A string descriptor of the exit status of the optimization.
  217. References
  218. ----------
  219. .. [2] Andersen, Erling D. "Finding all linearly dependent rows in
  220. large-scale linear programming." Optimization Methods and Software
  221. 6.3 (1995): 219-227.
  222. """
  223. tolapiv = 1e-8
  224. tolprimal = 1e-8
  225. status = 0
  226. message = ""
  227. inconsistent = ("There is a linear combination of rows of A_eq that "
  228. "results in zero, suggesting a redundant constraint. "
  229. "However the same linear combination of b_eq is "
  230. "nonzero, suggesting that the constraints conflict "
  231. "and the problem is infeasible.")
  232. A, rhs, status, message = _remove_zero_rows(A, rhs)
  233. if status != 0:
  234. return A, rhs, status, message
  235. m, n = A.shape
  236. v = list(range(m)) # Artificial column indices.
  237. b = list(v) # Basis column indices.
  238. # This is better as a list than a set because column order of basis matrix
  239. # needs to be consistent.
  240. k = set(range(m, m+n)) # Structural column indices.
  241. d = [] # Indices of dependent rows
  242. A_orig = A
  243. A = scipy.sparse.hstack((scipy.sparse.eye(m), A)).tocsc()
  244. e = np.zeros(m)
  245. # Implements basic algorithm from [2]
  246. # Uses only one of the suggested improvements (removing zero rows).
  247. # Removing column singletons would be easy, but it is not as important
  248. # because the procedure is performed only on the equality constraint
  249. # matrix from the original problem - not on the canonical form matrix,
  250. # which would have many more column singletons due to slack variables
  251. # from the inequality constraints.
  252. # The thoughts on "crashing" the initial basis sound useful, but the
  253. # description of the procedure seems to assume a lot of familiarity with
  254. # the subject; it is not very explicit. I already went through enough
  255. # trouble getting the basic algorithm working, so I was not interested in
  256. # trying to decipher this, too. (Overall, the paper is fraught with
  257. # mistakes and ambiguities - which is strange, because the rest of
  258. # Andersen's papers are quite good.)
  259. # I tried and tried and tried to improve performance using the
  260. # Bartels-Golub update. It works, but it's only practical if the LU
  261. # factorization can be specialized as described, and that is not possible
  262. # until the Scipy SuperLU interface permits control over column
  263. # permutation - see issue #7700.
  264. for i in v:
  265. B = A[:, b]
  266. e[i] = 1
  267. if i > 0:
  268. e[i-1] = 0
  269. pi = scipy.sparse.linalg.spsolve(B.transpose(), e).reshape(-1, 1)
  270. js = list(k-set(b)) # not efficient, but this is not the time sink...
  271. # Due to overhead, it tends to be faster (for problems tested) to
  272. # compute the full matrix-vector product rather than individual
  273. # vector-vector products (with the chance of terminating as soon
  274. # as any are nonzero). For very large matrices, it might be worth
  275. # it to compute, say, 100 or 1000 at a time and stop when a nonzero
  276. # is found.
  277. c = (np.abs(A[:, js].transpose().dot(pi)) > tolapiv).nonzero()[0]
  278. if len(c) > 0: # independent
  279. j = js[c[0]]
  280. # in a previous commit, the previous line was changed to choose
  281. # index j corresponding with the maximum dot product.
  282. # While this avoided issues with almost
  283. # singular matrices, it slowed the routine in most NETLIB tests.
  284. # I think this is because these columns were denser than the
  285. # first column with nonzero dot product (c[0]).
  286. # It would be nice to have a heuristic that balances sparsity with
  287. # high dot product, but I don't think it's worth the time to
  288. # develop one right now. Bartels-Golub update is a much higher
  289. # priority.
  290. b[i] = j # replace artificial column
  291. else:
  292. bibar = pi.T.dot(rhs.reshape(-1, 1))
  293. bnorm = np.linalg.norm(rhs)
  294. if abs(bibar)/(1 + bnorm) > tolprimal:
  295. status = 2
  296. message = inconsistent
  297. return A_orig, rhs, status, message
  298. else: # dependent
  299. d.append(i)
  300. keep = set(range(m))
  301. keep = list(keep - set(d))
  302. return A_orig[keep, :], rhs[keep], status, message
  303. def _remove_redundancy(A, b):
  304. """
  305. Eliminates redundant equations from system of equations defined by Ax = b
  306. and identifies infeasibilities.
  307. Parameters
  308. ----------
  309. A : 2-D array
  310. An array representing the left-hand side of a system of equations
  311. b : 1-D array
  312. An array representing the right-hand side of a system of equations
  313. Returns
  314. -------
  315. A : 2-D array
  316. An array representing the left-hand side of a system of equations
  317. b : 1-D array
  318. An array representing the right-hand side of a system of equations
  319. status: int
  320. An integer indicating the status of the system
  321. 0: No infeasibility identified
  322. 2: Trivially infeasible
  323. message : str
  324. A string descriptor of the exit status of the optimization.
  325. References
  326. ----------
  327. .. [2] Andersen, Erling D. "Finding all linearly dependent rows in
  328. large-scale linear programming." Optimization Methods and Software
  329. 6.3 (1995): 219-227.
  330. """
  331. A, b, status, message = _remove_zero_rows(A, b)
  332. if status != 0:
  333. return A, b, status, message
  334. U, s, Vh = svd(A)
  335. eps = np.finfo(float).eps
  336. tol = s.max() * max(A.shape) * eps
  337. m, n = A.shape
  338. s_min = s[-1] if m <= n else 0
  339. # this algorithm is faster than that of [2] when the nullspace is small
  340. # but it could probably be improvement by randomized algorithms and with
  341. # a sparse implementation.
  342. # it relies on repeated singular value decomposition to find linearly
  343. # dependent rows (as identified by columns of U that correspond with zero
  344. # singular values). Unfortunately, only one row can be removed per
  345. # decomposition (I tried otherwise; doing so can cause problems.)
  346. # It would be nice if we could do truncated SVD like sp.sparse.linalg.svds
  347. # but that function is unreliable at finding singular values near zero.
  348. # Finding max eigenvalue L of A A^T, then largest eigenvalue (and
  349. # associated eigenvector) of -A A^T + L I (I is identity) via power
  350. # iteration would also work in theory, but is only efficient if the
  351. # smallest nonzero eigenvalue of A A^T is close to the largest nonzero
  352. # eigenvalue.
  353. while abs(s_min) < tol:
  354. v = U[:, -1] # TODO: return these so user can eliminate from problem?
  355. # rows need to be represented in significant amount
  356. eligibleRows = np.abs(v) > tol * 10e6
  357. if not np.any(eligibleRows) or np.any(np.abs(v.dot(A)) > tol):
  358. status = 4
  359. message = ("Due to numerical issues, redundant equality "
  360. "constraints could not be removed automatically. "
  361. "Try providing your constraint matrices as sparse "
  362. "matrices to activate sparse presolve, try turning "
  363. "off redundancy removal, or try turning off presolve "
  364. "altogether.")
  365. break
  366. if np.any(np.abs(v.dot(b)) > tol):
  367. status = 2
  368. message = ("There is a linear combination of rows of A_eq that "
  369. "results in zero, suggesting a redundant constraint. "
  370. "However the same linear combination of b_eq is "
  371. "nonzero, suggesting that the constraints conflict "
  372. "and the problem is infeasible.")
  373. break
  374. i_remove = _get_densest(A, eligibleRows)
  375. A = np.delete(A, i_remove, axis=0)
  376. b = np.delete(b, i_remove)
  377. U, s, Vh = svd(A)
  378. m, n = A.shape
  379. s_min = s[-1] if m <= n else 0
  380. return A, b, status, message