| """HiGHS Linear Optimization Methods |
| |
| Interface to HiGHS linear optimization software. |
| https://highs.dev/ |
| |
| .. versionadded:: 1.5.0 |
| |
| References |
| ---------- |
| .. [1] Q. Huangfu and J.A.J. Hall. "Parallelizing the dual revised simplex |
| method." Mathematical Programming Computation, 10 (1), 119-142, |
| 2018. DOI: 10.1007/s12532-017-0130-5 |
| |
| """ |
|
|
| import inspect |
| import numpy as np |
| from ._optimize import OptimizeWarning, OptimizeResult |
| from warnings import warn |
| from ._highspy._highs_wrapper import _highs_wrapper |
| from ._highspy._core import( |
| kHighsInf, |
| HighsDebugLevel, |
| ObjSense, |
| HighsModelStatus, |
| ) |
| from ._highspy._core.simplex_constants import ( |
| SimplexStrategy, |
| SimplexEdgeWeightStrategy, |
| ) |
| from scipy.sparse import csc_matrix, vstack, issparse |
|
|
|
|
| def _highs_to_scipy_status_message(highs_status, highs_message): |
| """Converts HiGHS status number/message to SciPy status number/message""" |
|
|
| scipy_statuses_messages = { |
| None: (4, "HiGHS did not provide a status code. "), |
| HighsModelStatus.kNotset: (4, ""), |
| HighsModelStatus.kLoadError: (4, ""), |
| HighsModelStatus.kModelError: (2, ""), |
| HighsModelStatus.kPresolveError: (4, ""), |
| HighsModelStatus.kSolveError: (4, ""), |
| HighsModelStatus.kPostsolveError: (4, ""), |
| HighsModelStatus.kModelEmpty: (4, ""), |
| HighsModelStatus.kObjectiveBound: (4, ""), |
| HighsModelStatus.kObjectiveTarget: (4, ""), |
| HighsModelStatus.kOptimal: (0, "Optimization terminated successfully. "), |
| HighsModelStatus.kTimeLimit: (1, "Time limit reached. "), |
| HighsModelStatus.kIterationLimit: (1, "Iteration limit reached. "), |
| HighsModelStatus.kInfeasible: (2, "The problem is infeasible. "), |
| HighsModelStatus.kUnbounded: (3, "The problem is unbounded. "), |
| HighsModelStatus.kUnboundedOrInfeasible: (4, "The problem is unbounded " |
| "or infeasible. ")} |
| unrecognized = (4, "The HiGHS status code was not recognized. ") |
| scipy_status, scipy_message = ( |
| scipy_statuses_messages.get(highs_status, unrecognized)) |
| hstat = int(highs_status) if highs_status is not None else None |
| scipy_message = (f"{scipy_message}" |
| f"(HiGHS Status {hstat}: {highs_message})") |
| return scipy_status, scipy_message |
|
|
|
|
| def _replace_inf(x): |
| |
| infs = np.isinf(x) |
| with np.errstate(invalid="ignore"): |
| x[infs] = np.sign(x[infs])*kHighsInf |
| return x |
|
|
|
|
| def _convert_to_highs_enum(option, option_str, choices): |
| |
| |
| try: |
| return choices[option.lower()] |
| except AttributeError: |
| return choices[option] |
| except KeyError: |
| sig = inspect.signature(_linprog_highs) |
| default_str = sig.parameters[option_str].default |
| warn(f"Option {option_str} is {option}, but only values in " |
| f"{set(choices.keys())} are allowed. Using default: " |
| f"{default_str}.", |
| OptimizeWarning, stacklevel=3) |
| return choices[default_str] |
|
|
|
|
| def _linprog_highs(lp, solver, time_limit=None, presolve=True, |
| disp=False, maxiter=None, |
| dual_feasibility_tolerance=None, |
| primal_feasibility_tolerance=None, |
| ipm_optimality_tolerance=None, |
| simplex_dual_edge_weight_strategy=None, |
| mip_rel_gap=None, |
| mip_max_nodes=None, |
| **unknown_options): |
| r""" |
| Solve the following linear programming problem using one of the HiGHS |
| solvers: |
| |
| User-facing documentation is in _linprog_doc.py. |
| |
| Parameters |
| ---------- |
| lp : _LPProblem |
| A ``scipy.optimize._linprog_util._LPProblem`` ``namedtuple``. |
| solver : "ipm" or "simplex" or None |
| Which HiGHS solver to use. If ``None``, "simplex" will be used. |
| |
| Options |
| ------- |
| maxiter : int |
| The maximum number of iterations to perform in either phase. For |
| ``solver='ipm'``, this does not include the number of crossover |
| iterations. Default is the largest possible value for an ``int`` |
| on the platform. |
| disp : bool |
| Set to ``True`` if indicators of optimization status are to be printed |
| to the console each iteration; default ``False``. |
| time_limit : float |
| The maximum time in seconds allotted to solve the problem; default is |
| the largest possible value for a ``double`` on the platform. |
| presolve : bool |
| Presolve attempts to identify trivial infeasibilities, |
| identify trivial unboundedness, and simplify the problem before |
| sending it to the main solver. It is generally recommended |
| to keep the default setting ``True``; set to ``False`` if presolve is |
| to be disabled. |
| dual_feasibility_tolerance : double |
| Dual feasibility tolerance. Default is 1e-07. |
| The minimum of this and ``primal_feasibility_tolerance`` |
| is used for the feasibility tolerance when ``solver='ipm'``. |
| primal_feasibility_tolerance : double |
| Primal feasibility tolerance. Default is 1e-07. |
| The minimum of this and ``dual_feasibility_tolerance`` |
| is used for the feasibility tolerance when ``solver='ipm'``. |
| ipm_optimality_tolerance : double |
| Optimality tolerance for ``solver='ipm'``. Default is 1e-08. |
| Minimum possible value is 1e-12 and must be smaller than the largest |
| possible value for a ``double`` on the platform. |
| simplex_dual_edge_weight_strategy : str (default: None) |
| Strategy for simplex dual edge weights. The default, ``None``, |
| automatically selects one of the following. |
| |
| ``'dantzig'`` uses Dantzig's original strategy of choosing the most |
| negative reduced cost. |
| |
| ``'devex'`` uses the strategy described in [15]_. |
| |
| ``steepest`` uses the exact steepest edge strategy as described in |
| [16]_. |
| |
| ``'steepest-devex'`` begins with the exact steepest edge strategy |
| until the computation is too costly or inexact and then switches to |
| the devex method. |
| |
| Currently, using ``None`` always selects ``'steepest-devex'``, but this |
| may change as new options become available. |
| |
| mip_max_nodes : int |
| The maximum number of nodes allotted to solve the problem; default is |
| the largest possible value for a ``HighsInt`` on the platform. |
| Ignored if not using the MIP solver. |
| 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 |
| ------- |
| sol : dict |
| A dictionary consisting of the fields: |
| |
| x : 1D array |
| The values of the decision variables that minimizes the |
| objective function while satisfying the constraints. |
| fun : float |
| The optimal value of the objective function ``c @ x``. |
| slack : 1D array |
| The (nominally positive) values of the slack, |
| ``b_ub - A_ub @ x``. |
| con : 1D array |
| The (nominally zero) residuals of the equality constraints, |
| ``b_eq - A_eq @ x``. |
| success : bool |
| ``True`` when the algorithm succeeds in finding an optimal |
| solution. |
| status : int |
| An integer representing the exit status of the algorithm. |
| |
| ``0`` : Optimization terminated successfully. |
| |
| ``1`` : Iteration or time limit reached. |
| |
| ``2`` : Problem appears to be infeasible. |
| |
| ``3`` : Problem appears to be unbounded. |
| |
| ``4`` : The HiGHS solver ran into a problem. |
| |
| message : str |
| A string descriptor of the exit status of the algorithm. |
| nit : int |
| The total number of iterations performed. |
| For ``solver='simplex'``, this includes iterations in all |
| phases. For ``solver='ipm'``, this does not include |
| crossover iterations. |
| crossover_nit : int |
| The number of primal/dual pushes performed during the |
| crossover routine for ``solver='ipm'``. This is ``0`` |
| for ``solver='simplex'``. |
| ineqlin : OptimizeResult |
| Solution and sensitivity information corresponding to the |
| inequality constraints, `b_ub`. A dictionary consisting of the |
| fields: |
| |
| residual : np.ndnarray |
| The (nominally positive) values of the slack variables, |
| ``b_ub - A_ub @ x``. This quantity is also commonly |
| referred to as "slack". |
| |
| marginals : np.ndarray |
| The sensitivity (partial derivative) of the objective |
| function with respect to the right-hand side of the |
| inequality constraints, `b_ub`. |
| |
| eqlin : OptimizeResult |
| Solution and sensitivity information corresponding to the |
| equality constraints, `b_eq`. A dictionary consisting of the |
| fields: |
| |
| residual : np.ndarray |
| The (nominally zero) residuals of the equality constraints, |
| ``b_eq - A_eq @ x``. |
| |
| marginals : np.ndarray |
| The sensitivity (partial derivative) of the objective |
| function with respect to the right-hand side of the |
| equality constraints, `b_eq`. |
| |
| lower, upper : OptimizeResult |
| Solution and sensitivity information corresponding to the |
| lower and upper bounds on decision variables, `bounds`. |
| |
| residual : np.ndarray |
| The (nominally positive) values of the quantity |
| ``x - lb`` (lower) or ``ub - x`` (upper). |
| |
| marginals : np.ndarray |
| The sensitivity (partial derivative) of the objective |
| function with respect to the lower and upper |
| `bounds`. |
| |
| mip_node_count : int |
| The number of subproblems or "nodes" solved by the MILP |
| solver. Only present when `integrality` is not `None`. |
| |
| mip_dual_bound : float |
| The MILP solver's final estimate of the lower bound on the |
| optimal solution. Only present when `integrality` is not |
| `None`. |
| |
| mip_gap : float |
| The difference between the final objective function value |
| and the final dual bound, scaled by the final objective |
| function value. Only present when `integrality` is not |
| `None`. |
| |
| Notes |
| ----- |
| The result fields `ineqlin`, `eqlin`, `lower`, and `upper` all contain |
| `marginals`, or partial derivatives of the objective function with respect |
| to the right-hand side of each constraint. These partial derivatives are |
| also referred to as "Lagrange multipliers", "dual values", and |
| "shadow prices". The sign convention of `marginals` is opposite that |
| of Lagrange multipliers produced by many nonlinear solvers. |
| |
| References |
| ---------- |
| .. [15] Harris, Paula MJ. "Pivot selection methods of the Devex LP code." |
| Mathematical programming 5.1 (1973): 1-28. |
| .. [16] Goldfarb, Donald, and John Ker Reid. "A practicable steepest-edge |
| simplex algorithm." Mathematical Programming 12.1 (1977): 361-371. |
| """ |
| if unknown_options: |
| message = (f"Unrecognized options detected: {unknown_options}. " |
| "These will be passed to HiGHS verbatim.") |
| warn(message, OptimizeWarning, stacklevel=3) |
|
|
| |
| simplex_dual_edge_weight_strategy_enum = _convert_to_highs_enum( |
| simplex_dual_edge_weight_strategy, |
| 'simplex_dual_edge_weight_strategy', |
| choices={'dantzig': \ |
| SimplexEdgeWeightStrategy.kSimplexEdgeWeightStrategyDantzig, |
| 'devex': \ |
| SimplexEdgeWeightStrategy.kSimplexEdgeWeightStrategyDevex, |
| 'steepest-devex': \ |
| SimplexEdgeWeightStrategy.kSimplexEdgeWeightStrategyChoose, |
| 'steepest': \ |
| SimplexEdgeWeightStrategy.kSimplexEdgeWeightStrategySteepestEdge, |
| None: None}) |
|
|
| c, A_ub, b_ub, A_eq, b_eq, bounds, x0, integrality = lp |
|
|
| lb, ub = bounds.T.copy() |
| |
| with np.errstate(invalid="ignore"): |
| lhs_ub = -np.ones_like(b_ub)*np.inf |
| rhs_ub = b_ub |
| lhs_eq = b_eq |
| rhs_eq = b_eq |
| lhs = np.concatenate((lhs_ub, lhs_eq)) |
| rhs = np.concatenate((rhs_ub, rhs_eq)) |
|
|
| if issparse(A_ub) or issparse(A_eq): |
| A = vstack((A_ub, A_eq)) |
| else: |
| A = np.vstack((A_ub, A_eq)) |
| A = csc_matrix(A) |
|
|
| options = { |
| 'presolve': presolve, |
| 'sense': ObjSense.kMinimize, |
| 'solver': solver, |
| 'time_limit': time_limit, |
| 'highs_debug_level': HighsDebugLevel.kHighsDebugLevelNone, |
| 'dual_feasibility_tolerance': dual_feasibility_tolerance, |
| 'ipm_optimality_tolerance': ipm_optimality_tolerance, |
| 'log_to_console': disp, |
| 'mip_max_nodes': mip_max_nodes, |
| 'output_flag': disp, |
| 'primal_feasibility_tolerance': primal_feasibility_tolerance, |
| 'simplex_dual_edge_weight_strategy': |
| simplex_dual_edge_weight_strategy_enum, |
| 'simplex_strategy': SimplexStrategy.kSimplexStrategyDual, |
| 'ipm_iteration_limit': maxiter, |
| 'simplex_iteration_limit': maxiter, |
| 'mip_rel_gap': mip_rel_gap, |
| } |
| options.update(unknown_options) |
|
|
| |
| rhs = _replace_inf(rhs) |
| lhs = _replace_inf(lhs) |
| lb = _replace_inf(lb) |
| ub = _replace_inf(ub) |
|
|
| if integrality is None or np.sum(integrality) == 0: |
| integrality = np.empty(0) |
| else: |
| integrality = np.array(integrality) |
|
|
| res = _highs_wrapper(c, A.indptr, A.indices, A.data, lhs, rhs, |
| lb, ub, integrality.astype(np.uint8), options) |
|
|
| |
| |
| |
| if 'slack' in res: |
| slack = res['slack'] |
| con = np.array(slack[len(b_ub):]) |
| slack = np.array(slack[:len(b_ub)]) |
| else: |
| slack, con = None, None |
|
|
| |
| if 'lambda' in res: |
| lamda = res['lambda'] |
| marg_ineqlin = np.array(lamda[:len(b_ub)]) |
| marg_eqlin = np.array(lamda[len(b_ub):]) |
| marg_upper = np.array(res['marg_bnds'][1, :]) |
| marg_lower = np.array(res['marg_bnds'][0, :]) |
| else: |
| marg_ineqlin, marg_eqlin = None, None |
| marg_upper, marg_lower = None, None |
|
|
| |
|
|
| |
| highs_status = res.get('status', None) |
| highs_message = res.get('message', None) |
| status, message = _highs_to_scipy_status_message(highs_status, |
| highs_message) |
|
|
| x = res['x'] |
| sol = {'x': x, |
| 'slack': slack, |
| 'con': con, |
| 'ineqlin': OptimizeResult({ |
| 'residual': slack, |
| 'marginals': marg_ineqlin, |
| }), |
| 'eqlin': OptimizeResult({ |
| 'residual': con, |
| 'marginals': marg_eqlin, |
| }), |
| 'lower': OptimizeResult({ |
| 'residual': None if x is None else x - lb, |
| 'marginals': marg_lower, |
| }), |
| 'upper': OptimizeResult({ |
| 'residual': None if x is None else ub - x, |
| 'marginals': marg_upper |
| }), |
| 'fun': res.get('fun'), |
| 'status': status, |
| 'success': res['status'] == HighsModelStatus.kOptimal, |
| 'message': message, |
| 'nit': res.get('simplex_nit', 0) or res.get('ipm_nit', 0), |
| 'crossover_nit': res.get('crossover_nit'), |
| } |
|
|
| if np.any(x) and integrality is not None: |
| sol.update({ |
| 'mip_node_count': res.get('mip_node_count', 0), |
| 'mip_dual_bound': res.get('mip_dual_bound', 0.0), |
| 'mip_gap': res.get('mip_gap', 0.0), |
| }) |
|
|
| return sol |
|
|