Skip to content

Conversation

@MySweetEden
Copy link
Contributor

Motivation

PySCIPOpt already has recipe(s) that store optimization progress in memory. However, in-memory traces are not suitable for real-time, external observation (e.g., another process tailing progress, dashboards, log collectors).

This recipe focuses on the missing piece: a stream-friendly, structured output that can be consumed outside the running Python process during solve.

Design Decisions

  • JSONL format: Designed for streaming writes and partial reads; remains readable even if the run is interrupted or crashes
  • Real-time external output is the primary value:
    • Records progress updates as one JSON object per line
    • Flushes on key events so downstream consumers can react immediately
  • Schema compatibility with setTracefile() (PR Add setTracefile() method for structured optimization progress loggingAdd settracefile api #1158):
    • Uses the same JSONL field names (type, time, primalbound, dualbound, gap, nodes, nsol) for consistency across tracing approaches.
  • In-memory + file output: Keeps model.data["trace"] for convenience/testing, but the recipe is centered on file streaming via path=...
  • Robust termination signaling for external monitoring:
    • Always emits a final run_end record on normal termination, interruption, or exception
    • On exceptions, run_end includes structured error metadata (status, exception type, message)
    • Flushes run_end to make completion detection reliable

Events Recorded

  • bestsol_found: when a new best solution is found
  • dualbound_improved: when the dual bound improves
  • run_end: when optimization terminates (also emitted on interrupt/exception)

Fields

type, time, primalbound, dualbound, gap, nodes, nsol (aligned with the JSONL trace schema introduced in PR #1158)
(run_end may additionally include: status, exception, message on failure)

MySweetEden and others added 13 commits January 20, 2026 03:57
… handling; rename optimize_with_trace to optimizeTrace for clarity
…nified event writing method, improving clarity and consistency in event handling.
…ction, enhancing test coverage for both optimizeTrace and optimizeNogilTrace. Update assertions for trace data consistency.
…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.
…racking 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.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new PySCIPOpt recipe to stream structured optimization progress in real time using JSONL, enabling external tailing/monitoring while the solver runs.

Changes:

  • Introduces realtime_trace_jsonl recipe with optimizeTrace() / optimizeNogilTrace() to record selected SCIP events into model.data["trace"] and optionally a JSONL file.
  • Records bestsol_found, dualbound_improved, and a final run_end event with flushing intended for real-time consumption.
  • Adds tests covering in-memory tracing, file output, and interrupt handling; updates changelog.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
src/pyscipopt/recipes/realtime_trace_jsonl.py Implements the real-time JSONL tracing recipe and event handling.
tests/test_recipe_realtime_trace_jsonl.py Adds tests for in-memory traces, JSONL file output, and interruption behavior.
CHANGELOG.md Documents the addition of the new recipe.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +63 to +66
self._handler = _TraceEventhdlr()
self.model.includeEventhdlr(
self._handler, "realtime_trace_jsonl", "Realtime trace jsonl handler"
)
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

includeEventhdlr() registers an event handler plugin permanently (there is no corresponding remove/uninclude API). Calling optimizeTrace()/optimizeNogilTrace() multiple times on the same model will attempt to include another handler with the same name (realtime_trace_jsonl), which can raise a SCIP error and/or leave multiple live handlers capturing closed file handles and old _TraceRun instances. Refactor to include the handler at most once per model (e.g., stash/reuse it in model.data), and make the handler read its current sink (trace list / file handle) from mutable attributes rather than a closure over a per-run object.

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +101
if self._handler is not None:
for et in (
SCIP_EVENTTYPE.BESTSOLFOUND,
SCIP_EVENTTYPE.DUALBOUNDIMPROVED,
):
try:
self.model.dropEvent(et, self._handler)
except Exception:
pass
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dropEvent() decrements the model refcount unconditionally (see Model.dropEvent in scip.pxi), so calling it from __exit__ is unsafe if eventinit() never executed or if the events were not actually caught (it can underflow the INCREF/DECREF pairing). A safer pattern is to implement eventexit() on the event handler to drop exactly the events it caught (and only after eventinit() ran), and remove the unconditional dropEvent() calls from this context manager.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +61
self._write_event(
"dualbound_improved", fields=snapshot, flush=False
)
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a recipe marketed as “real-time JSONL streaming”, not flushing dualbound_improved events can delay visibility for external consumers tailing the file. Consider flushing here as well (or making flushing policy configurable), especially since dualbound_improved is one of the primary progress signals you record.

Copilot uses AI. Check for mistakes.
def __enter__(self):
if not hasattr(self.model, "data") or self.model.data is None:
self.model.data = {}
self.model.data.setdefault("trace", [])
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

model.data.setdefault("trace", []) will append to an existing trace from previous runs on the same model, while the file output is truncated (open(..., "w")). To keep in-memory and file behavior consistent (and avoid mixing multiple runs), reset model.data["trace"] to a new empty list at the start of each traced run.

Suggested change
self.model.data.setdefault("trace", [])
# Start a fresh trace for each run to keep in-memory and file behavior consistent
self.model.data["trace"] = []

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +50
def eventinit(s):
self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, s)
self.model.catchEvent(SCIP_EVENTTYPE.DUALBOUNDIMPROVED, s)

def eventexec(s, event):
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normal methods should have 'self', rather than 's', as their first parameter.

Suggested change
def eventinit(s):
self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, s)
self.model.catchEvent(SCIP_EVENTTYPE.DUALBOUNDIMPROVED, s)
def eventexec(s, event):
def eventinit(self):
self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self)
self.model.catchEvent(SCIP_EVENTTYPE.DUALBOUNDIMPROVED, self)
def eventexec(self, event):

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +50
def eventinit(s):
self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, s)
self.model.catchEvent(SCIP_EVENTTYPE.DUALBOUNDIMPROVED, s)

def eventexec(s, event):
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normal methods should have 'self', rather than 's', as their first parameter.

Suggested change
def eventinit(s):
self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, s)
self.model.catchEvent(SCIP_EVENTTYPE.DUALBOUNDIMPROVED, s)
def eventexec(s, event):
def eventinit(handler):
self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, handler)
self.model.catchEvent(SCIP_EVENTTYPE.DUALBOUNDIMPROVED, handler)
def eventexec(handler, event):

Copilot uses AI. Check for mistakes.
):
try:
self.model.dropEvent(et, self._handler)
except Exception:
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
@MySweetEden
Copy link
Contributor Author

I’ll address the comments over the weekend and push updates soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant