ommx_highs_adapter

目次

ommx_highs_adapter#

Submodules#

Exceptions#

OMMXHighsAdapterError

Common base class for all non-exit exceptions.

Classes#

HighsDiagnosticsAnalyzer

Post-processor for HiGHS diagnostics.

HighsProgressSnapshot

HiGHS MIP solve progress observed from one logging callback.

HighsTerminationReport

HiGHS-side termination summary recorded after model.run().

OMMXHighsAdapter

OMMX Adapter for HiGHS solver.

Package Contents#

exception OMMXHighsAdapterError#

Common base class for all non-exit exceptions.

class HighsDiagnosticsAnalyzer(diagnostics: Iterable[Any])#

Post-processor for HiGHS diagnostics.

Inspect the backend's termination reason and MIP progress separately from the candidate evaluation in Solution.

Collect a final report

Install the SDK and HiGHS Adapter using the installation guide. This complete example chooses at most one of two binary items and maximizes its value. No dataset, other adapter, or Experiment is required.

>>> from ommx import Instance
>>> from ommx.adapter import DiagnosticCollector
>>> from ommx_highs_adapter import OMMXHighsAdapter, HighsDiagnosticsAnalyzer
>>> instance = Instance.maximize()
>>> x = instance.new_binary("x")
>>> y = instance.new_binary("y")
>>> instance.objective = 3 * x + 2 * y
>>> _ = instance.add_constraint(x + y <= 1)
>>> collector = DiagnosticCollector()
>>> solution = OMMXHighsAdapter.solve(instance, diagnostics=collector)
>>> analysis = HighsDiagnosticsAnalyzer(collector.diagnostics)
>>> solution.objective, solution.feasible
(3.0, True)
>>> solution.decision_variables_df()[["name", "value"]].to_dict("records")
[{'name': 'x', 'value': 1.0}, {'name': 'y', 'value': 0.0}]
>>> result = analysis.termination_result
>>> assert result is not None
>>> result["status"], result["objective_value"], result["mip_dual_bound"], result["mip_gap"]
('Optimal', 3.0, 3.0, 0.0)

Optimal is HiGHS' report about the solved formulation. A feasible candidate by itself does not prove optimality. The primal bound is the objective bound from the best feasible candidate; the dual bound limits how good the optimum can be. For maximization they bound the optimum from below and above respectively; for minimization these directions reverse. gap is the backend's relative gap, whose definition can differ between solvers. Bounds or gaps need not be finite before a candidate is found.

Bounds and gaps describe the solver-facing objective. Sign, scale, and penalty transformations can separate it from OMMX's reported objective; see result interpretation before interpreting them as guarantees about the original problem.

Read progress and terminal values

The analyzer accepts either typed diagnostics collected by ommx.adapter.DiagnosticCollector or dictionaries loaded from ommx.experiment.Solve.diagnostics.

progress_history_records returns dictionaries without requiring pandas. progress_history_df returns a DataFrame indexed by solving_time_sec. Series such as primal_bound, dual_bound, and gap share that time index.

>>> analysis.progress_history_df.index.name
'solving_time_sec'
>>> assert len(analysis.dual_bound) == len(analysis.progress_history_records)
>>> assert analysis.termination_mip_dual_bound == 3.0
>>> assert analysis.termination_mip_gap == 0.0

HighsProgressSnapshot records MIP logging callbacks, including node counts, bounds, and gap. Small problems may finish with an empty history. Unlike the SCIP Analyzer, this Analyzer does not append a termination row to that history. Use termination_result or the termination_* properties for terminal scalar values, even when no callback was recorded. HighsTerminationReport includes status, objective, MIP bounds/gap, iteration and node counts, violation summaries, runtime, and HiGHS versions. Its field names are backend-specific (for example, mip_gap).

To plot a collected history, select its bound columns and handle non-finite values before drawing. This code works even if the history has no rows; it requires matplotlib in addition to pandas.

