add read me
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,338 @@
|
||||
from warnings import warn
|
||||
|
||||
import numpy as np
|
||||
import scipy.optimize._highspy._core as _h # type: ignore[import-not-found]
|
||||
from scipy.optimize._highspy import _highs_options as hopt # type: ignore[attr-defined]
|
||||
from scipy.optimize import OptimizeWarning
|
||||
|
||||
|
||||
def _highs_wrapper(c, indptr, indices, data, lhs, rhs, lb, ub, integrality, options):
|
||||
'''Solve linear programs using HiGHS [1]_.
|
||||
|
||||
Assume problems of the form:
|
||||
|
||||
MIN c.T @ x
|
||||
s.t. lhs <= A @ x <= rhs
|
||||
lb <= x <= ub
|
||||
|
||||
Parameters
|
||||
----------
|
||||
c : 1-D array, (n,)
|
||||
Array of objective value coefficients.
|
||||
astart : 1-D array
|
||||
CSC format index array.
|
||||
aindex : 1-D array
|
||||
CSC format index array.
|
||||
avalue : 1-D array
|
||||
Data array of the matrix.
|
||||
lhs : 1-D array (or None), (m,)
|
||||
Array of left hand side values of the inequality constraints.
|
||||
If ``lhs=None``, then an array of ``-inf`` is assumed.
|
||||
rhs : 1-D array, (m,)
|
||||
Array of right hand side values of the inequality constraints.
|
||||
lb : 1-D array (or None), (n,)
|
||||
Lower bounds on solution variables x. If ``lb=None``, then an
|
||||
array of all `0` is assumed.
|
||||
ub : 1-D array (or None), (n,)
|
||||
Upper bounds on solution variables x. If ``ub=None``, then an
|
||||
array of ``inf`` is assumed.
|
||||
options : dict
|
||||
A dictionary of solver options
|
||||
|
||||
Returns
|
||||
-------
|
||||
res : dict
|
||||
|
||||
If model_status is one of kOptimal,
|
||||
kObjectiveBound, kTimeLimit,
|
||||
kIterationLimit:
|
||||
|
||||
- ``status`` : HighsModelStatus
|
||||
Model status code.
|
||||
|
||||
- ``message`` : str
|
||||
Message corresponding to model status code.
|
||||
|
||||
- ``x`` : list
|
||||
Solution variables.
|
||||
|
||||
- ``slack`` : list
|
||||
Slack variables.
|
||||
|
||||
- ``lambda`` : list
|
||||
Lagrange multipliers associated with the constraints
|
||||
Ax = b.
|
||||
|
||||
- ``s`` : list
|
||||
Lagrange multipliers associated with the constraints
|
||||
x >= 0.
|
||||
|
||||
- ``fun``
|
||||
Final objective value.
|
||||
|
||||
- ``simplex_nit`` : int
|
||||
Number of iterations accomplished by the simplex
|
||||
solver.
|
||||
|
||||
- ``ipm_nit`` : int
|
||||
Number of iterations accomplished by the interior-
|
||||
point solver.
|
||||
|
||||
If model_status is not one of the above:
|
||||
|
||||
- ``status`` : HighsModelStatus
|
||||
Model status code.
|
||||
|
||||
- ``message`` : str
|
||||
Message corresponding to model status code.
|
||||
|
||||
Notes
|
||||
-----
|
||||
If ``options['write_solution_to_file']`` is ``True`` but
|
||||
``options['solution_file']`` is unset or ``''``, then the solution
|
||||
will be printed to ``stdout``.
|
||||
|
||||
If any iteration limit is reached, no solution will be
|
||||
available.
|
||||
|
||||
``OptimizeWarning`` will be raised if any option value set by
|
||||
the user is found to be incorrect.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] https://highs.dev/
|
||||
.. [2] https://www.maths.ed.ac.uk/hall/HiGHS/HighsOptions.html
|
||||
'''
|
||||
numcol = c.size
|
||||
numrow = rhs.size
|
||||
isMip = integrality is not None and np.sum(integrality) > 0
|
||||
|
||||
# default "null" return values
|
||||
res = {
|
||||
"x": None,
|
||||
"fun": None,
|
||||
}
|
||||
|
||||
# Fill up a HighsLp object
|
||||
lp = _h.HighsLp()
|
||||
lp.num_col_ = numcol
|
||||
lp.num_row_ = numrow
|
||||
lp.a_matrix_.num_col_ = numcol
|
||||
lp.a_matrix_.num_row_ = numrow
|
||||
lp.a_matrix_.format_ = _h.MatrixFormat.kColwise
|
||||
lp.col_cost_ = c
|
||||
lp.col_lower_ = lb
|
||||
lp.col_upper_ = ub
|
||||
lp.row_lower_ = lhs
|
||||
lp.row_upper_ = rhs
|
||||
lp.a_matrix_.start_ = indptr
|
||||
lp.a_matrix_.index_ = indices
|
||||
lp.a_matrix_.value_ = data
|
||||
if integrality.size > 0:
|
||||
lp.integrality_ = [_h.HighsVarType(i) for i in integrality]
|
||||
|
||||
# Make a Highs object and pass it everything
|
||||
highs = _h._Highs()
|
||||
highs_options = _h.HighsOptions()
|
||||
hoptmanager = hopt.HighsOptionsManager()
|
||||
for key, val in options.items():
|
||||
# handle filtering of unsupported and default options
|
||||
if val is None or key in ("sense",):
|
||||
continue
|
||||
|
||||
# ask for the option type
|
||||
opt_type = hoptmanager.get_option_type(key)
|
||||
if -1 == opt_type:
|
||||
warn(
|
||||
f"Unrecognized options detected: {dict({key: val})}",
|
||||
OptimizeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
continue
|
||||
else:
|
||||
if key in ("presolve", "parallel"):
|
||||
# handle fake bools (require bool -> str conversions)
|
||||
if isinstance(val, bool):
|
||||
val = "on" if val else "off"
|
||||
else:
|
||||
warn(
|
||||
f'Option f"{key}" is "{val}", but only True or False is '
|
||||
f"allowed. Using default.",
|
||||
OptimizeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
continue
|
||||
opt_type = _h.HighsOptionType(opt_type)
|
||||
status, msg = check_option(highs, key, val)
|
||||
if opt_type == _h.HighsOptionType.kBool:
|
||||
if not isinstance(val, bool):
|
||||
warn(
|
||||
f'Option f"{key}" is "{val}", but only True or False is '
|
||||
f"allowed. Using default.",
|
||||
OptimizeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
continue
|
||||
|
||||
# warn or set option
|
||||
if status != 0:
|
||||
warn(msg, OptimizeWarning, stacklevel=2)
|
||||
else:
|
||||
setattr(highs_options, key, val)
|
||||
|
||||
opt_status = highs.passOptions(highs_options)
|
||||
if opt_status == _h.HighsStatus.kError:
|
||||
res.update(
|
||||
{
|
||||
"status": highs.getModelStatus(),
|
||||
"message": highs.modelStatusToString(highs.getModelStatus()),
|
||||
}
|
||||
)
|
||||
return res
|
||||
|
||||
init_status = highs.passModel(lp)
|
||||
if init_status == _h.HighsStatus.kError:
|
||||
# if model fails to load, highs.getModelStatus() will be NOT_SET
|
||||
err_model_status = _h.HighsModelStatus.kModelError
|
||||
res.update(
|
||||
{
|
||||
"status": err_model_status,
|
||||
"message": highs.modelStatusToString(err_model_status),
|
||||
}
|
||||
)
|
||||
return res
|
||||
|
||||
# Solve the LP
|
||||
run_status = highs.run()
|
||||
if run_status == _h.HighsStatus.kError:
|
||||
res.update(
|
||||
{
|
||||
"status": highs.getModelStatus(),
|
||||
"message": highs.modelStatusToString(highs.getModelStatus()),
|
||||
}
|
||||
)
|
||||
return res
|
||||
|
||||
# Extract what we need from the solution
|
||||
model_status = highs.getModelStatus()
|
||||
|
||||
# it should always be safe to get the info object
|
||||
info = highs.getInfo()
|
||||
|
||||
# Failure modes:
|
||||
# LP: if we have anything other than an Optimal status, it
|
||||
# is unsafe (and unhelpful) to read any results
|
||||
# MIP: has a non-Optimal status or has timed out/reached max iterations
|
||||
# 1) If not Optimal/TimedOut/MaxIter status, there is no solution
|
||||
# 2) If TimedOut/MaxIter status, there may be a feasible solution.
|
||||
# if the objective function value is not Infinity, then the
|
||||
# current solution is feasible and can be returned. Else, there
|
||||
# is no solution.
|
||||
mipFailCondition = model_status not in (
|
||||
_h.HighsModelStatus.kOptimal,
|
||||
_h.HighsModelStatus.kTimeLimit,
|
||||
_h.HighsModelStatus.kIterationLimit,
|
||||
_h.HighsModelStatus.kSolutionLimit,
|
||||
) or (
|
||||
model_status
|
||||
in {
|
||||
_h.HighsModelStatus.kTimeLimit,
|
||||
_h.HighsModelStatus.kIterationLimit,
|
||||
_h.HighsModelStatus.kSolutionLimit,
|
||||
}
|
||||
and (info.objective_function_value == _h.kHighsInf)
|
||||
)
|
||||
lpFailCondition = model_status != _h.HighsModelStatus.kOptimal
|
||||
if (isMip and mipFailCondition) or (not isMip and lpFailCondition):
|
||||
res.update(
|
||||
{
|
||||
"status": model_status,
|
||||
"message": "model_status is "
|
||||
f"{highs.modelStatusToString(model_status)}; "
|
||||
"primal_status is "
|
||||
f"{highs.solutionStatusToString(info.primal_solution_status)}",
|
||||
"simplex_nit": info.simplex_iteration_count,
|
||||
"ipm_nit": info.ipm_iteration_count,
|
||||
"crossover_nit": info.crossover_iteration_count,
|
||||
}
|
||||
)
|
||||
return res
|
||||
|
||||
# Should be safe to read the solution:
|
||||
solution = highs.getSolution()
|
||||
basis = highs.getBasis()
|
||||
|
||||
# Lagrangians for bounds based on column statuses
|
||||
marg_bnds = np.zeros((2, numcol))
|
||||
basis_col_status = basis.col_status
|
||||
solution_col_dual = solution.col_dual
|
||||
for ii in range(numcol):
|
||||
if basis_col_status[ii] == _h.HighsBasisStatus.kLower:
|
||||
marg_bnds[0, ii] = solution_col_dual[ii]
|
||||
elif basis_col_status[ii] == _h.HighsBasisStatus.kUpper:
|
||||
marg_bnds[1, ii] = solution_col_dual[ii]
|
||||
|
||||
res.update(
|
||||
{
|
||||
"status": model_status,
|
||||
"message": highs.modelStatusToString(model_status),
|
||||
# Primal solution
|
||||
"x": np.array(solution.col_value),
|
||||
# Ax + s = b => Ax = b - s
|
||||
# Note: this is for all constraints (A_ub and A_eq)
|
||||
"slack": rhs - solution.row_value,
|
||||
# lambda are the lagrange multipliers associated with Ax=b
|
||||
"lambda": np.array(solution.row_dual),
|
||||
"marg_bnds": marg_bnds,
|
||||
"fun": info.objective_function_value,
|
||||
"simplex_nit": info.simplex_iteration_count,
|
||||
"ipm_nit": info.ipm_iteration_count,
|
||||
"crossover_nit": info.crossover_iteration_count,
|
||||
}
|
||||
)
|
||||
|
||||
if isMip:
|
||||
res.update(
|
||||
{
|
||||
"mip_node_count": info.mip_node_count,
|
||||
"mip_dual_bound": info.mip_dual_bound,
|
||||
"mip_gap": info.mip_gap,
|
||||
}
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def check_option(highs_inst, option, value):
|
||||
status, option_type = highs_inst.getOptionType(option)
|
||||
hoptmanager = hopt.HighsOptionsManager()
|
||||
|
||||
if status != _h.HighsStatus.kOk:
|
||||
return -1, "Invalid option name."
|
||||
|
||||
valid_types = {
|
||||
_h.HighsOptionType.kBool: bool,
|
||||
_h.HighsOptionType.kInt: int,
|
||||
_h.HighsOptionType.kDouble: float,
|
||||
_h.HighsOptionType.kString: str,
|
||||
}
|
||||
|
||||
expected_type = valid_types.get(option_type, None)
|
||||
|
||||
if expected_type is str:
|
||||
if not hoptmanager.check_string_option(option, value):
|
||||
return -1, "Invalid option value."
|
||||
if expected_type is float:
|
||||
if not hoptmanager.check_double_option(option, value):
|
||||
return -1, "Invalid option value."
|
||||
if expected_type is int:
|
||||
if not hoptmanager.check_int_option(option, value):
|
||||
return -1, "Invalid option value."
|
||||
|
||||
if expected_type is None:
|
||||
return 3, "Unknown option type."
|
||||
|
||||
status, current_value = highs_inst.getOptionValue(option)
|
||||
if status != _h.HighsStatus.kOk:
|
||||
return 4, "Failed to validate option value."
|
||||
return 0, "Check option succeeded."
|
||||
Reference in New Issue
Block a user