some new features
This commit is contained in:
392
.venv/lib/python3.12/site-packages/scipy/optimize/_milp.py
Normal file
392
.venv/lib/python3.12/site-packages/scipy/optimize/_milp.py
Normal file
@ -0,0 +1,392 @@
|
||||
import warnings
|
||||
import numpy as np
|
||||
from scipy.sparse import csc_array, vstack, issparse
|
||||
from scipy._lib._util import VisibleDeprecationWarning
|
||||
from ._highs._highs_wrapper import _highs_wrapper # type: ignore[import-not-found,import-untyped]
|
||||
from ._constraints import LinearConstraint, Bounds
|
||||
from ._optimize import OptimizeResult
|
||||
from ._linprog_highs import _highs_to_scipy_status_message
|
||||
|
||||
|
||||
def _constraints_to_components(constraints):
|
||||
"""
|
||||
Convert sequence of constraints to a single set of components A, b_l, b_u.
|
||||
|
||||
`constraints` could be
|
||||
|
||||
1. A LinearConstraint
|
||||
2. A tuple representing a LinearConstraint
|
||||
3. An invalid object
|
||||
4. A sequence of composed entirely of objects of type 1/2
|
||||
5. A sequence containing at least one object of type 3
|
||||
|
||||
We want to accept 1, 2, and 4 and reject 3 and 5.
|
||||
"""
|
||||
message = ("`constraints` (or each element within `constraints`) must be "
|
||||
"convertible into an instance of "
|
||||
"`scipy.optimize.LinearConstraint`.")
|
||||
As = []
|
||||
b_ls = []
|
||||
b_us = []
|
||||
|
||||
# Accept case 1 by standardizing as case 4
|
||||
if isinstance(constraints, LinearConstraint):
|
||||
constraints = [constraints]
|
||||
else:
|
||||
# Reject case 3
|
||||
try:
|
||||
iter(constraints)
|
||||
except TypeError as exc:
|
||||
raise ValueError(message) from exc
|
||||
|
||||
# Accept case 2 by standardizing as case 4
|
||||
if len(constraints) == 3:
|
||||
# argument could be a single tuple representing a LinearConstraint
|
||||
try:
|
||||
constraints = [LinearConstraint(*constraints)]
|
||||
except (TypeError, ValueError, VisibleDeprecationWarning):
|
||||
# argument was not a tuple representing a LinearConstraint
|
||||
pass
|
||||
|
||||
# Address cases 4/5
|
||||
for constraint in constraints:
|
||||
# if it's not a LinearConstraint or something that represents a
|
||||
# LinearConstraint at this point, it's invalid
|
||||
if not isinstance(constraint, LinearConstraint):
|
||||
try:
|
||||
constraint = LinearConstraint(*constraint)
|
||||
except TypeError as exc:
|
||||
raise ValueError(message) from exc
|
||||
As.append(csc_array(constraint.A))
|
||||
b_ls.append(np.atleast_1d(constraint.lb).astype(np.float64))
|
||||
b_us.append(np.atleast_1d(constraint.ub).astype(np.float64))
|
||||
|
||||
if len(As) > 1:
|
||||
A = vstack(As, format="csc")
|
||||
b_l = np.concatenate(b_ls)
|
||||
b_u = np.concatenate(b_us)
|
||||
else: # avoid unnecessary copying
|
||||
A = As[0]
|
||||
b_l = b_ls[0]
|
||||
b_u = b_us[0]
|
||||
|
||||
return A, b_l, b_u
|
||||
|
||||
|
||||
def _milp_iv(c, integrality, bounds, constraints, options):
|
||||
# objective IV
|
||||
if issparse(c):
|
||||
raise ValueError("`c` must be a dense array.")
|
||||
c = np.atleast_1d(c).astype(np.float64)
|
||||
if c.ndim != 1 or c.size == 0 or not np.all(np.isfinite(c)):
|
||||
message = ("`c` must be a one-dimensional array of finite numbers "
|
||||
"with at least one element.")
|
||||
raise ValueError(message)
|
||||
|
||||
# integrality IV
|
||||
if issparse(integrality):
|
||||
raise ValueError("`integrality` must be a dense array.")
|
||||
message = ("`integrality` must contain integers 0-3 and be broadcastable "
|
||||
"to `c.shape`.")
|
||||
if integrality is None:
|
||||
integrality = 0
|
||||
try:
|
||||
integrality = np.broadcast_to(integrality, c.shape).astype(np.uint8)
|
||||
except ValueError:
|
||||
raise ValueError(message)
|
||||
if integrality.min() < 0 or integrality.max() > 3:
|
||||
raise ValueError(message)
|
||||
|
||||
# bounds IV
|
||||
if bounds is None:
|
||||
bounds = Bounds(0, np.inf)
|
||||
elif not isinstance(bounds, Bounds):
|
||||
message = ("`bounds` must be convertible into an instance of "
|
||||
"`scipy.optimize.Bounds`.")
|
||||
try:
|
||||
bounds = Bounds(*bounds)
|
||||
except TypeError as exc:
|
||||
raise ValueError(message) from exc
|
||||
|
||||
try:
|
||||
lb = np.broadcast_to(bounds.lb, c.shape).astype(np.float64)
|
||||
ub = np.broadcast_to(bounds.ub, c.shape).astype(np.float64)
|
||||
except (ValueError, TypeError) as exc:
|
||||
message = ("`bounds.lb` and `bounds.ub` must contain reals and "
|
||||
"be broadcastable to `c.shape`.")
|
||||
raise ValueError(message) from exc
|
||||
|
||||
# constraints IV
|
||||
if not constraints:
|
||||
constraints = [LinearConstraint(np.empty((0, c.size)),
|
||||
np.empty((0,)), np.empty((0,)))]
|
||||
try:
|
||||
A, b_l, b_u = _constraints_to_components(constraints)
|
||||
except ValueError as exc:
|
||||
message = ("`constraints` (or each element within `constraints`) must "
|
||||
"be convertible into an instance of "
|
||||
"`scipy.optimize.LinearConstraint`.")
|
||||
raise ValueError(message) from exc
|
||||
|
||||
if A.shape != (b_l.size, c.size):
|
||||
message = "The shape of `A` must be (len(b_l), len(c))."
|
||||
raise ValueError(message)
|
||||
indptr, indices, data = A.indptr, A.indices, A.data.astype(np.float64)
|
||||
|
||||
# options IV
|
||||
options = options or {}
|
||||
supported_options = {'disp', 'presolve', 'time_limit', 'node_limit',
|
||||
'mip_rel_gap'}
|
||||
unsupported_options = set(options).difference(supported_options)
|
||||
if unsupported_options:
|
||||
message = (f"Unrecognized options detected: {unsupported_options}. "
|
||||
"These will be passed to HiGHS verbatim.")
|
||||
warnings.warn(message, RuntimeWarning, stacklevel=3)
|
||||
options_iv = {'log_to_console': options.pop("disp", False),
|
||||
'mip_max_nodes': options.pop("node_limit", None)}
|
||||
options_iv.update(options)
|
||||
|
||||
return c, integrality, lb, ub, indptr, indices, data, b_l, b_u, options_iv
|
||||
|
||||
|
||||
def milp(c, *, integrality=None, bounds=None, constraints=None, options=None):
|
||||
r"""
|
||||
Mixed-integer linear programming
|
||||
|
||||
Solves problems of the following form:
|
||||
|
||||
.. math::
|
||||
|
||||
\min_x \ & c^T x \\
|
||||
\mbox{such that} \ & b_l \leq A x \leq b_u,\\
|
||||
& l \leq x \leq u, \\
|
||||
& x_i \in \mathbb{Z}, i \in X_i
|
||||
|
||||
where :math:`x` is a vector of decision variables;
|
||||
:math:`c`, :math:`b_l`, :math:`b_u`, :math:`l`, and :math:`u` are vectors;
|
||||
:math:`A` is a matrix, and :math:`X_i` is the set of indices of
|
||||
decision variables that must be integral. (In this context, a
|
||||
variable that can assume only integer values is said to be "integral";
|
||||
it has an "integrality" constraint.)
|
||||
|
||||
Alternatively, that's:
|
||||
|
||||
minimize::
|
||||
|
||||
c @ x
|
||||
|
||||
such that::
|
||||
|
||||
b_l <= A @ x <= b_u
|
||||
l <= x <= u
|
||||
Specified elements of x must be integers
|
||||
|
||||
By default, ``l = 0`` and ``u = np.inf`` unless specified with
|
||||
``bounds``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
c : 1D dense array_like
|
||||
The coefficients of the linear objective function to be minimized.
|
||||
`c` is converted to a double precision array before the problem is
|
||||
solved.
|
||||
integrality : 1D dense array_like, optional
|
||||
Indicates the type of integrality constraint on each decision variable.
|
||||
|
||||
``0`` : Continuous variable; no integrality constraint.
|
||||
|
||||
``1`` : Integer variable; decision variable must be an integer
|
||||
within `bounds`.
|
||||
|
||||
``2`` : Semi-continuous variable; decision variable must be within
|
||||
`bounds` or take value ``0``.
|
||||
|
||||
``3`` : Semi-integer variable; decision variable must be an integer
|
||||
within `bounds` or take value ``0``.
|
||||
|
||||
By default, all variables are continuous. `integrality` is converted
|
||||
to an array of integers before the problem is solved.
|
||||
|
||||
bounds : scipy.optimize.Bounds, optional
|
||||
Bounds on the decision variables. Lower and upper bounds are converted
|
||||
to double precision arrays before the problem is solved. The
|
||||
``keep_feasible`` parameter of the `Bounds` object is ignored. If
|
||||
not specified, all decision variables are constrained to be
|
||||
non-negative.
|
||||
constraints : sequence of scipy.optimize.LinearConstraint, optional
|
||||
Linear constraints of the optimization problem. Arguments may be
|
||||
one of the following:
|
||||
|
||||
1. A single `LinearConstraint` object
|
||||
2. A single tuple that can be converted to a `LinearConstraint` object
|
||||
as ``LinearConstraint(*constraints)``
|
||||
3. A sequence composed entirely of objects of type 1. and 2.
|
||||
|
||||
Before the problem is solved, all values are converted to double
|
||||
precision, and the matrices of constraint coefficients are converted to
|
||||
instances of `scipy.sparse.csc_array`. The ``keep_feasible`` parameter
|
||||
of `LinearConstraint` objects is ignored.
|
||||
options : dict, optional
|
||||
A dictionary of solver options. The following keys are recognized.
|
||||
|
||||
disp : bool (default: ``False``)
|
||||
Set to ``True`` if indicators of optimization status are to be
|
||||
printed to the console during optimization.
|
||||
node_limit : int, optional
|
||||
The maximum number of nodes (linear program relaxations) to solve
|
||||
before stopping. Default is no maximum number of nodes.
|
||||
presolve : bool (default: ``True``)
|
||||
Presolve attempts to identify trivial infeasibilities,
|
||||
identify trivial unboundedness, and simplify the problem before
|
||||
sending it to the main solver.
|
||||
time_limit : float, optional
|
||||
The maximum number of seconds allotted to solve the problem.
|
||||
Default is no time limit.
|
||||
mip_rel_gap : float, optional
|
||||
Termination criterion for MIP solver: solver will terminate when
|
||||
the gap between the primal objective value and the dual objective
|
||||
bound, scaled by the primal objective value, is <= mip_rel_gap.
|
||||
|
||||
Returns
|
||||
-------
|
||||
res : OptimizeResult
|
||||
An instance of :class:`scipy.optimize.OptimizeResult`. The object
|
||||
is guaranteed to have the following attributes.
|
||||
|
||||
status : int
|
||||
An integer representing the exit status of the algorithm.
|
||||
|
||||
``0`` : Optimal solution found.
|
||||
|
||||
``1`` : Iteration or time limit reached.
|
||||
|
||||
``2`` : Problem is infeasible.
|
||||
|
||||
``3`` : Problem is unbounded.
|
||||
|
||||
``4`` : Other; see message for details.
|
||||
|
||||
success : bool
|
||||
``True`` when an optimal solution is found and ``False`` otherwise.
|
||||
|
||||
message : str
|
||||
A string descriptor of the exit status of the algorithm.
|
||||
|
||||
The following attributes will also be present, but the values may be
|
||||
``None``, depending on the solution status.
|
||||
|
||||
x : ndarray
|
||||
The values of the decision variables that minimize the
|
||||
objective function while satisfying the constraints.
|
||||
fun : float
|
||||
The optimal value of the objective function ``c @ x``.
|
||||
mip_node_count : int
|
||||
The number of subproblems or "nodes" solved by the MILP solver.
|
||||
mip_dual_bound : float
|
||||
The MILP solver's final estimate of the lower bound on the optimal
|
||||
solution.
|
||||
mip_gap : float
|
||||
The difference between the primal objective value and the dual
|
||||
objective bound, scaled by the primal objective value.
|
||||
|
||||
Notes
|
||||
-----
|
||||
`milp` is a wrapper of the HiGHS linear optimization software [1]_. The
|
||||
algorithm is deterministic, and it typically finds the global optimum of
|
||||
moderately challenging mixed-integer linear programs (when it exists).
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Huangfu, Q., Galabova, I., Feldmeier, M., and Hall, J. A. J.
|
||||
"HiGHS - high performance software for linear optimization."
|
||||
https://highs.dev/
|
||||
.. [2] Huangfu, Q. and Hall, J. A. J. "Parallelizing the dual revised
|
||||
simplex method." Mathematical Programming Computation, 10 (1),
|
||||
119-142, 2018. DOI: 10.1007/s12532-017-0130-5
|
||||
|
||||
Examples
|
||||
--------
|
||||
Consider the problem at
|
||||
https://en.wikipedia.org/wiki/Integer_programming#Example, which is
|
||||
expressed as a maximization problem of two variables. Since `milp` requires
|
||||
that the problem be expressed as a minimization problem, the objective
|
||||
function coefficients on the decision variables are:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> c = -np.array([0, 1])
|
||||
|
||||
Note the negative sign: we maximize the original objective function
|
||||
by minimizing the negative of the objective function.
|
||||
|
||||
We collect the coefficients of the constraints into arrays like:
|
||||
|
||||
>>> A = np.array([[-1, 1], [3, 2], [2, 3]])
|
||||
>>> b_u = np.array([1, 12, 12])
|
||||
>>> b_l = np.full_like(b_u, -np.inf, dtype=float)
|
||||
|
||||
Because there is no lower limit on these constraints, we have defined a
|
||||
variable ``b_l`` full of values representing negative infinity. This may
|
||||
be unfamiliar to users of `scipy.optimize.linprog`, which only accepts
|
||||
"less than" (or "upper bound") inequality constraints of the form
|
||||
``A_ub @ x <= b_u``. By accepting both ``b_l`` and ``b_u`` of constraints
|
||||
``b_l <= A_ub @ x <= b_u``, `milp` makes it easy to specify "greater than"
|
||||
inequality constraints, "less than" inequality constraints, and equality
|
||||
constraints concisely.
|
||||
|
||||
These arrays are collected into a single `LinearConstraint` object like:
|
||||
|
||||
>>> from scipy.optimize import LinearConstraint
|
||||
>>> constraints = LinearConstraint(A, b_l, b_u)
|
||||
|
||||
The non-negativity bounds on the decision variables are enforced by
|
||||
default, so we do not need to provide an argument for `bounds`.
|
||||
|
||||
Finally, the problem states that both decision variables must be integers:
|
||||
|
||||
>>> integrality = np.ones_like(c)
|
||||
|
||||
We solve the problem like:
|
||||
|
||||
>>> from scipy.optimize import milp
|
||||
>>> res = milp(c=c, constraints=constraints, integrality=integrality)
|
||||
>>> res.x
|
||||
[2.0, 2.0]
|
||||
|
||||
Note that had we solved the relaxed problem (without integrality
|
||||
constraints):
|
||||
|
||||
>>> res = milp(c=c, constraints=constraints) # OR:
|
||||
>>> # from scipy.optimize import linprog; res = linprog(c, A, b_u)
|
||||
>>> res.x
|
||||
[1.8, 2.8]
|
||||
|
||||
we would not have obtained the correct solution by rounding to the nearest
|
||||
integers.
|
||||
|
||||
Other examples are given :ref:`in the tutorial <tutorial-optimize_milp>`.
|
||||
|
||||
"""
|
||||
args_iv = _milp_iv(c, integrality, bounds, constraints, options)
|
||||
c, integrality, lb, ub, indptr, indices, data, b_l, b_u, options = args_iv
|
||||
|
||||
highs_res = _highs_wrapper(c, indptr, indices, data, b_l, b_u,
|
||||
lb, ub, integrality, options)
|
||||
|
||||
res = {}
|
||||
|
||||
# Convert to scipy-style status and message
|
||||
highs_status = highs_res.get('status', None)
|
||||
highs_message = highs_res.get('message', None)
|
||||
status, message = _highs_to_scipy_status_message(highs_status,
|
||||
highs_message)
|
||||
res['status'] = status
|
||||
res['message'] = message
|
||||
res['success'] = (status == 0)
|
||||
x = highs_res.get('x', None)
|
||||
res['x'] = np.array(x) if x is not None else None
|
||||
res['fun'] = highs_res.get('fun', None)
|
||||
res['mip_node_count'] = highs_res.get('mip_node_count', None)
|
||||
res['mip_dual_bound'] = highs_res.get('mip_dual_bound', None)
|
||||
res['mip_gap'] = highs_res.get('mip_gap', None)
|
||||
|
||||
return OptimizeResult(res)
|
||||
Reference in New Issue
Block a user