>>> import numpy as np
>>> from matplotlib import pyplot as plt
>>> def plot_bounds(history, ax):
...     bounds = history[["primal_bound", "dual_bound"]].astype(float)
...     for column in bounds:
...         series = bounds[column]
...         ax.plot(series.index, series.where(np.isfinite(series)), label=column)
...     ax.set(xlabel="Solver elapsed time (s)", ylabel="Solver objective bound")
...     ax.legend()
>>> fig, ax = plt.subplots()
>>> plot_bounds(analysis.progress_history_df, ax)
>>> plt.close(fig)

With no callbacks there are no observations to plot. Converting the columns to floats also works in this case, when pandas cannot infer a numeric type from any rows:

>>> fig, ax = plt.subplots()
>>> plot_bounds(HighsDiagnosticsAnalyzer([]).progress_history_df, ax)
>>> [len(line.get_ydata()) for line in ax.lines]
[0, 0]
>>> plt.close(fig)

Call plt.show() before closing a figure when working interactively. The SCIPDiagnosticsAnalyzer Reference illustrates how the bounds and gap change during a recorded solve. Use the appropriate Analyzer and backend definitions when drawing your own reports; the curves are observations, not a complete trace of every internal update.

Inspect a report when decoding fails

HiGHS records termination after model.run() and before decoding a Solution, so a collector can retain the report when decoding raises an adapter exception. This model has an unbounded objective:

>>> from ommx.adapter import UnboundedDetected
>>> unbounded = Instance.maximize()
>>> z = unbounded.new_continuous("z", lower=0)
>>> unbounded.objective = z
>>> collector = DiagnosticCollector()
>>> try:
...     OMMXHighsAdapter.solve(unbounded, diagnostics=collector)
... except UnboundedDetected:
...     failure = HighsDiagnosticsAnalyzer(collector.diagnostics).termination_result
>>> assert failure is not None and failure["status"] == "Unbounded"

A failure before the backend finishes need not produce a final report; termination_result is None when none was collected.

For optional persistence, log_solve() and log_sample() reserve the diagnostics keyword. Set store_diagnostics=True instead; collection is disabled by default. Pass the resulting Solve/Sampling diagnostics dictionaries directly to this Analyzer. Failure records and diagnostic persistence are best-effort, as described by those logging methods.

property dual_bound: Any#

Return MIP dual bounds indexed by solving time.

property event: Any#

Return progress event names indexed by solving time.

property gap: Any#

Return MIP gaps indexed by solving time.

property ipm_iteration_count: Any#

Return interior-point iteration counts indexed by solving time.

property mip_dual_bound: Any#

Alias for dual_bound.

property mip_gap: Any#

Alias for gap.

property mip_node_count: Any#

Return MIP node counts indexed by solving time.

property mip_primal_bound: Any#

Alias for primal_bound.

property node_count: Any#

Alias for mip_node_count.

property objective_value: Any#

Return objective values indexed by solving time.

property pdlp_iteration_count: Any#

Return PDLP iteration counts indexed by solving time.

property primal_bound: Any#

Return MIP primal bounds indexed by solving time.

property progress_history_df: Any#

Return progress snapshots as a pandas DataFrame indexed by time.

property progress_history_records: list[dict[str, object]]#

Return one dictionary per HiGHS MIP logging callback.

property progress_snapshots: tuple[HighsProgressSnapshot, ...]#

Typed progress snapshots in the order they were recorded.

property simplex_iteration_count: Any#

Return simplex iteration counts indexed by solving time.

property termination_mip_dual_bound: float | None#

Return the terminal HiGHS MIP dual bound, if present.

property termination_mip_gap: float | None#

Return the terminal HiGHS MIP gap, if present.

property termination_mip_node_count: int | None#

Return the terminal HiGHS MIP node count, if present.

property termination_objective_value: float | None#

Return the terminal objective value, if present.

property termination_result: dict[str, object] | None#

Return the terminal HiGHS report as one dictionary, if present.

property termination_status: str | None#

Return the terminal HiGHS model status, if present.

class HighsProgressSnapshot#

HiGHS MIP solve progress observed from one logging callback.

classmethod from_callback_event(event: Any) → HighsProgressSnapshot#
dual_bound: float#

MIP dual bound reported at the callback.

event: str#

HiGHS callback message, currently "MIP logging".

