|
""" |
|
Routines for removing redundant (linearly dependent) equations from linear |
|
programming equality constraints. |
|
""" |
|
|
|
|
|
import numpy as np |
|
from scipy.linalg import svd |
|
from scipy.linalg.interpolative import interp_decomp |
|
import scipy |
|
from scipy.linalg.blas import dtrsm |
|
|
|
|
|
def _row_count(A): |
|
""" |
|
Counts the number of nonzeros in each row of input array A. |
|
Nonzeros are defined as any element with absolute value greater than |
|
tol = 1e-13. This value should probably be an input to the function. |
|
|
|
Parameters |
|
---------- |
|
A : 2-D array |
|
An array representing a matrix |
|
|
|
Returns |
|
------- |
|
rowcount : 1-D array |
|
Number of nonzeros in each row of A |
|
|
|
""" |
|
tol = 1e-13 |
|
return np.array((abs(A) > tol).sum(axis=1)).flatten() |
|
|
|
|
|
def _get_densest(A, eligibleRows): |
|
""" |
|
Returns the index of the densest row of A. Ignores rows that are not |
|
eligible for consideration. |
|
|
|
Parameters |
|
---------- |
|
A : 2-D array |
|
An array representing a matrix |
|
eligibleRows : 1-D logical array |
|
Values indicate whether the corresponding row of A is eligible |
|
to be considered |
|
|
|
Returns |
|
------- |
|
i_densest : int |
|
Index of the densest row in A eligible for consideration |
|
|
|
""" |
|
rowCounts = _row_count(A) |
|
return np.argmax(rowCounts * eligibleRows) |
|
|
|
|
|
def _remove_zero_rows(A, b): |
|
""" |
|
Eliminates trivial equations from system of equations defined by Ax = b |
|
and identifies trivial infeasibilities |
|
|
|
Parameters |
|
---------- |
|
A : 2-D array |
|
An array representing the left-hand side of a system of equations |
|
b : 1-D array |
|
An array representing the right-hand side of a system of equations |
|
|
|
Returns |
|
------- |
|
A : 2-D array |
|
An array representing the left-hand side of a system of equations |
|
b : 1-D array |
|
An array representing the right-hand side of a system of equations |
|
status: int |
|
An integer indicating the status of the removal operation |
|
0: No infeasibility identified |
|
2: Trivially infeasible |
|
message : str |
|
A string descriptor of the exit status of the optimization. |
|
|
|
""" |
|
status = 0 |
|
message = "" |
|
i_zero = _row_count(A) == 0 |
|
A = A[np.logical_not(i_zero), :] |
|
if not np.allclose(b[i_zero], 0): |
|
status = 2 |
|
message = "There is a zero row in A_eq with a nonzero corresponding " \ |
|
"entry in b_eq. The problem is infeasible." |
|
b = b[np.logical_not(i_zero)] |
|
return A, b, status, message |
|
|
|
|
|
def bg_update_dense(plu, perm_r, v, j): |
|
LU, p = plu |
|
|
|
vperm = v[perm_r] |
|
u = dtrsm(1, LU, vperm, lower=1, diag=1) |
|
LU[:j+1, j] = u[:j+1] |
|
l = u[j+1:] |
|
piv = LU[j, j] |
|
LU[j+1:, j] += (l/piv) |
|
return LU, p |
|
|
|
|
|
def _remove_redundancy_pivot_dense(A, rhs, true_rank=None): |
|
""" |
|
Eliminates redundant equations from system of equations defined by Ax = b |
|
and identifies infeasibilities. |
|
|
|
Parameters |
|
---------- |
|
A : 2-D sparse matrix |
|
An matrix representing the left-hand side of a system of equations |
|
rhs : 1-D array |
|
An array representing the right-hand side of a system of equations |
|
|
|
Returns |
|
------- |
|
A : 2-D sparse matrix |
|
A matrix representing the left-hand side of a system of equations |
|
rhs : 1-D array |
|
An array representing the right-hand side of a system of equations |
|
status: int |
|
An integer indicating the status of the system |
|
0: No infeasibility identified |
|
2: Trivially infeasible |
|
message : str |
|
A string descriptor of the exit status of the optimization. |
|
|
|
References |
|
---------- |
|
.. [2] Andersen, Erling D. "Finding all linearly dependent rows in |
|
large-scale linear programming." Optimization Methods and Software |
|
6.3 (1995): 219-227. |
|
|
|
""" |
|
tolapiv = 1e-8 |
|
tolprimal = 1e-8 |
|
status = 0 |
|
message = "" |
|
inconsistent = ("There is a linear combination of rows of A_eq that " |
|
"results in zero, suggesting a redundant constraint. " |
|
"However the same linear combination of b_eq is " |
|
"nonzero, suggesting that the constraints conflict " |
|
"and the problem is infeasible.") |
|
A, rhs, status, message = _remove_zero_rows(A, rhs) |
|
|
|
if status != 0: |
|
return A, rhs, status, message |
|
|
|
m, n = A.shape |
|
|
|
v = list(range(m)) |
|
b = list(v) |
|
|
|
|
|
d = [] |
|
perm_r = None |
|
|
|
A_orig = A |
|
A = np.zeros((m, m + n), order='F') |
|
np.fill_diagonal(A, 1) |
|
A[:, m:] = A_orig |
|
e = np.zeros(m) |
|
|
|
js_candidates = np.arange(m, m+n, dtype=int) |
|
|
|
js_mask = np.ones(js_candidates.shape, dtype=bool) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lu = np.eye(m, order='F'), np.arange(m) |
|
perm_r = lu[1] |
|
for i in v: |
|
|
|
e[i] = 1 |
|
if i > 0: |
|
e[i-1] = 0 |
|
|
|
try: |
|
j = b[i-1] |
|
lu = bg_update_dense(lu, perm_r, A[:, j], i-1) |
|
except Exception: |
|
lu = scipy.linalg.lu_factor(A[:, b]) |
|
LU, p = lu |
|
perm_r = list(range(m)) |
|
for i1, i2 in enumerate(p): |
|
perm_r[i1], perm_r[i2] = perm_r[i2], perm_r[i1] |
|
|
|
pi = scipy.linalg.lu_solve(lu, e, trans=1) |
|
|
|
js = js_candidates[js_mask] |
|
batch = 50 |
|
|
|
|
|
|
|
for j_index in range(0, len(js), batch): |
|
j_indices = js[j_index: min(j_index+batch, len(js))] |
|
|
|
c = abs(A[:, j_indices].transpose().dot(pi)) |
|
if (c > tolapiv).any(): |
|
j = js[j_index + np.argmax(c)] |
|
b[i] = j |
|
js_mask[j-m] = False |
|
break |
|
else: |
|
bibar = pi.T.dot(rhs.reshape(-1, 1)) |
|
bnorm = np.linalg.norm(rhs) |
|
if abs(bibar)/(1+bnorm) > tolprimal: |
|
status = 2 |
|
message = inconsistent |
|
return A_orig, rhs, status, message |
|
else: |
|
d.append(i) |
|
if true_rank is not None and len(d) == m - true_rank: |
|
break |
|
|
|
keep = set(range(m)) |
|
keep = list(keep - set(d)) |
|
return A_orig[keep, :], rhs[keep], status, message |
|
|
|
|
|
def _remove_redundancy_pivot_sparse(A, rhs): |
|
""" |
|
Eliminates redundant equations from system of equations defined by Ax = b |
|
and identifies infeasibilities. |
|
|
|
Parameters |
|
---------- |
|
A : 2-D sparse matrix |
|
An matrix representing the left-hand side of a system of equations |
|
rhs : 1-D array |
|
An array representing the right-hand side of a system of equations |
|
|
|
Returns |
|
------- |
|
A : 2-D sparse matrix |
|
A matrix representing the left-hand side of a system of equations |
|
rhs : 1-D array |
|
An array representing the right-hand side of a system of equations |
|
status: int |
|
An integer indicating the status of the system |
|
0: No infeasibility identified |
|
2: Trivially infeasible |
|
message : str |
|
A string descriptor of the exit status of the optimization. |
|
|
|
References |
|
---------- |
|
.. [2] Andersen, Erling D. "Finding all linearly dependent rows in |
|
large-scale linear programming." Optimization Methods and Software |
|
6.3 (1995): 219-227. |
|
|
|
""" |
|
|
|
tolapiv = 1e-8 |
|
tolprimal = 1e-8 |
|
status = 0 |
|
message = "" |
|
inconsistent = ("There is a linear combination of rows of A_eq that " |
|
"results in zero, suggesting a redundant constraint. " |
|
"However the same linear combination of b_eq is " |
|
"nonzero, suggesting that the constraints conflict " |
|
"and the problem is infeasible.") |
|
A, rhs, status, message = _remove_zero_rows(A, rhs) |
|
|
|
if status != 0: |
|
return A, rhs, status, message |
|
|
|
m, n = A.shape |
|
|
|
v = list(range(m)) |
|
b = list(v) |
|
|
|
|
|
k = set(range(m, m+n)) |
|
d = [] |
|
|
|
A_orig = A |
|
A = scipy.sparse.hstack((scipy.sparse.eye(m), A)).tocsc() |
|
e = np.zeros(m) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for i in v: |
|
B = A[:, b] |
|
|
|
e[i] = 1 |
|
if i > 0: |
|
e[i-1] = 0 |
|
|
|
pi = scipy.sparse.linalg.spsolve(B.transpose(), e).reshape(-1, 1) |
|
|
|
js = list(k-set(b)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
c = (np.abs(A[:, js].transpose().dot(pi)) > tolapiv).nonzero()[0] |
|
if len(c) > 0: |
|
j = js[c[0]] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
b[i] = j |
|
else: |
|
bibar = pi.T.dot(rhs.reshape(-1, 1)) |
|
bnorm = np.linalg.norm(rhs) |
|
if abs(bibar)/(1 + bnorm) > tolprimal: |
|
status = 2 |
|
message = inconsistent |
|
return A_orig, rhs, status, message |
|
else: |
|
d.append(i) |
|
|
|
keep = set(range(m)) |
|
keep = list(keep - set(d)) |
|
return A_orig[keep, :], rhs[keep], status, message |
|
|
|
|
|
def _remove_redundancy_svd(A, b): |
|
""" |
|
Eliminates redundant equations from system of equations defined by Ax = b |
|
and identifies infeasibilities. |
|
|
|
Parameters |
|
---------- |
|
A : 2-D array |
|
An array representing the left-hand side of a system of equations |
|
b : 1-D array |
|
An array representing the right-hand side of a system of equations |
|
|
|
Returns |
|
------- |
|
A : 2-D array |
|
An array representing the left-hand side of a system of equations |
|
b : 1-D array |
|
An array representing the right-hand side of a system of equations |
|
status: int |
|
An integer indicating the status of the system |
|
0: No infeasibility identified |
|
2: Trivially infeasible |
|
message : str |
|
A string descriptor of the exit status of the optimization. |
|
|
|
References |
|
---------- |
|
.. [2] Andersen, Erling D. "Finding all linearly dependent rows in |
|
large-scale linear programming." Optimization Methods and Software |
|
6.3 (1995): 219-227. |
|
|
|
""" |
|
|
|
A, b, status, message = _remove_zero_rows(A, b) |
|
|
|
if status != 0: |
|
return A, b, status, message |
|
|
|
U, s, Vh = svd(A) |
|
eps = np.finfo(float).eps |
|
tol = s.max() * max(A.shape) * eps |
|
|
|
m, n = A.shape |
|
s_min = s[-1] if m <= n else 0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while abs(s_min) < tol: |
|
v = U[:, -1] |
|
|
|
eligibleRows = np.abs(v) > tol * 10e6 |
|
if not np.any(eligibleRows) or np.any(np.abs(v.dot(A)) > tol): |
|
status = 4 |
|
message = ("Due to numerical issues, redundant equality " |
|
"constraints could not be removed automatically. " |
|
"Try providing your constraint matrices as sparse " |
|
"matrices to activate sparse presolve, try turning " |
|
"off redundancy removal, or try turning off presolve " |
|
"altogether.") |
|
break |
|
if np.any(np.abs(v.dot(b)) > tol * 100): |
|
status = 2 |
|
message = ("There is a linear combination of rows of A_eq that " |
|
"results in zero, suggesting a redundant constraint. " |
|
"However the same linear combination of b_eq is " |
|
"nonzero, suggesting that the constraints conflict " |
|
"and the problem is infeasible.") |
|
break |
|
|
|
i_remove = _get_densest(A, eligibleRows) |
|
A = np.delete(A, i_remove, axis=0) |
|
b = np.delete(b, i_remove) |
|
U, s, Vh = svd(A) |
|
m, n = A.shape |
|
s_min = s[-1] if m <= n else 0 |
|
|
|
return A, b, status, message |
|
|
|
|
|
def _remove_redundancy_id(A, rhs, rank=None, randomized=True): |
|
"""Eliminates redundant equations from a system of equations. |
|
|
|
Eliminates redundant equations from system of equations defined by Ax = b |
|
and identifies infeasibilities. |
|
|
|
Parameters |
|
---------- |
|
A : 2-D array |
|
An array representing the left-hand side of a system of equations |
|
rhs : 1-D array |
|
An array representing the right-hand side of a system of equations |
|
rank : int, optional |
|
The rank of A |
|
randomized: bool, optional |
|
True for randomized interpolative decomposition |
|
|
|
Returns |
|
------- |
|
A : 2-D array |
|
An array representing the left-hand side of a system of equations |
|
rhs : 1-D array |
|
An array representing the right-hand side of a system of equations |
|
status: int |
|
An integer indicating the status of the system |
|
0: No infeasibility identified |
|
2: Trivially infeasible |
|
message : str |
|
A string descriptor of the exit status of the optimization. |
|
|
|
""" |
|
|
|
status = 0 |
|
message = "" |
|
inconsistent = ("There is a linear combination of rows of A_eq that " |
|
"results in zero, suggesting a redundant constraint. " |
|
"However the same linear combination of b_eq is " |
|
"nonzero, suggesting that the constraints conflict " |
|
"and the problem is infeasible.") |
|
|
|
A, rhs, status, message = _remove_zero_rows(A, rhs) |
|
|
|
if status != 0: |
|
return A, rhs, status, message |
|
|
|
m, n = A.shape |
|
|
|
k = rank |
|
if rank is None: |
|
k = np.linalg.matrix_rank(A) |
|
|
|
idx, proj = interp_decomp(A.T, k, rand=randomized) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not np.allclose(rhs[idx[:k]] @ proj, rhs[idx[k:]]): |
|
status = 2 |
|
message = inconsistent |
|
|
|
|
|
|
|
idx = sorted(idx[:k]) |
|
A2 = A[idx, :] |
|
rhs2 = rhs[idx] |
|
return A2, rhs2, status, message |
|
|