|
"""Revised simplex method for linear programming |
|
|
|
The *revised simplex* method uses the method described in [1]_, except |
|
that a factorization [2]_ of the basis matrix, rather than its inverse, |
|
is efficiently maintained and used to solve the linear systems at each |
|
iteration of the algorithm. |
|
|
|
.. versionadded:: 1.3.0 |
|
|
|
References |
|
---------- |
|
.. [1] Bertsimas, Dimitris, and J. Tsitsiklis. "Introduction to linear |
|
programming." Athena Scientific 1 (1997): 997. |
|
.. [2] Bartels, Richard H. "A stabilization of the simplex method." |
|
Journal in Numerische Mathematik 16.5 (1971): 414-434. |
|
|
|
""" |
|
|
|
|
|
import numpy as np |
|
from numpy.linalg import LinAlgError |
|
|
|
from scipy.linalg import solve |
|
from ._optimize import _check_unknown_options |
|
from ._bglu_dense import LU |
|
from ._bglu_dense import BGLU as BGLU |
|
from ._linprog_util import _postsolve |
|
from ._optimize import OptimizeResult |
|
|
|
|
|
def _phase_one(A, b, x0, callback, postsolve_args, maxiter, tol, disp, |
|
maxupdate, mast, pivot): |
|
""" |
|
The purpose of phase one is to find an initial basic feasible solution |
|
(BFS) to the original problem. |
|
|
|
Generates an auxiliary problem with a trivial BFS and an objective that |
|
minimizes infeasibility of the original problem. Solves the auxiliary |
|
problem using the main simplex routine (phase two). This either yields |
|
a BFS to the original problem or determines that the original problem is |
|
infeasible. If feasible, phase one detects redundant rows in the original |
|
constraint matrix and removes them, then chooses additional indices as |
|
necessary to complete a basis/BFS for the original problem. |
|
""" |
|
|
|
m, n = A.shape |
|
status = 0 |
|
|
|
|
|
A, b, c, basis, x, status = _generate_auxiliary_problem(A, b, x0, tol) |
|
|
|
if status == 6: |
|
residual = c.dot(x) |
|
iter_k = 0 |
|
return x, basis, A, b, residual, status, iter_k |
|
|
|
|
|
phase_one_n = n |
|
iter_k = 0 |
|
x, basis, status, iter_k = _phase_two(c, A, x, basis, callback, |
|
postsolve_args, |
|
maxiter, tol, disp, |
|
maxupdate, mast, pivot, |
|
iter_k, phase_one_n) |
|
|
|
|
|
residual = c.dot(x) |
|
if status == 0 and residual > tol: |
|
status = 2 |
|
|
|
|
|
|
|
|
|
keep_rows = np.ones(m, dtype=bool) |
|
for basis_column in basis[basis >= n]: |
|
B = A[:, basis] |
|
try: |
|
basis_finder = np.abs(solve(B, A)) |
|
pertinent_row = np.argmax(basis_finder[:, basis_column]) |
|
eligible_columns = np.ones(n, dtype=bool) |
|
eligible_columns[basis[basis < n]] = 0 |
|
eligible_column_indices = np.where(eligible_columns)[0] |
|
index = np.argmax(basis_finder[:, :n] |
|
[pertinent_row, eligible_columns]) |
|
new_basis_column = eligible_column_indices[index] |
|
if basis_finder[pertinent_row, new_basis_column] < tol: |
|
keep_rows[pertinent_row] = False |
|
else: |
|
basis[basis == basis_column] = new_basis_column |
|
except LinAlgError: |
|
status = 4 |
|
|
|
|
|
A = A[keep_rows, :n] |
|
basis = basis[keep_rows] |
|
x = x[:n] |
|
m = A.shape[0] |
|
return x, basis, A, b, residual, status, iter_k |
|
|
|
|
|
def _get_more_basis_columns(A, basis): |
|
""" |
|
Called when the auxiliary problem terminates with artificial columns in |
|
the basis, which must be removed and replaced with non-artificial |
|
columns. Finds additional columns that do not make the matrix singular. |
|
""" |
|
m, n = A.shape |
|
|
|
|
|
a = np.arange(m+n) |
|
bl = np.zeros(len(a), dtype=bool) |
|
bl[basis] = 1 |
|
options = a[~bl] |
|
options = options[options < n] |
|
|
|
|
|
B = np.zeros((m, m)) |
|
B[:, 0:len(basis)] = A[:, basis] |
|
|
|
if (basis.size > 0 and |
|
np.linalg.matrix_rank(B[:, :len(basis)]) < len(basis)): |
|
raise Exception("Basis has dependent columns") |
|
|
|
rank = 0 |
|
for i in range(n): |
|
|
|
new_basis = np.random.permutation(options)[:m-len(basis)] |
|
B[:, len(basis):] = A[:, new_basis] |
|
rank = np.linalg.matrix_rank(B) |
|
if rank == m: |
|
break |
|
|
|
return np.concatenate((basis, new_basis)) |
|
|
|
|
|
def _generate_auxiliary_problem(A, b, x0, tol): |
|
""" |
|
Modifies original problem to create an auxiliary problem with a trivial |
|
initial basic feasible solution and an objective that minimizes |
|
infeasibility in the original problem. |
|
|
|
Conceptually, this is done by stacking an identity matrix on the right of |
|
the original constraint matrix, adding artificial variables to correspond |
|
with each of these new columns, and generating a cost vector that is all |
|
zeros except for ones corresponding with each of the new variables. |
|
|
|
A initial basic feasible solution is trivial: all variables are zero |
|
except for the artificial variables, which are set equal to the |
|
corresponding element of the right hand side `b`. |
|
|
|
Running the simplex method on this auxiliary problem drives all of the |
|
artificial variables - and thus the cost - to zero if the original problem |
|
is feasible. The original problem is declared infeasible otherwise. |
|
|
|
Much of the complexity below is to improve efficiency by using singleton |
|
columns in the original problem where possible, thus generating artificial |
|
variables only as necessary, and using an initial 'guess' basic feasible |
|
solution. |
|
""" |
|
status = 0 |
|
m, n = A.shape |
|
|
|
if x0 is not None: |
|
x = x0 |
|
else: |
|
x = np.zeros(n) |
|
|
|
r = b - A@x |
|
|
|
A[r < 0] = -A[r < 0] |
|
b[r < 0] = -b[r < 0] |
|
r[r < 0] *= -1 |
|
|
|
|
|
|
|
|
|
|
|
if x0 is None: |
|
nonzero_constraints = np.arange(m) |
|
else: |
|
nonzero_constraints = np.where(r > tol)[0] |
|
|
|
|
|
basis = np.where(np.abs(x) > tol)[0] |
|
|
|
if len(nonzero_constraints) == 0 and len(basis) <= m: |
|
c = np.zeros(n) |
|
basis = _get_more_basis_columns(A, basis) |
|
return A, b, c, basis, x, status |
|
elif (len(nonzero_constraints) > m - len(basis) or |
|
np.any(x < 0)): |
|
c = np.zeros(n) |
|
status = 6 |
|
return A, b, c, basis, x, status |
|
|
|
|
|
cols, rows = _select_singleton_columns(A, r) |
|
|
|
|
|
i_tofix = np.isin(rows, nonzero_constraints) |
|
|
|
|
|
i_notinbasis = np.logical_not(np.isin(cols, basis)) |
|
i_fix_without_aux = np.logical_and(i_tofix, i_notinbasis) |
|
rows = rows[i_fix_without_aux] |
|
cols = cols[i_fix_without_aux] |
|
|
|
|
|
|
|
arows = nonzero_constraints[np.logical_not( |
|
np.isin(nonzero_constraints, rows))] |
|
n_aux = len(arows) |
|
acols = n + np.arange(n_aux) |
|
|
|
basis_ng = np.concatenate((cols, acols)) |
|
basis_ng_rows = np.concatenate((rows, arows)) |
|
|
|
|
|
A = np.hstack((A, np.zeros((m, n_aux)))) |
|
A[arows, acols] = 1 |
|
|
|
|
|
x = np.concatenate((x, np.zeros(n_aux))) |
|
x[basis_ng] = r[basis_ng_rows]/A[basis_ng_rows, basis_ng] |
|
|
|
|
|
c = np.zeros(n_aux + n) |
|
c[acols] = 1 |
|
|
|
|
|
|
|
|
|
basis = np.concatenate((basis, basis_ng)) |
|
basis = _get_more_basis_columns(A, basis) |
|
|
|
return A, b, c, basis, x, status |
|
|
|
|
|
def _select_singleton_columns(A, b): |
|
""" |
|
Finds singleton columns for which the singleton entry is of the same sign |
|
as the right-hand side; these columns are eligible for inclusion in an |
|
initial basis. Determines the rows in which the singleton entries are |
|
located. For each of these rows, returns the indices of the one singleton |
|
column and its corresponding row. |
|
""" |
|
|
|
column_indices = np.nonzero(np.sum(np.abs(A) != 0, axis=0) == 1)[0] |
|
columns = A[:, column_indices] |
|
row_indices = np.zeros(len(column_indices), dtype=int) |
|
nonzero_rows, nonzero_columns = np.nonzero(columns) |
|
row_indices[nonzero_columns] = nonzero_rows |
|
|
|
|
|
|
|
same_sign = A[row_indices, column_indices]*b[row_indices] >= 0 |
|
column_indices = column_indices[same_sign][::-1] |
|
row_indices = row_indices[same_sign][::-1] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
unique_row_indices, first_columns = np.unique(row_indices, |
|
return_index=True) |
|
return column_indices[first_columns], unique_row_indices |
|
|
|
|
|
def _find_nonzero_rows(A, tol): |
|
""" |
|
Returns logical array indicating the locations of rows with at least |
|
one nonzero element. |
|
""" |
|
return np.any(np.abs(A) > tol, axis=1) |
|
|
|
|
|
def _select_enter_pivot(c_hat, bl, a, rule="bland", tol=1e-12): |
|
""" |
|
Selects a pivot to enter the basis. Currently Bland's rule - the smallest |
|
index that has a negative reduced cost - is the default. |
|
""" |
|
if rule.lower() == "mrc": |
|
return a[~bl][np.argmin(c_hat)] |
|
else: |
|
return a[~bl][c_hat < -tol][0] |
|
|
|
|
|
def _display_iter(phase, iteration, slack, con, fun): |
|
""" |
|
Print indicators of optimization status to the console. |
|
""" |
|
header = True if not iteration % 20 else False |
|
|
|
if header: |
|
print("Phase", |
|
"Iteration", |
|
"Minimum Slack ", |
|
"Constraint Residual", |
|
"Objective ") |
|
|
|
|
|
fmt = '{0:<6}{1:<10}{2:<20.13}{3:<20.13}{4:<20.13}' |
|
try: |
|
slack = np.min(slack) |
|
except ValueError: |
|
slack = "NA" |
|
print(fmt.format(phase, iteration, slack, np.linalg.norm(con), fun)) |
|
|
|
|
|
def _display_and_callback(phase_one_n, x, postsolve_args, status, |
|
iteration, disp, callback): |
|
if phase_one_n is not None: |
|
phase = 1 |
|
x_postsolve = x[:phase_one_n] |
|
else: |
|
phase = 2 |
|
x_postsolve = x |
|
x_o, fun, slack, con = _postsolve(x_postsolve, |
|
postsolve_args) |
|
|
|
if callback is not None: |
|
res = OptimizeResult({'x': x_o, 'fun': fun, 'slack': slack, |
|
'con': con, 'nit': iteration, |
|
'phase': phase, 'complete': False, |
|
'status': status, 'message': "", |
|
'success': False}) |
|
callback(res) |
|
if disp: |
|
_display_iter(phase, iteration, slack, con, fun) |
|
|
|
|
|
def _phase_two(c, A, x, b, callback, postsolve_args, maxiter, tol, disp, |
|
maxupdate, mast, pivot, iteration=0, phase_one_n=None): |
|
""" |
|
The heart of the simplex method. Beginning with a basic feasible solution, |
|
moves to adjacent basic feasible solutions successively lower reduced cost. |
|
Terminates when there are no basic feasible solutions with lower reduced |
|
cost or if the problem is determined to be unbounded. |
|
|
|
This implementation follows the revised simplex method based on LU |
|
decomposition. Rather than maintaining a tableau or an inverse of the |
|
basis matrix, we keep a factorization of the basis matrix that allows |
|
efficient solution of linear systems while avoiding stability issues |
|
associated with inverted matrices. |
|
""" |
|
m, n = A.shape |
|
status = 0 |
|
a = np.arange(n) |
|
ab = np.arange(m) |
|
if maxupdate: |
|
|
|
B = BGLU(A, b, maxupdate, mast) |
|
else: |
|
B = LU(A, b) |
|
|
|
for iteration in range(iteration, maxiter): |
|
|
|
if disp or callback is not None: |
|
_display_and_callback(phase_one_n, x, postsolve_args, status, |
|
iteration, disp, callback) |
|
|
|
bl = np.zeros(len(a), dtype=bool) |
|
bl[b] = 1 |
|
|
|
xb = x[b] |
|
cb = c[b] |
|
|
|
try: |
|
v = B.solve(cb, transposed=True) |
|
except LinAlgError: |
|
status = 4 |
|
break |
|
|
|
|
|
c_hat = c - v.dot(A) |
|
c_hat = c_hat[~bl] |
|
|
|
|
|
|
|
|
|
|
|
if np.all(c_hat >= -tol): |
|
break |
|
|
|
j = _select_enter_pivot(c_hat, bl, a, rule=pivot, tol=tol) |
|
u = B.solve(A[:, j]) |
|
|
|
i = u > tol |
|
if not np.any(i): |
|
status = 3 |
|
break |
|
|
|
th = xb[i]/u[i] |
|
l = np.argmin(th) |
|
th_star = th[l] |
|
|
|
x[b] = x[b] - th_star*u |
|
x[j] = th_star |
|
B.update(ab[i][l], j) |
|
b = B.b |
|
|
|
else: |
|
|
|
|
|
|
|
iteration += 1 |
|
status = 1 |
|
if disp or callback is not None: |
|
_display_and_callback(phase_one_n, x, postsolve_args, status, |
|
iteration, disp, callback) |
|
|
|
return x, b, status, iteration |
|
|
|
|
|
def _linprog_rs(c, c0, A, b, x0, callback, postsolve_args, |
|
maxiter=5000, tol=1e-12, disp=False, |
|
maxupdate=10, mast=False, pivot="mrc", |
|
**unknown_options): |
|
""" |
|
Solve the following linear programming problem via a two-phase |
|
revised simplex algorithm.:: |
|
|
|
minimize: c @ x |
|
|
|
subject to: A @ x == b |
|
0 <= x < oo |
|
|
|
User-facing documentation is in _linprog_doc.py. |
|
|
|
Parameters |
|
---------- |
|
c : 1-D array |
|
Coefficients of the linear objective function to be minimized. |
|
c0 : float |
|
Constant term in objective function due to fixed (and eliminated) |
|
variables. (Currently unused.) |
|
A : 2-D array |
|
2-D array which, when matrix-multiplied by ``x``, gives the values of |
|
the equality constraints at ``x``. |
|
b : 1-D array |
|
1-D array of values representing the RHS of each equality constraint |
|
(row) in ``A_eq``. |
|
x0 : 1-D array, optional |
|
Starting values of the independent variables, which will be refined by |
|
the optimization algorithm. For the revised simplex method, these must |
|
correspond with a basic feasible solution. |
|
callback : callable, optional |
|
If a callback function is provided, it will be called within each |
|
iteration of the algorithm. The callback function must accept a single |
|
`scipy.optimize.OptimizeResult` consisting of the following fields: |
|
|
|
x : 1-D array |
|
Current solution vector. |
|
fun : float |
|
Current value of the objective function ``c @ x``. |
|
success : bool |
|
True only when an algorithm has completed successfully, |
|
so this is always False as the callback function is called |
|
only while the algorithm is still iterating. |
|
slack : 1-D array |
|
The values of the slack variables. Each slack variable |
|
corresponds to an inequality constraint. If the slack is zero, |
|
the corresponding constraint is active. |
|
con : 1-D array |
|
The (nominally zero) residuals of the equality constraints, |
|
that is, ``b - A_eq @ x``. |
|
phase : int |
|
The phase of the algorithm being executed. |
|
status : int |
|
For revised simplex, this is always 0 because if a different |
|
status is detected, the algorithm terminates. |
|
nit : int |
|
The number of iterations performed. |
|
message : str |
|
A string descriptor of the exit status of the optimization. |
|
postsolve_args : tuple |
|
Data needed by _postsolve to convert the solution to the standard-form |
|
problem into the solution to the original problem. |
|
|
|
Options |
|
------- |
|
maxiter : int |
|
The maximum number of iterations to perform in either phase. |
|
tol : float |
|
The tolerance which determines when a solution is "close enough" to |
|
zero in Phase 1 to be considered a basic feasible solution or close |
|
enough to positive to serve as an optimal solution. |
|
disp : bool |
|
Set to ``True`` if indicators of optimization status are to be printed |
|
to the console each iteration. |
|
maxupdate : int |
|
The maximum number of updates performed on the LU factorization. |
|
After this many updates is reached, the basis matrix is factorized |
|
from scratch. |
|
mast : bool |
|
Minimize Amortized Solve Time. If enabled, the average time to solve |
|
a linear system using the basis factorization is measured. Typically, |
|
the average solve time will decrease with each successive solve after |
|
initial factorization, as factorization takes much more time than the |
|
solve operation (and updates). Eventually, however, the updated |
|
factorization becomes sufficiently complex that the average solve time |
|
begins to increase. When this is detected, the basis is refactorized |
|
from scratch. Enable this option to maximize speed at the risk of |
|
nondeterministic behavior. Ignored if ``maxupdate`` is 0. |
|
pivot : "mrc" or "bland" |
|
Pivot rule: Minimum Reduced Cost (default) or Bland's rule. Choose |
|
Bland's rule if iteration limit is reached and cycling is suspected. |
|
unknown_options : dict |
|
Optional arguments not used by this particular solver. If |
|
`unknown_options` is non-empty a warning is issued listing all |
|
unused options. |
|
|
|
Returns |
|
------- |
|
x : 1-D array |
|
Solution vector. |
|
status : int |
|
An integer representing the exit status of the optimization:: |
|
|
|
0 : Optimization terminated successfully |
|
1 : Iteration limit reached |
|
2 : Problem appears to be infeasible |
|
3 : Problem appears to be unbounded |
|
4 : Numerical difficulties encountered |
|
5 : No constraints; turn presolve on |
|
6 : Guess x0 cannot be converted to a basic feasible solution |
|
|
|
message : str |
|
A string descriptor of the exit status of the optimization. |
|
iteration : int |
|
The number of iterations taken to solve the problem. |
|
""" |
|
|
|
_check_unknown_options(unknown_options) |
|
|
|
messages = ["Optimization terminated successfully.", |
|
"Iteration limit reached.", |
|
"The problem appears infeasible, as the phase one auxiliary " |
|
"problem terminated successfully with a residual of {0:.1e}, " |
|
"greater than the tolerance {1} required for the solution to " |
|
"be considered feasible. Consider increasing the tolerance to " |
|
"be greater than {0:.1e}. If this tolerance is unacceptably " |
|
"large, the problem is likely infeasible.", |
|
"The problem is unbounded, as the simplex algorithm found " |
|
"a basic feasible solution from which there is a direction " |
|
"with negative reduced cost in which all decision variables " |
|
"increase.", |
|
"Numerical difficulties encountered; consider trying " |
|
"method='interior-point'.", |
|
"Problems with no constraints are trivially solved; please " |
|
"turn presolve on.", |
|
"The guess x0 cannot be converted to a basic feasible " |
|
"solution. " |
|
] |
|
|
|
if A.size == 0: |
|
return np.zeros(c.shape), 5, messages[5], 0 |
|
|
|
x, basis, A, b, residual, status, iteration = ( |
|
_phase_one(A, b, x0, callback, postsolve_args, |
|
maxiter, tol, disp, maxupdate, mast, pivot)) |
|
|
|
if status == 0: |
|
x, basis, status, iteration = _phase_two(c, A, x, basis, callback, |
|
postsolve_args, |
|
maxiter, tol, disp, |
|
maxupdate, mast, pivot, |
|
iteration) |
|
|
|
return x, status, messages[status].format(residual, tol), iteration |
|
|