Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 setTracefile() method for structured optimization progress logging
### Fixed
- all fundamental callbacks now raise an error if not implemented
- Fixed the type of MatrixExpr.sum(axis=...) result from MatrixVariable to MatrixExpr.
Expand Down
3 changes: 3 additions & 0 deletions src/pyscipopt/scip.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -2228,6 +2228,9 @@ cdef class Model:
cdef _benders_subproblems
# store iis, if found
cdef SCIP_IIS* _iis
cdef _tracefile_path
cdef _tracefile_mode
cdef _tracefile_handle

@staticmethod
cdef create(SCIP* scip)
66 changes: 65 additions & 1 deletion src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import sys
import warnings
import locale
import json

cimport cython
from cpython cimport Py_INCREF, Py_DECREF
Expand Down Expand Up @@ -316,7 +317,7 @@
if rc == SCIP_OKAY:
pass
elif rc == SCIP_ERROR:
raise Exception('SCIP: unspecified error!')

Check failure on line 320 in src/pyscipopt/scip.pxi

View workflow job for this annotation

GitHub Actions / test-coverage (3.11)

SCIP: unspecified error!
elif rc == SCIP_NOMEMORY:
raise MemoryError('SCIP: insufficient memory error!')
elif rc == SCIP_READERROR:
Expand All @@ -335,7 +336,7 @@
raise Exception('SCIP: method cannot be called at this time'
+ ' in solution process!')
elif rc == SCIP_INVALIDDATA:
raise Exception('SCIP: error in input data!')

Check failure on line 339 in src/pyscipopt/scip.pxi

View workflow job for this annotation

GitHub Actions / test-coverage (3.11)

SCIP: error in input data!
elif rc == SCIP_INVALIDRESULT:
raise Exception('SCIP: method returned an invalid result code!')
elif rc == SCIP_PLUGINNOTFOUND:
Expand Down Expand Up @@ -2771,6 +2772,19 @@

PY_SCIP_CALL(SCIPiisGreedyMakeIrreducible(self._iis))

class _TraceEventHandler(Eventhdlr):
"""Internal event handler for trace output."""

def eventinit(self):
self.model.catchEvent(SCIP_EVENTTYPE_BESTSOLFOUND, self)

def eventexit(self):
self.model.dropEvent(SCIP_EVENTTYPE_BESTSOLFOUND, self)

def eventexec(self, event):
if event.getType() == SCIP_EVENTTYPE_BESTSOLFOUND:
self.model._write_trace_event("solution_update")


# - remove create(), includeDefaultPlugins(), createProbBasic() methods
# - replace free() by "destructor"
Expand Down Expand Up @@ -2816,6 +2830,9 @@
self._generated_event_handlers_count = 0
self._benders_subproblems = [] # Keep references to Benders subproblem Models
self._iis = NULL
self._tracefile_path = None
self._tracefile_mode = "a"
self._tracefile_handle = None

if not createscip:
# if no SCIP instance should be created, then an empty Model object is created.
Expand Down Expand Up @@ -8490,9 +8507,24 @@

def optimize(self):
"""Optimize the problem."""
PY_SCIP_CALL(SCIPsolve(self._scip))
tracefile = None
if self._tracefile_path:
tracefile = open(self._tracefile_path, self._tracefile_mode)
self._tracefile_handle = tracefile
handler = _TraceEventHandler()
self.includeEventhdlr(handler, "trace_handler", "Trace event handler")

try:
PY_SCIP_CALL(SCIPsolve(self._scip))
finally:
self._write_trace_event("solve_finish")
if tracefile:
tracefile.close()
self._tracefile_handle = None

self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip))


def optimizeNogil(self):
"""Optimize the problem without GIL."""
cdef SCIP_RETCODE rc;
Expand Down Expand Up @@ -11267,6 +11299,38 @@
SCIPsetMessagehdlrLogfile(self._scip, c_path)
else:
SCIPsetMessagehdlrLogfile(self._scip, NULL)

def setTracefile(self, path, mode="a"):
"""
Enable or disable structured trace output to a file.

Trace output is a machine-readable JSONL format, separate from the human-readable log controlled by setLogfile().

Parameters
----------
path : str or None
Path to trace file, or None to disable tracing.
mode : str
"a" (append, default) or "w" (overwrite).
"""
self._tracefile_path = path
self._tracefile_mode = mode

def _write_trace_event(self, event_type):
"""Write a trace event to the trace file."""
f = self._tracefile_handle
if f:
ev = {
"type": event_type,
"time": self.getSolvingTime(),
"primalbound": self.getPrimalbound(),
"dualbound": self.getDualbound(),
"gap": self.getGap(),
"nodes": self.getNNodes(),
"nsol": self.getNSols(),
}
f.write(json.dumps(ev) + "\n")


# Parameter Methods

Expand Down
1 change: 1 addition & 0 deletions src/pyscipopt/scip.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,7 @@ class Model:
self, solution: Incomplete, var: Incomplete, val: Incomplete
) -> Incomplete: ...
def setStringParam(self, name: Incomplete, value: Incomplete) -> Incomplete: ...
def setTracefile(self, path: Incomplete, mode: Incomplete = ...) -> Incomplete: ...
def setupBendersSubproblem(
self,
probnumber: Incomplete,
Expand Down
54 changes: 54 additions & 0 deletions tests/test_tracefile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import json

from pyscipopt import Model


def test_tracefile_basic(tmp_path):
"""Basic tracefile functionality test."""
trace_path = tmp_path / "trace.jsonl"

m = Model()
m.hideOutput()
x = m.addVar("x", vtype="I", lb=0, ub=10)
m.setObjective(x, "maximize")
m.setTracefile(str(trace_path))
m.optimize()

assert trace_path.exists()

with open(trace_path) as f:
lines = f.readlines()

assert len(lines) >= 1

events = [json.loads(line) for line in lines]
types = [e["type"] for e in events]

assert "solve_finish" in types

required_fields = {
"type",
"time",
"primalbound",
"dualbound",
"gap",
"nodes",
"nsol",
}
for e in events:
assert required_fields <= set(e.keys())


def test_tracefile_none(tmp_path):
"""Test disabling tracefile with None."""
trace_path = tmp_path / "trace.jsonl"

m = Model()
m.hideOutput()
x = m.addVar("x", vtype="I", lb=0, ub=10)
m.setObjective(x, "maximize")
m.setTracefile(str(trace_path))
m.setTracefile(None) # Disable
m.optimize()

assert not trace_path.exists()
Loading