Adapter 固有 diagnostics#
どの Solver Adapter でも、求解結果として返す OMMX 側の型は
Solution です。これは Adapter 共通の出力であり、どの backend
solver で解いた場合でも、decode 済みの OMMX state、feasibility、optimality、
objective value を同じ形で扱うためのものです。
diagnostics はこれとは意図的に別の枠組みです。diagnostics は、共通の
Solution contract には入らない solver 側の詳しい情報を保持するための
adapter 固有の仕組みです。例えば backend の termination status、primal / dual bound、
gap、実行時間、node 数、solution pool、adapter 固有の warning などが該当します。
そのため、diagnostics の形と意味は adapter と backend solver ごとに定義されます。
共通の OMMX 結果が必要な場合は Solution を参照してください。
backend solver が求解中に何を観測し、何を報告し、どこまで証明したかを確認したい場合に
diagnostics を使います。
共通の入口は solve() の予約済み diagnostics
keyword です。adapter は DiagnosticsSink を受け取り、
backend 固有の dataclass diagnostics を
DiagnosticsSink.record() で記録します。
どの diagnostic type を出力するかは adapter ごとに決まります。追加情報がない adapter は
sink に何も記録しなくても構いません。
adapter は solve 中、backend solver の callback 内から record() を呼ぶことがあります。
そのため collector は最終的な termination report の前に progress event を受け取れます。
一方、Experiment への保存は 1 Solve あたり 1 つの diagnostics BLOB として行われます。
組み込みの DiagnosticCollector.record() は受け取った Python object を append するだけで、
直接収集では object identity を保ちます。dataclass 変換と msgpack serialization は、
adapter が戻った後に Run.log_solve が最終的な diagnostics BLOB を保存するときまで遅延されます。
diagnostics の永続化は best-effort です。adapter が solution を返した後に dataclass 変換、
msgpack serialization、または diagnostics BLOB の保存に失敗しても、Run.log_solve は
Solve entry を記録し、diagnostics なしの Solve として保存します。
diagnostics sink は record() から例外を投げない規約です。記録に失敗した場合、sink は
失敗を log して正常に戻るべきです。sink が例外を投げた場合、それは sink 側の contract 違反であり、
adapter は回復を試みずにその例外を伝搬して構いません。
直接 solve して diagnostics を取得する#
adapter を直接呼ぶ場合は、ommx.adapter から export されている
DiagnosticCollector を diagnostics sink として渡します。collector には adapter が
記録した typed diagnostic report instance がそのまま保存されます。
以下は PySCIPOpt Adapter の例です。PySCIPOpt Adapter は、SCIP が監視対象の
progress event を出すたびに SCIPProgressSnapshot
を記録し、その後に
SCIPTerminationReport を 1 つ記録します。
from ommx.adapter import DiagnosticCollector
from ommx_pyscipopt_adapter import OMMXPySCIPOptAdapter, SCIPTerminationReport
collector = DiagnosticCollector()
solution = OMMXPySCIPOptAdapter.solve(
instance,
diagnostics=collector,
)
report = collector.diagnostics[-1]
assert isinstance(report, SCIPTerminationReport)
print(report.status)
print(report.primal_bound, report.dual_bound, report.gap)
collector.diagnostics は list です。adapter は複数の diagnostic event や report を記録でき、
具体的な item type は adapter 固有です。
Experiment に diagnostics を保存する#
log_solve() を使う場合、ユーザー側から
diagnostics keyword を渡さないでください。この keyword は Run.log_solve が予約しており、
adapter に diagnostics sink を渡して、記録された diagnostics を Experiment Artifact の
Solve entry に保存します。
from ommx.experiment import Experiment
from ommx_pyscipopt_adapter import OMMXPySCIPOptAdapter
with Experiment() as experiment:
with experiment.run() as run:
solution = run.log_solve(OMMXPySCIPOptAdapter, instance)
loaded_experiment = experiment
solve = loaded_experiment.runs[0].solves[0]
print(solve.diagnostics)
Experiment から diagnostics で読み出した
diagnostics は、元の dataclass instance ではなく dictionary の list として返ります。
これにより、保存済み Artifact は求解時に使われた Python class 定義から独立して読めます。
Solve entry には adapter class name と adapter options がすでに記録されています。
そのため diagnostics は Python type annotation なしで保存されます。どの analyzer に渡すかは
Solve の adapter metadata から判断し、例えば PySCIPOpt Adapter の diagnostics は
SCIPDiagnosticsAnalyzer に渡します。
solve() が OMMX Solution を返す前に例外を投げた場合でも、
可能な限り Run.log_solve は failed Solve entry を記録します。この entry は
status == "failed" または "interrupted"、output Solution なし、失敗前に収集済みの
diagnostics あり、という形で保存されます。
PySCIPOpt Adapter diagnostics#
diagnostics が要求された場合、PySCIPOpt Adapter は model.optimize() の前に SCIP の
event handler を登録します。現在は BESTSOLFOUND と DUALBOUNDIMPROVED event を
監視し、観測された event ごとに
SCIPProgressSnapshot を 1 つ記録します。各 snapshot は
SCIP event callback の中で見えている model state です。
SCIP は BESTSOLFOUND callback を、集計済みの model 統計がすべて更新される前に呼ぶことがあります。
各 snapshot はその callback から見えている model state として扱い、終了時点の値は
SCIPTerminationReport を参照してください。
PySCIPOpt Adapter は、model.optimize() が終了した後、PySCIPOpt model を OMMX
Solution に decode する前に最終的な
SCIPTerminationReport を記録します。このため、
direct DiagnosticCollector を渡している場合は、decode の段階で
InfeasibleDetected や
UnboundedDetected などの adapter exception が発生する場合でも、
SCIP の終了 report を確認できます。
diagnostic entry の完全な schema は API Reference を参照してください。
solve 後の解析には、typed collector の中身にも Experiment から読み出した dictionary にも
SCIPDiagnosticsAnalyzer を使えます。
from ommx_pyscipopt_adapter import SCIPDiagnosticsAnalyzer
analysis = SCIPDiagnosticsAnalyzer(collector.diagnostics)
progress = analysis.progress_df()
gap_series = analysis.gap_evolution_df()
incumbents = analysis.incumbent_evolution_df()
termination = analysis.termination_report
DataFrame helper は pandas を必要とします。pandas が使えない環境では
progress_records()、gap_evolution_records()、incumbent_evolution_records()、
termination_records() を使ってください。
bound と gap は SCIP から直接取得した値です。time limit などで最適性が証明されていない場合や、 OMMX Solution に decode できなかった場合に、SCIP がどこまで証明していたかを確認するために使えます。
from ommx.adapter import DiagnosticCollector, UnboundedDetected
from ommx_pyscipopt_adapter import OMMXPySCIPOptAdapter
collector = DiagnosticCollector()
try:
OMMXPySCIPOptAdapter.solve(instance, diagnostics=collector)
except UnboundedDetected:
report = collector.diagnostics[-1]
assert report.status == "unbounded"
print(report.dual_bound, report.gap)
Experiment から読み出した diagnostics では、各 progress event と termination report は
dictionary として表現されます。直接取得した場合と同じ records / DataFrame view が必要な場合は、
その list をそのまま SCIPDiagnosticsAnalyzer に渡してください。