Adapter-specific Diagnostics#

Adapter diagnostics preserve solver-side information that does not fit in the portable Solution. Use Solution for the decoded OMMX result. Use diagnostics when you need to inspect what the backend solver observed, reported, or proved.

Record Diagnostics with the PySCIPOpt Adapter#

The PySCIPOpt Adapter records SCIP progress and termination information when you pass a DiagnosticCollector to solve(). The usual way to read that data is through SCIPDiagnosticsAnalyzer.

from ommx import adapter, dataset
from ommx_pyscipopt_adapter import (
    OMMXPySCIPOptAdapter as Adapter,
    SCIPDiagnosticsAnalyzer,
)

instance = dataset.miplib2017("air05")

diag = adapter.DiagnosticCollector()
solution = Adapter.solve(instance, diagnostics=diag)

analyze = SCIPDiagnosticsAnalyzer(diag.diagnostics)

analyze.progress_history_df[["primal_bound", "dual_bound"]].loc[5:].plot()
SCIP primal and dual bound history over solving time

SCIP primal and dual bound history read through SCIPDiagnosticsAnalyzer.#

progress_history_df is a pandas DataFrame indexed by solving_time_sec. Series properties such as dual_bound, gap, and incumbent_objective use the same time index, so they are ready for time-based plots. The history includes a final TERMINATION row when diagnostics include a SCIP termination report. termination_result is a dictionary containing that final SCIP report.

dual_bound = analyze.dual_bound
gap = analyze.gap
incumbents = analyze.incumbent_objective
termination = analyze.termination_result

The DataFrame and Series helpers require pandas. When pandas is not available, use progress_history_records for the progress history, including the final TERMINATION row when present, and termination_result for the final report.

What PySCIPOpt Records#

The PySCIPOpt Adapter records two kinds of SCIP diagnostics.

SCIPProgressSnapshot is a progress sample recorded from SCIP event callbacks and from the final termination report. The adapter currently listens for BESTSOLFOUND and DUALBOUNDIMPROVED, then adds a final TERMINATION snapshot. A progress snapshot includes fields such as solving_time_sec, node_count, primal_bound, dual_bound, gap, and incumbent_objective.

SCIPTerminationReport is the final SCIP report recorded after model.optimize() finishes and before the PySCIPOpt model is decoded back into an OMMX Solution. It includes fields such as status, primal_bound, dual_bound, gap, objective_value, node counts, LP and cut counters, primal-dual integral, timings, and SCIP/PySCIPOpt version metadata.

Callback progress snapshots are callback-time observations. SCIP may call a BESTSOLFOUND callback before every aggregate statistic has been updated, so use the TERMINATION row or the termination report for terminal values.

For the complete member lists, see the API Reference for SCIPProgressSnapshot, SCIPTerminationReport, and SCIPDiagnosticsAnalyzer.

Record Diagnostics with the HiGHS Adapter#

The HiGHS Adapter records MIP progress and termination information when you pass a DiagnosticCollector to solve(). Read that data through HighsDiagnosticsAnalyzer.

from ommx import adapter
from ommx_highs_adapter import OMMXHighsAdapter, HighsDiagnosticsAnalyzer

diag = adapter.DiagnosticCollector()
solution = OMMXHighsAdapter.solve(instance, diagnostics=diag)

analysis = HighsDiagnosticsAnalyzer(diag.diagnostics)

analysis.progress_history_df[["primal_bound", "dual_bound"]].plot()
print(analysis.dual_bound)
print(analysis.termination_result)

progress_history_df is a pandas DataFrame indexed by solving_time_sec. Series properties such as dual_bound, gap, and primal_bound use the same time index, so they are ready for time-based plots.

HighsProgressSnapshot is recorded from HiGHS MIP logging callbacks. A progress snapshot includes fields such as solving_time_sec, mip_node_count, primal_bound, dual_bound, and gap.

HighsTerminationReport is recorded after model.run() finishes and before the HiGHS model is decoded back into an OMMX Solution. It includes fields such as status, objective_value, mip_dual_bound, mip_gap, mip_node_count, iteration counts, feasibility violation summaries, runtime, and HiGHS version metadata. Use termination_result or the termination_* properties when you need terminal scalar values.

Failure Handling#

Direct collection is useful when OMMX Solution decoding fails. The PySCIPOpt and HiGHS Adapters record the termination report before decoding, so the collector can still contain the final solver status and bounds when the solve raises an adapter exception such as InfeasibleDetected or UnboundedDetected.

from ommx.adapter import DiagnosticCollector, UnboundedDetected
from ommx_pyscipopt_adapter import OMMXPySCIPOptAdapter, SCIPDiagnosticsAnalyzer

collector = DiagnosticCollector()

try:
    OMMXPySCIPOptAdapter.solve(instance, diagnostics=collector)
except UnboundedDetected:
    analysis = SCIPDiagnosticsAnalyzer(collector.diagnostics)
    print(analysis.termination_result)

Experiment Integration#

When using log_solve() or log_sample(), do not pass the diagnostics keyword yourself. The logging method owns that reserved keyword, and diagnostics collection is disabled by default. Set store_diagnostics=True to pass a diagnostics sink to the adapter and store recorded diagnostics with the corresponding Solve or Sampling entry in the Experiment Artifact.

from ommx.experiment import Experiment
from ommx_pyscipopt_adapter import OMMXPySCIPOptAdapter, SCIPDiagnosticsAnalyzer

with Experiment() as experiment:
    with experiment.run() as run:
        solution = run.log_solve(
            OMMXPySCIPOptAdapter,
            instance,
            store_diagnostics=True,
        )

solve = experiment.runs[0].solves[0]
analysis = SCIPDiagnosticsAnalyzer(solve.diagnostics)

print(analysis.dual_bound)
print(analysis.termination_result)

Diagnostics loaded from an Experiment through diagnostics are dictionaries, not the original dataclass instances. This keeps stored Artifacts independent of the Python class definitions used when the solve was recorded. Pass that list directly to SCIPDiagnosticsAnalyzer when you want the same records, DataFrame, or Series views as direct collection.

If solve() raises before returning an OMMX Solution, Run.log_solve still records a failed Solve entry when possible. That entry has status == "failed" or "interrupted", no output Solution, and any diagnostics collected before the failure when store_diagnostics=True. Run.log_sample records sampler failures separately as Sampling entries with no output SampleSet.

See the API Reference for the adapter diagnostics contract: DiagnosticsSink, DiagnosticCollector, and solve() or sample().