diff --git a/CHANGELOG.md b/CHANGELOG.md index 69f3fe58e..0295fff25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Added automated script for generating type stubs - Include parameter names in type stubs - Speed up MatrixExpr.sum(axis=...) via quicksum +- Added structured_optimization_trace recipe for structured optimization progress tracking ### 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/structured_optimization_trace.py b/src/pyscipopt/recipes/structured_optimization_trace.py new file mode 100644 index 000000000..4ba73def7 --- /dev/null +++ b/src/pyscipopt/recipes/structured_optimization_trace.py @@ -0,0 +1,37 @@ +from pyscipopt import SCIP_EVENTTYPE, Eventhdlr, Model + + +def attach_structured_optimization_trace(model: Model): + """ + Attaches an event handler that records optimization progress in structured JSONL format. + + Args: + model: SCIP Model + """ + + class _TraceEventhdlr(Eventhdlr): + def eventinit(self): + self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self) + self.model.catchEvent(SCIP_EVENTTYPE.DUALBOUNDIMPROVED, self) + + def eventexec(self, event): + record = { + "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(record) + + if not hasattr(model, "data") or model.data is None: + model.data = {} + model.data["trace"] = [] + + hdlr = _TraceEventhdlr() + model.includeEventhdlr( + hdlr, "structured_trace", "Structured optimization trace handler" + ) + + return model diff --git a/tests/test_recipe_structured_optimization_trace.py b/tests/test_recipe_structured_optimization_trace.py new file mode 100644 index 000000000..334d5107c --- /dev/null +++ b/tests/test_recipe_structured_optimization_trace.py @@ -0,0 +1,32 @@ +from helpers.utils import bin_packing_model + +from pyscipopt.recipes.structured_optimization_trace import ( + attach_structured_optimization_trace, +) + + +def test_structured_optimization_trace(): + from random import randint + + model = bin_packing_model(sizes=[randint(1, 40) for _ in range(120)], capacity=50) + model.setParam("limits/time", 5) + + model.data = {"test": True} + model = attach_structured_optimization_trace(model) + + assert "test" in model.data + assert "trace" in model.data + + model.optimize() + + 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]