123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- """Simplex method for linear programming
- The *simplex* method uses a traditional, full-tableau implementation of
- Dantzig's simplex algorithm [1]_, [2]_ (*not* the Nelder-Mead simplex).
- This algorithm is included for backwards compatibility and educational
- purposes.
- .. versionadded:: 0.15.0
- Warnings
- --------
- The simplex method may encounter numerical difficulties when pivot
- values are close to the specified tolerance. If encountered try
- remove any redundant constraints, change the pivot strategy to Bland's
- rule or increase the tolerance value.
- Alternatively, more robust methods maybe be used. See
- :ref:`'interior-point' <optimize.linprog-interior-point>` and
- :ref:`'revised simplex' <optimize.linprog-revised_simplex>`.
- References
- ----------
- .. [1] Dantzig, George B., Linear programming and extensions. Rand
- Corporation Research Study Princeton Univ. Press, Princeton, NJ,
- 1963
- .. [2] Hillier, S.H. and Lieberman, G.J. (1995), "Introduction to
- Mathematical Programming", McGraw-Hill, Chapter 4.
- """
- import numpy as np
- from warnings import warn
- from ._optimize import OptimizeResult, OptimizeWarning, _check_unknown_options
- from ._linprog_util import _postsolve
- def _pivot_col(T, tol=1e-9, bland=False):
- """
- Given a linear programming simplex tableau, determine the column
- of the variable to enter the basis.
- Parameters
- ----------
- T : 2-D array
- A 2-D array representing the simplex tableau, T, corresponding to the
- linear programming problem. It should have the form:
- [[A[0, 0], A[0, 1], ..., A[0, n_total], b[0]],
- [A[1, 0], A[1, 1], ..., A[1, n_total], b[1]],
- .
- .
- .
- [A[m, 0], A[m, 1], ..., A[m, n_total], b[m]],
- [c[0], c[1], ..., c[n_total], 0]]
- for a Phase 2 problem, or the form:
- [[A[0, 0], A[0, 1], ..., A[0, n_total], b[0]],
- [A[1, 0], A[1, 1], ..., A[1, n_total], b[1]],
- .
- .
- .
- [A[m, 0], A[m, 1], ..., A[m, n_total], b[m]],
- [c[0], c[1], ..., c[n_total], 0],
- [c'[0], c'[1], ..., c'[n_total], 0]]
- for a Phase 1 problem (a problem in which a basic feasible solution is
- sought prior to maximizing the actual objective. ``T`` is modified in
- place by ``_solve_simplex``.
- tol : float
- Elements in the objective row larger than -tol will not be considered
- for pivoting. Nominally this value is zero, but numerical issues
- cause a tolerance about zero to be necessary.
- bland : bool
- If True, use Bland's rule for selection of the column (select the
- first column with a negative coefficient in the objective row,
- regardless of magnitude).
- Returns
- -------
- status: bool
- True if a suitable pivot column was found, otherwise False.
- A return of False indicates that the linear programming simplex
- algorithm is complete.
- col: int
- The index of the column of the pivot element.
- If status is False, col will be returned as nan.
- """
- ma = np.ma.masked_where(T[-1, :-1] >= -tol, T[-1, :-1], copy=False)
- if ma.count() == 0:
- return False, np.nan
- if bland:
- # ma.mask is sometimes 0d
- return True, np.nonzero(np.logical_not(np.atleast_1d(ma.mask)))[0][0]
- return True, np.ma.nonzero(ma == ma.min())[0][0]
- def _pivot_row(T, basis, pivcol, phase, tol=1e-9, bland=False):
- """
- Given a linear programming simplex tableau, determine the row for the
- pivot operation.
- Parameters
- ----------
- T : 2-D array
- A 2-D array representing the simplex tableau, T, corresponding to the
- linear programming problem. It should have the form:
- [[A[0, 0], A[0, 1], ..., A[0, n_total], b[0]],
- [A[1, 0], A[1, 1], ..., A[1, n_total], b[1]],
- .
- .
- .
- [A[m, 0], A[m, 1], ..., A[m, n_total], b[m]],
- [c[0], c[1], ..., c[n_total], 0]]
- for a Phase 2 problem, or the form:
- [[A[0, 0], A[0, 1], ..., A[0, n_total], b[0]],
- [A[1, 0], A[1, 1], ..., A[1, n_total], b[1]],
- .
- .
- .
- [A[m, 0], A[m, 1], ..., A[m, n_total], b[m]],
- [c[0], c[1], ..., c[n_total], 0],
- [c'[0], c'[1], ..., c'[n_total], 0]]
- for a Phase 1 problem (a Problem in which a basic feasible solution is
- sought prior to maximizing the actual objective. ``T`` is modified in
- place by ``_solve_simplex``.
- basis : array
- A list of the current basic variables.
- pivcol : int
- The index of the pivot column.
- phase : int
- The phase of the simplex algorithm (1 or 2).
- tol : float
- Elements in the pivot column smaller than tol will not be considered
- for pivoting. Nominally this value is zero, but numerical issues
- cause a tolerance about zero to be necessary.
- bland : bool
- If True, use Bland's rule for selection of the row (if more than one
- row can be used, choose the one with the lowest variable index).
- Returns
- -------
- status: bool
- True if a suitable pivot row was found, otherwise False. A return
- of False indicates that the linear programming problem is unbounded.
- row: int
- The index of the row of the pivot element. If status is False, row
- will be returned as nan.
- """
- if phase == 1:
- k = 2
- else:
- k = 1
- ma = np.ma.masked_where(T[:-k, pivcol] <= tol, T[:-k, pivcol], copy=False)
- if ma.count() == 0:
- return False, np.nan
- mb = np.ma.masked_where(T[:-k, pivcol] <= tol, T[:-k, -1], copy=False)
- q = mb / ma
- min_rows = np.ma.nonzero(q == q.min())[0]
- if bland:
- return True, min_rows[np.argmin(np.take(basis, min_rows))]
- return True, min_rows[0]
- def _apply_pivot(T, basis, pivrow, pivcol, tol=1e-9):
- """
- Pivot the simplex tableau inplace on the element given by (pivrow, pivol).
- The entering variable corresponds to the column given by pivcol forcing
- the variable basis[pivrow] to leave the basis.
- Parameters
- ----------
- T : 2-D array
- A 2-D array representing the simplex tableau, T, corresponding to the
- linear programming problem. It should have the form:
- [[A[0, 0], A[0, 1], ..., A[0, n_total], b[0]],
- [A[1, 0], A[1, 1], ..., A[1, n_total], b[1]],
- .
- .
- .
- [A[m, 0], A[m, 1], ..., A[m, n_total], b[m]],
- [c[0], c[1], ..., c[n_total], 0]]
- for a Phase 2 problem, or the form:
- [[A[0, 0], A[0, 1], ..., A[0, n_total], b[0]],
- [A[1, 0], A[1, 1], ..., A[1, n_total], b[1]],
- .
- .
- .
- [A[m, 0], A[m, 1], ..., A[m, n_total], b[m]],
- [c[0], c[1], ..., c[n_total], 0],
- [c'[0], c'[1], ..., c'[n_total], 0]]
- for a Phase 1 problem (a problem in which a basic feasible solution is
- sought prior to maximizing the actual objective. ``T`` is modified in
- place by ``_solve_simplex``.
- basis : 1-D array
- An array of the indices of the basic variables, such that basis[i]
- contains the column corresponding to the basic variable for row i.
- Basis is modified in place by _apply_pivot.
- pivrow : int
- Row index of the pivot.
- pivcol : int
- Column index of the pivot.
- """
- basis[pivrow] = pivcol
- pivval = T[pivrow, pivcol]
- T[pivrow] = T[pivrow] / pivval
- for irow in range(T.shape[0]):
- if irow != pivrow:
- T[irow] = T[irow] - T[pivrow] * T[irow, pivcol]
- # The selected pivot should never lead to a pivot value less than the tol.
- if np.isclose(pivval, tol, atol=0, rtol=1e4):
- message = (
- "The pivot operation produces a pivot value of:{0: .1e}, "
- "which is only slightly greater than the specified "
- "tolerance{1: .1e}. This may lead to issues regarding the "
- "numerical stability of the simplex method. "
- "Removing redundant constraints, changing the pivot strategy "
- "via Bland's rule or increasing the tolerance may "
- "help reduce the issue.".format(pivval, tol))
- warn(message, OptimizeWarning, stacklevel=5)
- def _solve_simplex(T, n, basis, callback, postsolve_args,
- maxiter=1000, tol=1e-9, phase=2, bland=False, nit0=0,
- ):
- """
- Solve a linear programming problem in "standard form" using the Simplex
- Method. Linear Programming is intended to solve the following problem form:
- Minimize::
- c @ x
- Subject to::
- A @ x == b
- x >= 0
- Parameters
- ----------
- T : 2-D array
- A 2-D array representing the simplex tableau, T, corresponding to the
- linear programming problem. It should have the form:
- [[A[0, 0], A[0, 1], ..., A[0, n_total], b[0]],
- [A[1, 0], A[1, 1], ..., A[1, n_total], b[1]],
- .
- .
- .
- [A[m, 0], A[m, 1], ..., A[m, n_total], b[m]],
- [c[0], c[1], ..., c[n_total], 0]]
- for a Phase 2 problem, or the form:
- [[A[0, 0], A[0, 1], ..., A[0, n_total], b[0]],
- [A[1, 0], A[1, 1], ..., A[1, n_total], b[1]],
- .
- .
- .
- [A[m, 0], A[m, 1], ..., A[m, n_total], b[m]],
- [c[0], c[1], ..., c[n_total], 0],
- [c'[0], c'[1], ..., c'[n_total], 0]]
- for a Phase 1 problem (a problem in which a basic feasible solution is
- sought prior to maximizing the actual objective. ``T`` is modified in
- place by ``_solve_simplex``.
- n : int
- The number of true variables in the problem.
- basis : 1-D array
- An array of the indices of the basic variables, such that basis[i]
- contains the column corresponding to the basic variable for row i.
- Basis is modified in place by _solve_simplex
- callback : callable, optional
- If a callback function is provided, it will be called within each
- iteration of the algorithm. The callback must accept a
- `scipy.optimize.OptimizeResult` consisting of the following fields:
- x : 1-D array
- Current solution vector
- fun : float
- Current value of the objective function
- success : bool
- True only when a phase has completed successfully. This
- will be False for most iterations.
- 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 optimization being executed. In phase 1 a basic
- feasible solution is sought and the T has an additional row
- representing an alternate objective function.
- 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 : Serious numerical difficulties encountered
- 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.
- maxiter : int
- The maximum number of iterations to perform before aborting the
- optimization.
- 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.
- phase : int
- The phase of the optimization being executed. In phase 1 a basic
- feasible solution is sought and the T has an additional row
- representing an alternate objective function.
- bland : bool
- If True, choose pivots using Bland's rule [3]_. In problems which
- fail to converge due to cycling, using Bland's rule can provide
- convergence at the expense of a less optimal path about the simplex.
- nit0 : int
- The initial iteration number used to keep an accurate iteration total
- in a two-phase problem.
- Returns
- -------
- nit : int
- The number of iterations. Used to keep an accurate iteration total
- in the two-phase problem.
- 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 : Serious numerical difficulties encountered
- """
- nit = nit0
- status = 0
- message = ''
- complete = False
- if phase == 1:
- m = T.shape[1]-2
- elif phase == 2:
- m = T.shape[1]-1
- else:
- raise ValueError("Argument 'phase' to _solve_simplex must be 1 or 2")
- if phase == 2:
- # Check if any artificial variables are still in the basis.
- # If yes, check if any coefficients from this row and a column
- # corresponding to one of the non-artificial variable is non-zero.
- # If found, pivot at this term. If not, start phase 2.
- # Do this for all artificial variables in the basis.
- # Ref: "An Introduction to Linear Programming and Game Theory"
- # by Paul R. Thie, Gerard E. Keough, 3rd Ed,
- # Chapter 3.7 Redundant Systems (pag 102)
- for pivrow in [row for row in range(basis.size)
- if basis[row] > T.shape[1] - 2]:
- non_zero_row = [col for col in range(T.shape[1] - 1)
- if abs(T[pivrow, col]) > tol]
- if len(non_zero_row) > 0:
- pivcol = non_zero_row[0]
- _apply_pivot(T, basis, pivrow, pivcol, tol)
- nit += 1
- if len(basis[:m]) == 0:
- solution = np.empty(T.shape[1] - 1, dtype=np.float64)
- else:
- solution = np.empty(max(T.shape[1] - 1, max(basis[:m]) + 1),
- dtype=np.float64)
- while not complete:
- # Find the pivot column
- pivcol_found, pivcol = _pivot_col(T, tol, bland)
- if not pivcol_found:
- pivcol = np.nan
- pivrow = np.nan
- status = 0
- complete = True
- else:
- # Find the pivot row
- pivrow_found, pivrow = _pivot_row(T, basis, pivcol, phase, tol, bland)
- if not pivrow_found:
- status = 3
- complete = True
- if callback is not None:
- solution[:] = 0
- solution[basis[:n]] = T[:n, -1]
- x = solution[:m]
- x, fun, slack, con = _postsolve(
- x, postsolve_args
- )
- res = OptimizeResult({
- 'x': x,
- 'fun': fun,
- 'slack': slack,
- 'con': con,
- 'status': status,
- 'message': message,
- 'nit': nit,
- 'success': status == 0 and complete,
- 'phase': phase,
- 'complete': complete,
- })
- callback(res)
- if not complete:
- if nit >= maxiter:
- # Iteration limit exceeded
- status = 1
- complete = True
- else:
- _apply_pivot(T, basis, pivrow, pivcol, tol)
- nit += 1
- return nit, status
- def _linprog_simplex(c, c0, A, b, callback, postsolve_args,
- maxiter=1000, tol=1e-9, disp=False, bland=False,
- **unknown_options):
- """
- Minimize a linear objective function subject to linear equality and
- non-negativity constraints using the two phase simplex method.
- Linear programming is intended to solve problems of the following form:
- Minimize::
- c @ x
- Subject to::
- A @ x == b
- x >= 0
- 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. (Purely for display.)
- A : 2-D array
- 2-D array such that ``A @ x``, gives the values of the equality
- constraints at ``x``.
- b : 1-D array
- 1-D array of values representing the right hand side of each equality
- constraint (row) in ``A``.
- 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
- success : bool
- True when an algorithm has completed successfully.
- 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
- An integer representing the status of the optimization::
- 0 : Algorithm proceeding nominally
- 1 : Iteration limit reached
- 2 : Problem appears to be infeasible
- 3 : Problem appears to be unbounded
- 4 : Serious numerical difficulties encountered
- 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.
- disp : bool
- If True, print exit status message to sys.stdout
- 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.
- bland : bool
- If True, use Bland's anti-cycling rule [3]_ to choose pivots to
- prevent cycling. If False, choose pivots which should lead to a
- converged solution more quickly. The latter method is subject to
- cycling (non-convergence) in rare instances.
- 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 : Serious numerical difficulties encountered
- message : str
- A string descriptor of the exit status of the optimization.
- iteration : int
- The number of iterations taken to solve the problem.
- References
- ----------
- .. [1] Dantzig, George B., Linear programming and extensions. Rand
- Corporation Research Study Princeton Univ. Press, Princeton, NJ,
- 1963
- .. [2] Hillier, S.H. and Lieberman, G.J. (1995), "Introduction to
- Mathematical Programming", McGraw-Hill, Chapter 4.
- .. [3] Bland, Robert G. New finite pivoting rules for the simplex method.
- Mathematics of Operations Research (2), 1977: pp. 103-107.
- Notes
- -----
- The expected problem formulation differs between the top level ``linprog``
- module and the method specific solvers. The method specific solvers expect a
- problem in standard form:
- Minimize::
- c @ x
- Subject to::
- A @ x == b
- x >= 0
- Whereas the top level ``linprog`` module expects a problem of form:
- Minimize::
- c @ x
- Subject to::
- A_ub @ x <= b_ub
- A_eq @ x == b_eq
- lb <= x <= ub
- where ``lb = 0`` and ``ub = None`` unless set in ``bounds``.
- The original problem contains equality, upper-bound and variable constraints
- whereas the method specific solver requires equality constraints and
- variable non-negativity.
- ``linprog`` module converts the original problem to standard form by
- converting the simple bounds to upper bound constraints, introducing
- non-negative slack variables for inequality constraints, and expressing
- unbounded variables as the difference between two non-negative variables.
- """
- _check_unknown_options(unknown_options)
- status = 0
- messages = {0: "Optimization terminated successfully.",
- 1: "Iteration limit reached.",
- 2: "Optimization failed. Unable to find a feasible"
- " starting point.",
- 3: "Optimization failed. The problem appears to be unbounded.",
- 4: "Optimization failed. Singular matrix encountered."}
- n, m = A.shape
- # All constraints must have b >= 0.
- is_negative_constraint = np.less(b, 0)
- A[is_negative_constraint] *= -1
- b[is_negative_constraint] *= -1
- # As all constraints are equality constraints the artificial variables
- # will also be basic variables.
- av = np.arange(n) + m
- basis = av.copy()
- # Format the phase one tableau by adding artificial variables and stacking
- # the constraints, the objective row and pseudo-objective row.
- row_constraints = np.hstack((A, np.eye(n), b[:, np.newaxis]))
- row_objective = np.hstack((c, np.zeros(n), c0))
- row_pseudo_objective = -row_constraints.sum(axis=0)
- row_pseudo_objective[av] = 0
- T = np.vstack((row_constraints, row_objective, row_pseudo_objective))
- nit1, status = _solve_simplex(T, n, basis, callback=callback,
- postsolve_args=postsolve_args,
- maxiter=maxiter, tol=tol, phase=1,
- bland=bland
- )
- # if pseudo objective is zero, remove the last row from the tableau and
- # proceed to phase 2
- nit2 = nit1
- if abs(T[-1, -1]) < tol:
- # Remove the pseudo-objective row from the tableau
- T = T[:-1, :]
- # Remove the artificial variable columns from the tableau
- T = np.delete(T, av, 1)
- else:
- # Failure to find a feasible starting point
- status = 2
- messages[status] = (
- "Phase 1 of the simplex method failed to find a feasible "
- "solution. The pseudo-objective function evaluates to {0:.1e} "
- "which exceeds the required tolerance of {1} for a solution to be "
- "considered 'close enough' to zero to be a basic solution. "
- "Consider increasing the tolerance to be greater than {0:.1e}. "
- "If this tolerance is unacceptably large the problem may be "
- "infeasible.".format(abs(T[-1, -1]), tol)
- )
- if status == 0:
- # Phase 2
- nit2, status = _solve_simplex(T, n, basis, callback=callback,
- postsolve_args=postsolve_args,
- maxiter=maxiter, tol=tol, phase=2,
- bland=bland, nit0=nit1
- )
- solution = np.zeros(n + m)
- solution[basis[:n]] = T[:n, -1]
- x = solution[:m]
- return x, status, messages[status], int(nit2)
|