From 08308d77c56652b9e1bc9bef13fb7704053b32dc Mon Sep 17 00:00:00 2001 From: mymissuniverse Date: Tue, 20 Jan 2026 03:57:04 +0900 Subject: [PATCH 01/11] Add trace_run recipe for structured trace output --- src/pyscipopt/recipes/trace_run.py | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/pyscipopt/recipes/trace_run.py diff --git a/src/pyscipopt/recipes/trace_run.py b/src/pyscipopt/recipes/trace_run.py new file mode 100644 index 000000000..daf2ceec3 --- /dev/null +++ b/src/pyscipopt/recipes/trace_run.py @@ -0,0 +1,66 @@ +import json + +from pyscipopt import SCIP_EVENTTYPE, Eventhdlr + + +class _TraceRun: + def __init__(self, model, path=None): + self.model = model + self.path = path + self._fh = None + self._handler = None + + def __enter__(self): + if not hasattr(self.model, "data") or self.model.data is None: + self.model.data = {} + self.model.data["trace"] = [] + + if self.path is not None: + self._fh = open(self.path, "w", buffering=1) + + class _TraceEventhdlr(Eventhdlr): + def eventinit(s): + s.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, s) + s.model.catchEvent(SCIP_EVENTTYPE.DUALBOUNDIMPROVED, s) + + def eventexec(s, event): + self._write_event("solution_update") + + def eventexit(s): + s.model.dropEvent(SCIP_EVENTTYPE.BESTSOLFOUND, s) + s.model.dropEvent(SCIP_EVENTTYPE.DUALBOUNDIMPROVED, s) + + self._handler = _TraceEventhdlr() + self.model.includeEventhdlr(self._handler, "trace_run", "Trace run handler") + + return None + + def __exit__(self, exc_type, exc, tb): + try: + self._write_event("solve_finish") + finally: + if self._fh: + self._fh.close() + self._fh = None + if self._handler is not None: + self._handler.eventexit() + self._handler = None + + def _write_event(self, event_type): + event = { + "type": event_type, + "time": self.model.getSolvingTime(), + "primalbound": self.model.getPrimalbound(), + "dualbound": self.model.getDualbound(), + "gap": self.model.getGap(), + "nodes": self.model.getNNodes(), + "nsol": self.model.getNSols(), + } + self.model.data["trace"].append(event) + if self._fh is not None: + self._fh.write(json.dumps(event) + "\n") + self._fh.flush() + + +def trace_run(model, path=None): + return _TraceRun(model, path) From 408b6da47adb1a7ce7b7d24c13a741951c2f5bee Mon Sep 17 00:00:00 2001 From: mymissuniverse Date: Wed, 21 Jan 2026 00:19:30 +0900 Subject: [PATCH 02/11] Refine trace_run recipe behavior --- src/pyscipopt/recipes/trace_run.py | 45 ++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/pyscipopt/recipes/trace_run.py b/src/pyscipopt/recipes/trace_run.py index daf2ceec3..93a726267 100644 --- a/src/pyscipopt/recipes/trace_run.py +++ b/src/pyscipopt/recipes/trace_run.py @@ -13,10 +13,10 @@ def __init__(self, model, path=None): def __enter__(self): if not hasattr(self.model, "data") or self.model.data is None: self.model.data = {} - self.model.data["trace"] = [] + self.model.data.setdefault("trace", []) if self.path is not None: - self._fh = open(self.path, "w", buffering=1) + self._fh = open(self.path, "w") class _TraceEventhdlr(Eventhdlr): def eventinit(s): @@ -24,29 +24,42 @@ def eventinit(s): s.model.catchEvent(SCIP_EVENTTYPE.DUALBOUNDIMPROVED, s) def eventexec(s, event): - self._write_event("solution_update") - - def eventexit(s): - s.model.dropEvent(SCIP_EVENTTYPE.BESTSOLFOUND, s) - s.model.dropEvent(SCIP_EVENTTYPE.DUALBOUNDIMPROVED, s) + et = event.getType() + if et == SCIP_EVENTTYPE.BESTSOLFOUND: + self._write_event("bestsol_found", flush=True) + elif et == SCIP_EVENTTYPE.DUALBOUNDIMPROVED: + self._write_event("dualbound_improved", flush=False) self._handler = _TraceEventhdlr() self.model.includeEventhdlr(self._handler, "trace_run", "Trace run handler") - return None + return self def __exit__(self, exc_type, exc, tb): try: - self._write_event("solve_finish") + self._write_event("run_end", flush=True) finally: if self._fh: - self._fh.close() - self._fh = None + try: + self._fh.close() + finally: + self._fh = None if self._handler is not None: - self._handler.eventexit() + try: + self.model.dropEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self._handler) + except Exception: + pass + try: + self.model.dropEvent( + SCIP_EVENTTYPE.DUALBOUNDIMPROVED, self._handler + ) + except Exception: + pass self._handler = None - def _write_event(self, event_type): + return False + + def _write_event(self, event_type, flush=True): event = { "type": event_type, "time": self.model.getSolvingTime(), @@ -56,10 +69,14 @@ def _write_event(self, event_type): "nodes": self.model.getNNodes(), "nsol": self.model.getNSols(), } + if event_type == "run_end": + status = self.model.getStatus() + event["status"] = getattr(status, "name", None) or repr(status) self.model.data["trace"].append(event) if self._fh is not None: self._fh.write(json.dumps(event) + "\n") - self._fh.flush() + if flush: + self._fh.flush() def trace_run(model, path=None): From 868022f62ea0317b56829d59c96c630083e31767 Mon Sep 17 00:00:00 2001 From: mymissuniverse Date: Wed, 21 Jan 2026 02:58:31 +0900 Subject: [PATCH 03/11] Add tests for trace_run recipe --- tests/test_trace_run.py | 84 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/test_trace_run.py diff --git a/tests/test_trace_run.py b/tests/test_trace_run.py new file mode 100644 index 000000000..dd1ec1a73 --- /dev/null +++ b/tests/test_trace_run.py @@ -0,0 +1,84 @@ +import json +from random import randint + +import pytest +from helpers.utils import bin_packing_model + +from pyscipopt import SCIP_EVENTTYPE, Eventhdlr +from pyscipopt.recipes.trace_run import trace_run + + +def test_trace_run_in_memory(): + model = bin_packing_model(sizes=[randint(1, 40) for _ in range(120)], capacity=50) + model.setParam("limits/time", 5) + + model.data = {"test": True} + + with trace_run(model, path=None): + model.optimize() + + assert "test" in model.data + assert "trace" in model.data + + required_fields = {"time", "primalbound", "dualbound", "gap", "nodes", "nsol"} + for record in model.data["trace"]: + assert required_fields <= set(record.keys()) + + primalbounds = [r["primalbound"] for r in model.data["trace"]] + for i in range(1, len(primalbounds)): + assert primalbounds[i] <= primalbounds[i - 1] + + dualbounds = [r["dualbound"] for r in model.data["trace"]] + for i in range(1, len(dualbounds)): + assert dualbounds[i] >= dualbounds[i - 1] + + types = [r["type"] for r in model.data["trace"]] + assert "run_end" in types + + +def test_trace_run_file_output(tmp_path): + model = bin_packing_model(sizes=[randint(1, 40) for _ in range(120)], capacity=50) + model.setParam("limits/time", 5) + + path = tmp_path / "trace.jsonl" + + with trace_run(model, path=str(path)): + model.optimize() + + assert path.exists() + + records = [json.loads(line) for line in path.read_text().splitlines()] + assert len(records) > 0 + + types = [r["type"] for r in records] + assert "run_end" in types + + +class _StopOnBest(Eventhdlr): + def eventinit(self): + self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self) + + def eventexec(self, event): + # SCIPが想定している安全な中断 + self.model.interruptSolve() + + def eventexit(self): + self.model.dropEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self) + + +def test_trace_run_forced_exception_after_bestsol(): + model = bin_packing_model(sizes=[randint(1, 40) for _ in range(120)], capacity=50) + + model.setParam("limits/time", 5) + + stopper = _StopOnBest() + model.includeEventhdlr(stopper, "stopper", "Stop on bestsol") + + with pytest.raises(RuntimeError): + with trace_run(model, path=None): + model.optimize() + raise RuntimeError("forced after interrupt") + + types = [r["type"] for r in model.data["trace"]] + assert "bestsol_found" in types + assert "run_end" in types From 9d2dfac30a6a71e060002acce460b040398bdea8 Mon Sep 17 00:00:00 2001 From: mymissuniverse Date: Wed, 21 Jan 2026 03:00:21 +0900 Subject: [PATCH 04/11] Rename tests for trace_run recipe --- tests/{test_trace_run.py => test_recipe_trace_run.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_trace_run.py => test_recipe_trace_run.py} (100%) diff --git a/tests/test_trace_run.py b/tests/test_recipe_trace_run.py similarity index 100% rename from tests/test_trace_run.py rename to tests/test_recipe_trace_run.py From 4ab23b2c71c931c84dcb40769fe63297b918d220 Mon Sep 17 00:00:00 2001 From: mymissuniverse Date: Fri, 23 Jan 2026 02:46:50 +0900 Subject: [PATCH 05/11] Refactor trace_run recipe to enhance event handling and optimize function naming --- src/pyscipopt/recipes/trace_run.py | 56 +++++++++++++++++++++--------- tests/test_recipe_trace_run.py | 31 ++++++----------- 2 files changed, 50 insertions(+), 37 deletions(-) diff --git a/src/pyscipopt/recipes/trace_run.py b/src/pyscipopt/recipes/trace_run.py index 93a726267..710f8dad7 100644 --- a/src/pyscipopt/recipes/trace_run.py +++ b/src/pyscipopt/recipes/trace_run.py @@ -20,15 +20,15 @@ def __enter__(self): class _TraceEventhdlr(Eventhdlr): def eventinit(s): - s.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, s) - s.model.catchEvent(SCIP_EVENTTYPE.DUALBOUNDIMPROVED, s) + self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, s) + self.model.catchEvent(SCIP_EVENTTYPE.DUALBOUNDIMPROVED, s) def eventexec(s, event): et = event.getType() if et == SCIP_EVENTTYPE.BESTSOLFOUND: - self._write_event("bestsol_found", flush=True) + self._write_event("bestsol_found", collect=True, flush=True) elif et == SCIP_EVENTTYPE.DUALBOUNDIMPROVED: - self._write_event("dualbound_improved", flush=False) + self._write_event("dualbound_improved", collect=True, flush=False) self._handler = _TraceEventhdlr() self.model.includeEventhdlr(self._handler, "trace_run", "Trace run handler") @@ -37,7 +37,15 @@ def eventexec(s, event): def __exit__(self, exc_type, exc, tb): try: - self._write_event("run_end", flush=True) + if exc_type is None: + self._write_event("run_end", collect=True, flush=True) + else: + self._write_event( + "run_end", + collect=False, + extra={"status": "exception", "exception": exc_type.__name__}, + flush=True, + ) finally: if self._fh: try: @@ -59,19 +67,29 @@ def __exit__(self, exc_type, exc, tb): return False - def _write_event(self, event_type, flush=True): + def _write_event(self, event_type, collect=False, extra=None, flush=True): event = { "type": event_type, - "time": self.model.getSolvingTime(), - "primalbound": self.model.getPrimalbound(), - "dualbound": self.model.getDualbound(), - "gap": self.model.getGap(), - "nodes": self.model.getNNodes(), - "nsol": self.model.getNSols(), } - if event_type == "run_end": - status = self.model.getStatus() - event["status"] = getattr(status, "name", None) or repr(status) + if collect: + event.update( + { + "time": self.model.getSolvingTime(), + "primalbound": self.model.getPrimalbound(), + "dualbound": self.model.getDualbound(), + "gap": self.model.getGap(), + "nodes": self.model.getNNodes(), + "nsol": self.model.getNSols(), + } + ) + + if event_type == "run_end": + status = self.model.getStatus() + event["status"] = getattr(status, "name", None) or repr(status) + + if extra: + event.update(extra) + self.model.data["trace"].append(event) if self._fh is not None: self._fh.write(json.dumps(event) + "\n") @@ -79,5 +97,9 @@ def _write_event(self, event_type, flush=True): self._fh.flush() -def trace_run(model, path=None): - return _TraceRun(model, path) +def optimize_with_trace(model, path=None, nogil=False): + with _TraceRun(model, path): + if nogil: + model.optimizeNogil() + else: + model.optimize() diff --git a/tests/test_recipe_trace_run.py b/tests/test_recipe_trace_run.py index dd1ec1a73..252c5a005 100644 --- a/tests/test_recipe_trace_run.py +++ b/tests/test_recipe_trace_run.py @@ -1,11 +1,10 @@ import json from random import randint -import pytest from helpers.utils import bin_packing_model from pyscipopt import SCIP_EVENTTYPE, Eventhdlr -from pyscipopt.recipes.trace_run import trace_run +from pyscipopt.recipes.trace_run import optimize_with_trace def test_trace_run_in_memory(): @@ -14,8 +13,7 @@ def test_trace_run_in_memory(): model.data = {"test": True} - with trace_run(model, path=None): - model.optimize() + optimize_with_trace(model, path=None) assert "test" in model.data assert "trace" in model.data @@ -42,8 +40,7 @@ def test_trace_run_file_output(tmp_path): path = tmp_path / "trace.jsonl" - with trace_run(model, path=str(path)): - model.optimize() + optimize_with_trace(model, path=str(path)) assert path.exists() @@ -54,30 +51,24 @@ def test_trace_run_file_output(tmp_path): assert "run_end" in types -class _StopOnBest(Eventhdlr): +class _InterruptOnBest(Eventhdlr): def eventinit(self): self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self) def eventexec(self, event): - # SCIPが想定している安全な中断 self.model.interruptSolve() - def eventexit(self): - self.model.dropEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self) - - -def test_trace_run_forced_exception_after_bestsol(): - model = bin_packing_model(sizes=[randint(1, 40) for _ in range(120)], capacity=50) +def test_optimize_with_trace_records_run_end_on_interrupt(): + model = bin_packing_model( + sizes=[randint(1, 40) for _ in range(120)], + capacity=50, + ) model.setParam("limits/time", 5) - stopper = _StopOnBest() - model.includeEventhdlr(stopper, "stopper", "Stop on bestsol") + model.includeEventhdlr(_InterruptOnBest(), "stopper", "Interrupt on bestsol") - with pytest.raises(RuntimeError): - with trace_run(model, path=None): - model.optimize() - raise RuntimeError("forced after interrupt") + optimize_with_trace(model, path=None, nogil=False) types = [r["type"] for r in model.data["trace"]] assert "bestsol_found" in types From 1f1bead008890dfc6ed501fb35b09d3764318d1d Mon Sep 17 00:00:00 2001 From: mymissuniverse Date: Sat, 24 Jan 2026 00:51:07 +0900 Subject: [PATCH 06/11] Enhance _TraceRun class to include snapshot logging and improve event handling; rename optimize_with_trace to optimizeTrace for clarity --- src/pyscipopt/recipes/trace_run.py | 94 ++++++++++++++++-------------- tests/test_recipe_trace_run.py | 8 +-- 2 files changed, 54 insertions(+), 48 deletions(-) diff --git a/src/pyscipopt/recipes/trace_run.py b/src/pyscipopt/recipes/trace_run.py index 710f8dad7..931d034ed 100644 --- a/src/pyscipopt/recipes/trace_run.py +++ b/src/pyscipopt/recipes/trace_run.py @@ -10,6 +10,8 @@ def __init__(self, model, path=None): self._fh = None self._handler = None + self._last_snapshot = {} + def __enter__(self): if not hasattr(self.model, "data") or self.model.data is None: self.model.data = {} @@ -26,9 +28,15 @@ def eventinit(s): def eventexec(s, event): et = event.getType() if et == SCIP_EVENTTYPE.BESTSOLFOUND: - self._write_event("bestsol_found", collect=True, flush=True) + snap = self._snapshot_now() + self._last_snapshot = snap + self._log_snapshot_event("bestsol_found", extra=snap, flush=True) elif et == SCIP_EVENTTYPE.DUALBOUNDIMPROVED: - self._write_event("dualbound_improved", collect=True, flush=False) + snap = self._snapshot_now() + self._last_snapshot = snap + self._log_snapshot_event( + "dualbound_improved", extra=snap, flush=False + ) self._handler = _TraceEventhdlr() self.model.includeEventhdlr(self._handler, "trace_run", "Trace run handler") @@ -36,57 +44,53 @@ def eventexec(s, event): return self def __exit__(self, exc_type, exc, tb): + extra = {} + if self._last_snapshot: + extra.update(self._last_snapshot) + + if exc_type is not None: + extra.update( + { + "status": "exception", + "exception": exc_type.__name__, + "message": str(exc) if exc is not None else None, + } + ) + try: - if exc_type is None: - self._write_event("run_end", collect=True, flush=True) - else: - self._write_event( - "run_end", - collect=False, - extra={"status": "exception", "exception": exc_type.__name__}, - flush=True, - ) + self._log_snapshot_event("run_end", extra=extra, flush=True) finally: if self._fh: try: self._fh.close() finally: self._fh = None + if self._handler is not None: - try: - self.model.dropEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self._handler) - except Exception: - pass - try: - self.model.dropEvent( - SCIP_EVENTTYPE.DUALBOUNDIMPROVED, self._handler - ) - except Exception: - pass + for et in ( + SCIP_EVENTTYPE.BESTSOLFOUND, + SCIP_EVENTTYPE.DUALBOUNDIMPROVED, + ): + try: + self.model.dropEvent(et, self._handler) + except Exception: + pass self._handler = None return False - def _write_event(self, event_type, collect=False, extra=None, flush=True): - event = { - "type": event_type, + def _snapshot_now(self) -> dict: + return { + "time": self.model.getSolvingTime(), + "primalbound": self.model.getPrimalbound(), + "dualbound": self.model.getDualbound(), + "gap": self.model.getGap(), + "nodes": self.model.getNNodes(), + "nsol": self.model.getNSols(), } - if collect: - event.update( - { - "time": self.model.getSolvingTime(), - "primalbound": self.model.getPrimalbound(), - "dualbound": self.model.getDualbound(), - "gap": self.model.getGap(), - "nodes": self.model.getNNodes(), - "nsol": self.model.getNSols(), - } - ) - - if event_type == "run_end": - status = self.model.getStatus() - event["status"] = getattr(status, "name", None) or repr(status) + def _log_snapshot_event(self, event_type, extra=None, flush=True): + event = {"type": event_type} if extra: event.update(extra) @@ -97,9 +101,11 @@ def _write_event(self, event_type, collect=False, extra=None, flush=True): self._fh.flush() -def optimize_with_trace(model, path=None, nogil=False): +def optimizeTrace(model, path=None): + with _TraceRun(model, path): + model.optimize() + + +def optimizeNogilTrace(model, path=None): with _TraceRun(model, path): - if nogil: - model.optimizeNogil() - else: - model.optimize() + model.optimizeNogil() diff --git a/tests/test_recipe_trace_run.py b/tests/test_recipe_trace_run.py index 252c5a005..cb8237fe2 100644 --- a/tests/test_recipe_trace_run.py +++ b/tests/test_recipe_trace_run.py @@ -4,7 +4,7 @@ from helpers.utils import bin_packing_model from pyscipopt import SCIP_EVENTTYPE, Eventhdlr -from pyscipopt.recipes.trace_run import optimize_with_trace +from pyscipopt.recipes.trace_run import optimizeTrace def test_trace_run_in_memory(): @@ -13,7 +13,7 @@ def test_trace_run_in_memory(): model.data = {"test": True} - optimize_with_trace(model, path=None) + optimizeTrace(model, path=None) assert "test" in model.data assert "trace" in model.data @@ -40,7 +40,7 @@ def test_trace_run_file_output(tmp_path): path = tmp_path / "trace.jsonl" - optimize_with_trace(model, path=str(path)) + optimizeTrace(model, path=str(path)) assert path.exists() @@ -68,7 +68,7 @@ def test_optimize_with_trace_records_run_end_on_interrupt(): model.includeEventhdlr(_InterruptOnBest(), "stopper", "Interrupt on bestsol") - optimize_with_trace(model, path=None, nogil=False) + optimizeTrace(model, path=None) types = [r["type"] for r in model.data["trace"]] assert "bestsol_found" in types From 2c9444cd68de2dd9ed8c1e268e2d7fe74e359842 Mon Sep 17 00:00:00 2001 From: mymissuniverse Date: Sat, 24 Jan 2026 01:14:55 +0900 Subject: [PATCH 07/11] Refactor _TraceRun class to replace snapshot logging methods with a unified event writing method, improving clarity and consistency in event handling. --- src/pyscipopt/recipes/trace_run.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/pyscipopt/recipes/trace_run.py b/src/pyscipopt/recipes/trace_run.py index 931d034ed..aecdc3a67 100644 --- a/src/pyscipopt/recipes/trace_run.py +++ b/src/pyscipopt/recipes/trace_run.py @@ -28,14 +28,14 @@ def eventinit(s): def eventexec(s, event): et = event.getType() if et == SCIP_EVENTTYPE.BESTSOLFOUND: - snap = self._snapshot_now() - self._last_snapshot = snap - self._log_snapshot_event("bestsol_found", extra=snap, flush=True) + snapshot = self._snapshot_now() + self._last_snapshot = snapshot + self._write_event("bestsol_found", fields=snapshot, flush=True) elif et == SCIP_EVENTTYPE.DUALBOUNDIMPROVED: - snap = self._snapshot_now() - self._last_snapshot = snap - self._log_snapshot_event( - "dualbound_improved", extra=snap, flush=False + snapshot = self._snapshot_now() + self._last_snapshot = snapshot + self._write_event( + "dualbound_improved", fields=snapshot, flush=False ) self._handler = _TraceEventhdlr() @@ -44,12 +44,12 @@ def eventexec(s, event): return self def __exit__(self, exc_type, exc, tb): - extra = {} + fields = {} if self._last_snapshot: - extra.update(self._last_snapshot) + fields.update(self._last_snapshot) if exc_type is not None: - extra.update( + fields.update( { "status": "exception", "exception": exc_type.__name__, @@ -58,7 +58,7 @@ def __exit__(self, exc_type, exc, tb): ) try: - self._log_snapshot_event("run_end", extra=extra, flush=True) + self._write_event("run_end", fields=fields, flush=True) finally: if self._fh: try: @@ -89,10 +89,10 @@ def _snapshot_now(self) -> dict: "nsol": self.model.getNSols(), } - def _log_snapshot_event(self, event_type, extra=None, flush=True): + def _write_event(self, event_type, fields=None, flush=True): event = {"type": event_type} - if extra: - event.update(extra) + if fields: + event.update(fields) self.model.data["trace"].append(event) if self._fh is not None: From 211359a3a8faf8b4f00c31ab895a5bffbd447d61 Mon Sep 17 00:00:00 2001 From: mymissuniverse Date: Sat, 24 Jan 2026 01:22:32 +0900 Subject: [PATCH 08/11] Refactor tests for trace_run recipe to use parameterized optimize function, enhancing test coverage for both optimizeTrace and optimizeNogilTrace. Update assertions for trace data consistency. --- tests/test_recipe_trace_run.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/tests/test_recipe_trace_run.py b/tests/test_recipe_trace_run.py index cb8237fe2..575d09577 100644 --- a/tests/test_recipe_trace_run.py +++ b/tests/test_recipe_trace_run.py @@ -1,46 +1,58 @@ import json from random import randint +import pytest from helpers.utils import bin_packing_model from pyscipopt import SCIP_EVENTTYPE, Eventhdlr -from pyscipopt.recipes.trace_run import optimizeTrace +from pyscipopt.recipes.trace_run import optimizeNogilTrace, optimizeTrace -def test_trace_run_in_memory(): +@pytest.fixture( + params=[optimizeTrace, optimizeNogilTrace], ids=["optimize", "optimize_nogil"] +) +def optimize(request): + return request.param + + +def test_trace_run_in_memory(optimize): model = bin_packing_model(sizes=[randint(1, 40) for _ in range(120)], capacity=50) model.setParam("limits/time", 5) model.data = {"test": True} - optimizeTrace(model, path=None) + optimize(model, path=None) assert "test" in model.data assert "trace" in model.data required_fields = {"time", "primalbound", "dualbound", "gap", "nodes", "nsol"} + + types = [r["type"] for r in model.data["trace"]] + assert ("bestsol_found" in types) or ("dualbound_improved" in types) + for record in model.data["trace"]: - assert required_fields <= set(record.keys()) + if record["type"] != "run_end": + assert required_fields <= set(record.keys()) - primalbounds = [r["primalbound"] for r in model.data["trace"]] + primalbounds = [r["primalbound"] for r in model.data["trace"] if "primalbound" in r] for i in range(1, len(primalbounds)): assert primalbounds[i] <= primalbounds[i - 1] - dualbounds = [r["dualbound"] for r in model.data["trace"]] + dualbounds = [r["dualbound"] for r in model.data["trace"] if "dualbound" in r] for i in range(1, len(dualbounds)): assert dualbounds[i] >= dualbounds[i - 1] - types = [r["type"] for r in model.data["trace"]] assert "run_end" in types -def test_trace_run_file_output(tmp_path): +def test_trace_run_file_output(optimize, tmp_path): model = bin_packing_model(sizes=[randint(1, 40) for _ in range(120)], capacity=50) model.setParam("limits/time", 5) path = tmp_path / "trace.jsonl" - optimizeTrace(model, path=str(path)) + optimize(model, path=str(path)) assert path.exists() @@ -59,7 +71,7 @@ def eventexec(self, event): self.model.interruptSolve() -def test_optimize_with_trace_records_run_end_on_interrupt(): +def test_optimize_with_trace_records_run_end_on_interrupt(optimize): model = bin_packing_model( sizes=[randint(1, 40) for _ in range(120)], capacity=50, @@ -68,7 +80,7 @@ def test_optimize_with_trace_records_run_end_on_interrupt(): model.includeEventhdlr(_InterruptOnBest(), "stopper", "Interrupt on bestsol") - optimizeTrace(model, path=None) + optimize(model, path=None) types = [r["type"] for r in model.data["trace"]] assert "bestsol_found" in types From 2e8f3bdf5548a0c531c385a4581db5858be27da8 Mon Sep 17 00:00:00 2001 From: mymissuniverse Date: Sat, 24 Jan 2026 01:44:55 +0900 Subject: [PATCH 09/11] Add docstring to _TraceRun class for real-time optimization progress tracking This update introduces a comprehensive docstring for the _TraceRun class, detailing its purpose, arguments, return values, and usage examples. This enhancement improves code documentation and usability for future developers. --- src/pyscipopt/recipes/trace_run.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/pyscipopt/recipes/trace_run.py b/src/pyscipopt/recipes/trace_run.py index aecdc3a67..c204c763f 100644 --- a/src/pyscipopt/recipes/trace_run.py +++ b/src/pyscipopt/recipes/trace_run.py @@ -4,6 +4,28 @@ class _TraceRun: + """ + Record optimization progress in real time while the solver is running. + + Args + ---- + model: pyscipopt.Model + path: str | None + - None: in-memory only + - str : also write JSONL (one JSON object per line) for streaming/real-time consumption + + Returns + ------- + None + Updates `model.data["trace"]` as a side effect. + + Usage + ----- + optimizeTrace(model) # real-time in-memory trace + optimizeTrace(model, "trace.jsonl") # real-time JSONL stream + in-memory + optimizeNogilTrace(model, "trace.jsonl") # nogil variant + """ + def __init__(self, model, path=None): self.model = model self.path = path From afbe9e07dd4a8c3e487025c0563a1981172feeca Mon Sep 17 00:00:00 2001 From: mymissuniverse Date: Sat, 24 Jan 2026 02:03:32 +0900 Subject: [PATCH 10/11] Add realtime_trace_jsonl recipe for real-time optimization progress tracking with JSONL output This commit introduces the realtime_trace_jsonl recipe, which allows for real-time tracking of optimization progress and outputs the data in JSONL format. Additionally, the CHANGELOG has been updated to reflect this new feature. --- CHANGELOG.md | 1 + .../recipes/{trace_run.py => realtime_trace_jsonl.py} | 4 +++- ...ipe_trace_run.py => test_recipe_realtime_trace_jsonl.py} | 6 +++--- 3 files changed, 7 insertions(+), 4 deletions(-) rename src/pyscipopt/recipes/{trace_run.py => realtime_trace_jsonl.py} (96%) rename tests/{test_recipe_trace_run.py => test_recipe_realtime_trace_jsonl.py} (92%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0295fff25..e389fa7b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Include parameter names in type stubs - Speed up MatrixExpr.sum(axis=...) via quicksum - Added structured_optimization_trace recipe for structured optimization progress tracking +- Added realtime_trace_jsonl recipe for real-time optimization progress tracking with JSONL streaming output ### Fixed - all fundamental callbacks now raise an error if not implemented - Fixed the type of MatrixExpr.sum(axis=...) result from MatrixVariable to MatrixExpr. diff --git a/src/pyscipopt/recipes/trace_run.py b/src/pyscipopt/recipes/realtime_trace_jsonl.py similarity index 96% rename from src/pyscipopt/recipes/trace_run.py rename to src/pyscipopt/recipes/realtime_trace_jsonl.py index c204c763f..b5d124d8c 100644 --- a/src/pyscipopt/recipes/trace_run.py +++ b/src/pyscipopt/recipes/realtime_trace_jsonl.py @@ -61,7 +61,9 @@ def eventexec(s, event): ) self._handler = _TraceEventhdlr() - self.model.includeEventhdlr(self._handler, "trace_run", "Trace run handler") + self.model.includeEventhdlr( + self._handler, "realtime_trace_jsonl", "Realtime trace jsonl handler" + ) return self diff --git a/tests/test_recipe_trace_run.py b/tests/test_recipe_realtime_trace_jsonl.py similarity index 92% rename from tests/test_recipe_trace_run.py rename to tests/test_recipe_realtime_trace_jsonl.py index 575d09577..61db00bbc 100644 --- a/tests/test_recipe_trace_run.py +++ b/tests/test_recipe_realtime_trace_jsonl.py @@ -5,7 +5,7 @@ from helpers.utils import bin_packing_model from pyscipopt import SCIP_EVENTTYPE, Eventhdlr -from pyscipopt.recipes.trace_run import optimizeNogilTrace, optimizeTrace +from pyscipopt.recipes.realtime_trace_jsonl import optimizeNogilTrace, optimizeTrace @pytest.fixture( @@ -15,7 +15,7 @@ def optimize(request): return request.param -def test_trace_run_in_memory(optimize): +def test_realtime_trace_in_memory(optimize): model = bin_packing_model(sizes=[randint(1, 40) for _ in range(120)], capacity=50) model.setParam("limits/time", 5) @@ -46,7 +46,7 @@ def test_trace_run_in_memory(optimize): assert "run_end" in types -def test_trace_run_file_output(optimize, tmp_path): +def test_realtime_trace_file_output(optimize, tmp_path): model = bin_packing_model(sizes=[randint(1, 40) for _ in range(120)], capacity=50) model.setParam("limits/time", 5) From 6bf73554ce8bd230b93da0e82ca7eab48c8946a6 Mon Sep 17 00:00:00 2001 From: mymissuniverse Date: Sat, 24 Jan 2026 23:04:31 +0900 Subject: [PATCH 11/11] Update usage examples in _TraceRun class docstring to use keyword arguments for clarity --- src/pyscipopt/recipes/realtime_trace_jsonl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/recipes/realtime_trace_jsonl.py b/src/pyscipopt/recipes/realtime_trace_jsonl.py index b5d124d8c..ef8f681ab 100644 --- a/src/pyscipopt/recipes/realtime_trace_jsonl.py +++ b/src/pyscipopt/recipes/realtime_trace_jsonl.py @@ -22,8 +22,8 @@ class _TraceRun: Usage ----- optimizeTrace(model) # real-time in-memory trace - optimizeTrace(model, "trace.jsonl") # real-time JSONL stream + in-memory - optimizeNogilTrace(model, "trace.jsonl") # nogil variant + optimizeTrace(model, path="trace.jsonl") # real-time JSONL stream + in-memory + optimizeNogilTrace(model, path="trace.jsonl") # nogil variant """ def __init__(self, model, path=None):