gap: float#

MIP relative gap reported at the callback.

ipm_iteration_count: int#

Interior-point iterations at the callback.

mip_node_count: int#

MIP branch-and-bound nodes processed at the callback.

objective_value: float#

Objective value reported at the callback.

pdlp_iteration_count: int#

PDLP iterations at the callback.

primal_bound: float#

MIP primal bound reported at the callback.

simplex_iteration_count: int#

Simplex iterations at the callback.

solving_time_sec: float#

HiGHS runtime when the callback ran.

class HighsTerminationReport#

HiGHS-side termination summary recorded after model.run().

The HiGHS adapter records this report before decoding the optimized HiGHS model back into an OMMX solution. It is therefore available even when decoding raises an adapter exception such as infeasible or unbounded detection.

classmethod from_model(model: highspy.Highs) → HighsTerminationReport#
crossover_iteration_count: int#

Number of crossover iterations after interior-point optimization.

dual_solution_status: int#

HiGHS dual solution status code.

highs_githash: str#

HiGHS git hash reported by highspy.

highs_version: str#

HiGHS version used through highspy.

ipm_iteration_count: int#

Number of interior-point iterations.

max_dual_infeasibility: float#

Maximum dual infeasibility in the final solution.

max_integrality_violation: float#

Maximum integrality violation in the final solution.

max_primal_infeasibility: float#

Maximum primal infeasibility in the final solution.

mip_dual_bound: float#

MIP dual bound reported by HighsInfo.mip_dual_bound.

mip_gap: float#

MIP relative gap reported by HighsInfo.mip_gap.

mip_node_count: int#

Number of MIP branch-and-bound nodes processed by HiGHS.

objective_value: float | None#

Objective value reported by HiGHS, or None when no objective is valid.

pdlp_iteration_count: int#

Number of PDLP iterations.

primal_dual_integral: float#

HiGHS primal-dual integral.

primal_solution_status: int#

HiGHS primal solution status code.

run_time_sec: float#

HiGHS runtime in seconds.

simplex_iteration_count: int#

Number of simplex iterations.

status: str#

HiGHS model status, such as "Optimal" or "Infeasible".

class OMMXHighsAdapter(ommx_instance: Instance, *, verbose: bool = False)#

OMMX Adapter for HiGHS solver.

This adapter encodes linear OMMX problems for HiGHS and returns an evaluated ommx.Solution. Install ommx-highs-adapter alongside a compatible OMMX SDK to run these examples. INPUT_CLASS describes exact inputs; solve() prepares an isolated copy with the recommended policy before solving. Constructors and solve_without_preparation() instead require an already applicable input.

Solve a linear mixed-integer problem

>>> from ommx_highs_adapter import OMMXHighsAdapter
>>> from ommx import Instance, Solution
>>> instance = Instance.maximize()
>>> x = instance.new_binary("x")
>>> y = instance.new_integer("y", lower=0, upper=10)
>>> instance.objective = 2 * x + 3 * y
>>> _ = instance.add_constraint(x + y <= 5)
>>> solution = OMMXHighsAdapter.solve(instance)
>>> assert solution.feasible
>>> assert solution.optimality == Solution.OPTIMAL
>>> assert abs(solution.objective - 15.0) < 1e-6
>>> assert solution.state.entries == {x.id: 0.0, y.id: 5.0}

The objective property describes the returned candidate; it is not by itself a guarantee of optimality. ommx.Solution explains how to extract and interpret evaluated values.

Prepare Indicator and SOS1 constraints without changing the source

HiGHS input through this Adapter has only regular linear constraints. The following model has an Indicator requiring option_a + option_b <= 1 when enabled is one, and an SOS1 allowing at most one of option_b and option_c to be nonzero. The easy solve path lowers both on its copy. This example is self-contained; it uses the same model that ommx_pyscipopt_adapter.OMMXPySCIPOptAdapter can encode natively.

>>> from ommx import Instance, Sos1Constraint, ConstraintKind
>>> from ommx_highs_adapter import OMMXHighsAdapter
>>> source = Instance.maximize()
>>> enabled = source.new_binary("enabled")
>>> option_a = source.new_binary("option_a")
>>> option_b = source.new_binary("option_b")
>>> option_c = source.new_binary("option_c")
>>> source.objective = 10 * enabled + 6 * option_a + 5 * option_b + 4 * option_c
>>> _ = source.add_indicator_constraint((option_a + option_b <= 1).with_indicator(enabled))
>>> _ = source.add_sos1_constraint(
...     Sos1Constraint(variables=[option_b, option_c], name="exclusive_options")
... )
>>> assert not OMMXHighsAdapter.INPUT_CLASS.contains(source)
>>> solution = OMMXHighsAdapter.solve(source)
>>> assert solution.feasible
>>> assert abs(solution.objective - 20.0) < 1e-6
>>> assert source.active_special_constraint_kinds == {
...     ConstraintKind.Indicator, ConstraintKind.Sos1,
... }
>>> assert not source.removed_constraints

All variables here are Binary with finite bounds, so the recommended lowering can construct the required linear rows. An arbitrary model need not be preparable. INPUT_CLASS describes direct acceptance; recommended_preparation_policy() returns an editable recommendation, not a guarantee of success. For an application-selected policy, call ommx.Instance.prepare() explicitly and then solve_without_preparation(); the PySCIPOpt Adapter Reference provides a complete example of that sequence.

Inspect conversion, search and decoding times

>>> from ommx import Instance
>>> from ommx.tracing import capture_trace
>>> problem = Instance.minimize()
>>> x = problem.new_binary("x")
>>> problem.objective = -x
>>> with capture_trace("highs_solve") as trace:
...     solution = OMMXHighsAdapter.solve(problem, verbose=False)
>>> assert solution.feasible and solution.objective == -1
>>> assert {"convert", "call", "decode"} <= {span.name for span in trace.spans}

Display the trace to read the stages within the solve. The parent span includes the children's time. Each duration describes this run.

Translation Specifications#

Decision Variables#

The adapter handles the following translations for decision variables:

ID Management:

  • OMMX: Variables managed by IDs (non-sequential integers)

  • HiGHS: Variables managed by array indices (0-based sequential)

  • Mapping maintained internally for bidirectional conversion

Variable Types:

OMMX Type

HiGHS Type

Bounds

Kind.Binary

HighsVarType.kInteger

[0, 1]

Kind.Integer

HighsVarType.kInteger

[var.bound.lower, var.bound.upper]

Kind.Continuous

HighsVarType.kContinuous

[var.bound.lower, var.bound.upper]

Kind.SemiInteger

Not supported (support planned)

-

Kind.SemiContinuous

Not supported (support planned)

-

Note: Semi-integer and semi-continuous variables are planned for future support but are currently unsupported. Exact-input APIs reject these kinds with AdapterNotApplicableError; the easy API reports that recommended Preparation could not reach INPUT_CLASS.

Constraints#

Supported Function Types:

  • Constant functions (ommx.Function.constant)

  • Linear functions (ommx.Function.linear)

Constraint Types:

OMMX Constraint

Mathematical Form

HiGHS Constraint

Equality.EqualToZero

f(x) = 0

const_expr == 0

Equality.LessThanOrEqualToZero

f(x) ≤ 0

const_expr <= 0

Constant Constraint Handling:

Constant equalities and inequalities are passed to HiGHS as rows with no variable coefficients. HiGHS determines feasibility using its configured tolerances. A definite infeasible status raises InfeasibleDetected; an unresolved infeasible-or-unbounded status raises InfeasibleOrUnboundedDetected during decoding. When a constant-row bound reaches HiGHS's infinity threshold, the adapter temporarily raises infinite_bound while adding that row and restores it immediately afterward. Other rows and solver settings are unchanged.

Constraint ID Management:

  • OMMX constraint IDs converted to HiGHS constraint names via str(constraint.id)

Objective Function#

Optimization Direction:

OMMX Direction

HiGHS Method

Sense.Minimize

model.minimize(...)

Sense.Maximize

model.maximize(...)

Function Types:

  • Constant objectives: Processing skipped

  • Linear objectives: Converted to HiGHS linear expressions

