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. termination_result is a dictionary containing the 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 progress samples 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. The adapter currently listens for BESTSOLFOUND and DUALBOUNDIMPROVED. 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.

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

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

Failure Handling#

Direct collection is useful when OMMX Solution decoding fails. The PySCIPOpt Adapter records the termination report before decoding, so the collector can still contain the final SCIP 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(), do not pass the diagnostics keyword yourself. Run.log_solve 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 Solve 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.

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