523 lines
18 KiB
Python
523 lines
18 KiB
Python
"""
|
|
Routines for removing redundant (linearly dependent) equations from linear
|
|
programming equality constraints.
|
|
"""
|
|
# Author: Matt Haberland
|
|
|
|
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)) # Artificial column indices.
|
|
b = list(v) # Basis column indices.
|
|
# This is better as a list than a set because column order of basis matrix
|
|
# needs to be consistent.
|
|
d = [] # Indices of dependent rows
|
|
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) # candidate columns for basis
|
|
# manual masking was faster than masked array
|
|
js_mask = np.ones(js_candidates.shape, dtype=bool)
|
|
|
|
# Implements basic algorithm from [2]
|
|
# Uses some of the suggested improvements (removing zero rows and
|
|
# Bartels-Golub update idea).
|
|
# Removing column singletons would be easy, but it is not as important
|
|
# because the procedure is performed only on the equality constraint
|
|
# matrix from the original problem - not on the canonical form matrix,
|
|
# which would have many more column singletons due to slack variables
|
|
# from the inequality constraints.
|
|
# The thoughts on "crashing" the initial basis are only really useful if
|
|
# the matrix is sparse.
|
|
|
|
lu = np.eye(m, order='F'), np.arange(m) # initial LU is trivial
|
|
perm_r = lu[1]
|
|
for i in v:
|
|
|
|
e[i] = 1
|
|
if i > 0:
|
|
e[i-1] = 0
|
|
|
|
try: # fails for i==0 and any time it gets ill-conditioned
|
|
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
|
|
|
|
# This is a tiny bit faster than looping over columns indivually,
|
|
# like for j in js: if abs(A[:,j].transpose().dot(pi)) > tolapiv:
|
|
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)] # very independent column
|
|
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: # inconsistent
|
|
status = 2
|
|
message = inconsistent
|
|
return A_orig, rhs, status, message
|
|
else: # dependent
|
|
d.append(i)
|
|
if true_rank is not None and len(d) == m - true_rank:
|
|
break # found all redundancies
|
|
|
|
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)) # Artificial column indices.
|
|
b = list(v) # Basis column indices.
|
|
# This is better as a list than a set because column order of basis matrix
|
|
# needs to be consistent.
|
|
k = set(range(m, m+n)) # Structural column indices.
|
|
d = [] # Indices of dependent rows
|
|
|
|
A_orig = A
|
|
A = scipy.sparse.hstack((scipy.sparse.eye(m), A)).tocsc()
|
|
e = np.zeros(m)
|
|
|
|
# Implements basic algorithm from [2]
|
|
# Uses only one of the suggested improvements (removing zero rows).
|
|
# Removing column singletons would be easy, but it is not as important
|
|
# because the procedure is performed only on the equality constraint
|
|
# matrix from the original problem - not on the canonical form matrix,
|
|
# which would have many more column singletons due to slack variables
|
|
# from the inequality constraints.
|
|
# The thoughts on "crashing" the initial basis sound useful, but the
|
|
# description of the procedure seems to assume a lot of familiarity with
|
|
# the subject; it is not very explicit. I already went through enough
|
|
# trouble getting the basic algorithm working, so I was not interested in
|
|
# trying to decipher this, too. (Overall, the paper is fraught with
|
|
# mistakes and ambiguities - which is strange, because the rest of
|
|
# Andersen's papers are quite good.)
|
|
# I tried and tried and tried to improve performance using the
|
|
# Bartels-Golub update. It works, but it's only practical if the LU
|
|
# factorization can be specialized as described, and that is not possible
|
|
# until the SciPy SuperLU interface permits control over column
|
|
# permutation - see issue #7700.
|
|
|
|
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)) # not efficient, but this is not the time sink...
|
|
|
|
# Due to overhead, it tends to be faster (for problems tested) to
|
|
# compute the full matrix-vector product rather than individual
|
|
# vector-vector products (with the chance of terminating as soon
|
|
# as any are nonzero). For very large matrices, it might be worth
|
|
# it to compute, say, 100 or 1000 at a time and stop when a nonzero
|
|
# is found.
|
|
|
|
c = (np.abs(A[:, js].transpose().dot(pi)) > tolapiv).nonzero()[0]
|
|
if len(c) > 0: # independent
|
|
j = js[c[0]]
|
|
# in a previous commit, the previous line was changed to choose
|
|
# index j corresponding with the maximum dot product.
|
|
# While this avoided issues with almost
|
|
# singular matrices, it slowed the routine in most NETLIB tests.
|
|
# I think this is because these columns were denser than the
|
|
# first column with nonzero dot product (c[0]).
|
|
# It would be nice to have a heuristic that balances sparsity with
|
|
# high dot product, but I don't think it's worth the time to
|
|
# develop one right now. Bartels-Golub update is a much higher
|
|
# priority.
|
|
b[i] = j # replace artificial column
|
|
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: # dependent
|
|
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
|
|
|
|
# this algorithm is faster than that of [2] when the nullspace is small
|
|
# but it could probably be improvement by randomized algorithms and with
|
|
# a sparse implementation.
|
|
# it relies on repeated singular value decomposition to find linearly
|
|
# dependent rows (as identified by columns of U that correspond with zero
|
|
# singular values). Unfortunately, only one row can be removed per
|
|
# decomposition (I tried otherwise; doing so can cause problems.)
|
|
# It would be nice if we could do truncated SVD like sp.sparse.linalg.svds
|
|
# but that function is unreliable at finding singular values near zero.
|
|
# Finding max eigenvalue L of A A^T, then largest eigenvalue (and
|
|
# associated eigenvector) of -A A^T + L I (I is identity) via power
|
|
# iteration would also work in theory, but is only efficient if the
|
|
# smallest nonzero eigenvalue of A A^T is close to the largest nonzero
|
|
# eigenvalue.
|
|
|
|
while abs(s_min) < tol:
|
|
v = U[:, -1] # TODO: return these so user can eliminate from problem?
|
|
# rows need to be represented in significant amount
|
|
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): # factor of 100 to fix 10038 and 10349
|
|
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)
|
|
|
|
# first k entries in idx are indices of the independent rows
|
|
# remaining entries are the indices of the m-k dependent rows
|
|
# proj provides a linear combinations of rows of A2 that form the
|
|
# remaining m-k (dependent) rows. The same linear combination of entries
|
|
# in rhs2 must give the remaining m-k entries. If not, the system is
|
|
# inconsistent, and the problem is infeasible.
|
|
if not np.allclose(rhs[idx[:k]] @ proj, rhs[idx[k:]]):
|
|
status = 2
|
|
message = inconsistent
|
|
|
|
# sort indices because the other redundancy removal routines leave rows
|
|
# in original order and tests were written with that in mind
|
|
idx = sorted(idx[:k])
|
|
A2 = A[idx, :]
|
|
rhs2 = rhs[idx]
|
|
return A2, rhs2, status, message
|