Solution Decoding#

Variable Values: Extracted from HiGHS solution.col_value using maintained ID mapping

Optimality Status:

  • A HiGHS kOptimal status becomes OPTIMALITY_UNSPECIFIED when it does not transport to the output objective

Dual Variables: Extracted from solution.row_dual when the active objective is also the output objective. They are omitted when output-objective projection would require a dual mapping that OMMX does not define.

Error Handling#

Unsupported Features:

  • Quadratic functions (this Adapter encodes linear problems only)

  • Semi-integer variables (Kind.SemiInteger) - support planned

  • Semi-continuous variables (Kind.SemiContinuous) - support planned

  • Constraint types other than Equality.EqualToZero/ Equality.LessThanOrEqualToZero

Solver Status Mapping:

HiGHS Status

Exception

kInfeasible

InfeasibleDetected

kUnbounded

UnboundedDetected

kUnboundedOrInfeasible

InfeasibleOrUnboundedDetected

kNotset

OMMXHighsAdapterError

Limitations#

  1. Linear problems only (no quadratic constraints or objectives)

  2. Constraint forms limited to equality (= 0) and inequality (≤ 0)

  3. Variable types limited to Binary, Integer, and Continuous

    • Kind.SemiInteger support is planned but not yet implemented

    • Kind.SemiContinuous support is planned but not yet implemented

classmethod check_applicability(ommx_instance: Instance) → InstanceClassMembershipReport#

Check INPUT_CLASS membership without mutation.

decode(data: highspy.Highs) → Solution#

Convert an optimized HiGHS model back to an OMMX Solution.

This method translates HiGHS solver results into OMMX format, including variable values, optimality status, and transportable dual variable information. Backend optimality is mapped through the instance's output-objective semantics and remains unspecified when it does not transport.

Parameters#
datahighspy.Highs

The HiGHS model that has been optimized. Must be the same model returned by solver_input property.

Returns#
Solution

Complete OMMX solution containing: - Variable values mapped back to original OMMX IDs - Constraint evaluations and feasibility status - Optimality information from HiGHS when transportable to the output objective - Dual variables for linear constraints without output-objective projection

Raises#
OMMXHighsAdapterError

If the model has not been optimized yet

InfeasibleDetected

If HiGHS determined the problem is infeasible

UnboundedDetected

If HiGHS determined the problem is unbounded

InfeasibleOrUnboundedDetected

If HiGHS reports infeasibility or unboundedness without distinguishing which

Notes#

This method should only be used after solving the model with HiGHS. Any modifications to the HiGHS model structure after creation may make the decoding process incompatible.

When the active objective is also the output objective, dual variables are extracted from HiGHS's row_dual and mapped to OMMX constraints based on their order. If output_objective is present, every dual is omitted because OMMX does not define how the active formulation's dual certificate maps to the projected output semantics.

Examples#
>>> from ommx_highs_adapter import OMMXHighsAdapter
>>> from ommx import Instance, DecisionVariable
>>>
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x],
...     objective=x,
...     constraints={},
...     sense=Sense.Maximize,
... )
>>>
>>> adapter = OMMXHighsAdapter(instance)
>>> model = adapter.solver_input
>>> model.run()
<...>
>>> solution = adapter.decode(model)
>>> solution.objective
1.0
decode_to_state(data: highspy.Highs) → State#

Extract variable values from an optimized HiGHS model as an OMMX State.

Parameters#
datahighspy.Highs

The optimized HiGHS model

Returns#
State

OMMX state containing variable values mapped to original OMMX IDs

Raises#
OMMXHighsAdapterError

If the model has not been optimized

InfeasibleDetected

If the model is infeasible

UnboundedDetected

If the model is unbounded

InfeasibleOrUnboundedDetected

If HiGHS reports infeasibility or unboundedness without distinguishing which

