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 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().