add read me

This commit is contained in:
2026-01-09 10:28:44 +11:00
commit edaf914b73
13417 changed files with 2952119 additions and 0 deletions

View File

@@ -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."