Examples#
>>> from ommx_highs_adapter import OMMXHighsAdapter
>>> from ommx import Instance, DecisionVariable
>>>
>>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
>>> instance = Instance.from_components(
...     decision_variables=[x1],
...     objective=x1,
...     constraints={},
...     sense=Sense.Minimize,
... )
>>> adapter = OMMXHighsAdapter(instance)
>>> model = adapter.solver_input
>>> model.run()
<...>
>>> state = adapter.decode_to_state(model)
>>> state.entries
{1: 0.0}
classmethod recommended_preparation_policy() → PreparationPolicy#

Recommend lowering special constraints before using HiGHS.

HiGHS accepts both optimization senses, Binary, Integer, and Continuous variables, and linear regular constraints directly. Its recommendation therefore enables only lowering for the special-constraint families currently understood by OMMX. The easy API applies a fresh policy to an isolated copy; callers may instead edit and apply one before invoking solve_without_preparation().

classmethod require_applicable(ommx_instance: Instance) → InstanceClassMembershipReport#

Return the membership report or raise AdapterNotApplicableError.

classmethod solve(ommx_instance: Instance, *, verbose: bool = False, diagnostics: DiagnosticsSink | None = None) → Solution#

Solve an OMMX optimization problem using HiGHS solver.

This method provides a convenient interface for solving optimization problems without needing to manually instantiate the adapter. It handles the complete workflow: translation to HiGHS format, solving, and result conversion.

Parameters#
ommx_instanceInstance

The OMMX optimization problem to solve. The input is not modified; an isolated copy is prepared with the recommended HiGHS policy.

verbosebool, default=False

If True, enable HiGHS's console logging for debugging

Returns#
Solution

The solution containing: - Variable values in solution.state.entries - Objective value in solution.objective - Constraint evaluations in solution.constraints - Optimality status in solution.optimality - Dual variables when the active and output objectives coincide

Raises#
InfeasibleDetected

When the optimization problem has no feasible solution

UnboundedDetected

When the optimization problem is unbounded

InfeasibleOrUnboundedDetected

When HiGHS reports infeasibility or unboundedness without distinguishing which

PreparationTargetNotReachedError

When the recommended preparation cannot reach the Adapter input class

OMMXHighsAdapterError

When conversion or HiGHS encounters an adapter-specific error

Examples#

Knapsack Problem

>>> from ommx import Instance, DecisionVariable, Solution
>>> from ommx_highs_adapter import OMMXHighsAdapter
>>>
>>> p = [10, 13, 18, 32, 7, 15]  # profits
>>> w = [11, 15, 20, 35, 10, 33]  # weights
>>> x = [DecisionVariable.binary(i) for i in range(6)]
>>> instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(p[i] * x[i] for i in range(6)),
...     constraints={0: sum(w[i] * x[i] for i in range(6)) <= 47},
...     sense=Sense.Maximize,
... )
>>>
>>> solution = OMMXHighsAdapter.solve(instance)
>>> sorted([(id, value) for id, value in solution.state.entries.items()])
[(0, 1.0), (1, 0.0), (2, 0.0), (3, 1.0), (4, 0.0), (5, 0.0)]
>>> solution.feasible
True
>>> assert solution.optimality == Solution.OPTIMAL
>>> solution.objective
42.0

Infeasible Problem

>>> x = DecisionVariable.integer(0, upper=3, lower=0)
>>> instance = Instance.from_components(
...     decision_variables=[x],
...     objective=x,
...     constraints={0: x >= 4},  # Impossible: x ≤ 3 and x ≥ 4
...     sense=Sense.Maximize,
... )
>>> OMMXHighsAdapter.solve(instance)
Traceback (most recent call last):
    ...
ommx.InfeasibleDetected: Model was infeasible
classmethod solve_without_preparation(ommx_instance: Instance, *, verbose: bool = False, diagnostics: DiagnosticsSink | None = None) → Solution#

Solve an exact HiGHS Adapter input without preparing it.

INPUT_CLASS: ClassVar[InstanceClass]#

Required condition for an exact Adapter input.

property solver_input: highspy.Highs#

The HiGHS model generated from the OMMX instance.

Returns#
highspy.Highs

The HiGHS model ready for optimization. This model contains: - Decision variables translated from OMMX IDs to HiGHS indices - Constraints converted to HiGHS linear expressions - Objective function set according to optimization direction