Add setTracefile() method for structured optimization progress loggingAdd settracefile api#1158
Conversation
3b26fd1 to
6b7bd0e
Compare
|
Hey @MySweetEden , thank you for the contribution! However, can you please make it as a recipe, rather than a part of from pyscipopt import Model
from pyscipopt.recipes.traceback import attach_traceback_eventhdlr
m = Model()
attach_traceback_eventhdlr(m)A bit like the primal_dual_evolution recipe. How do you feel about this approach? Everything could be implemented in Python, as well. |
|
Thank you for the feedback! I tried implementing it as a recipe, similar to SummaryDuring implementation, I discovered a constraint: the final optimization values cannot be reliably recorded with the recipe approach. Constraint Found
Impact on Existing CodeThe same constraint affects !pip install pyscipopt --quiet
from pyscipopt import Model
from pyscipopt.recipes.primal_dual_evolution import attach_primal_dual_evolution_eventhdlr
m = Model()
m.hideOutput()
x = m.addVar('x', vtype='I', lb=0, ub=10)
y = m.addVar('y', vtype='I', lb=0, ub=10)
m.addCons(x + y <= 15)
m.setObjective(x + 2*y, 'maximize')
attach_primal_dual_evolution_eventhdlr(m)
m.optimize()
print('primal_log:', m.data['primal_log'])
print('dual_log:', m.data['dual_log'])
print(f'Final: primalbound={m.getPrimalbound()}, dualbound={m.getDualbound()}, gap={m.getGap()}')The final primalbound is 25.0, but the last recorded value in Possible DirectionsA. Accept the limitation in recipes
B. Add a hook in PySCIPOpt's
C. Propose a new event type to SCIP (Not recommended)
Open Questions
|
|
Ah, this is another problem, but still good that you caught it! Apparently, |
|
Every improvements should trigger an event, so this is a bug somewhere. |
|
@DominikKamp can reproduce in SCIP with /**
* Test to check if BESTSOLFOUND events are triggered during presolving.
*
* Bug report: https://github.com/scipopt/PySCIPOpt/pull/1158
*
* Expected: Events should be triggered when solutions are found during presolving.
* Observed: Events are NOT triggered during presolving.
*/
#include <stdio.h>
#include <string.h>
#include <scip/scip.h>
#include <scip/scipdefplugins.h>
/* Event handler data */
struct SCIP_EventhdlrData {
int bestSolCount;
};
/* Callback when event is executed */
static SCIP_DECL_EVENTEXEC(eventExecTest)
{
SCIP_EVENTHDLRDATA* eventhdlrdata;
SCIP_EVENTTYPE eventtype;
eventhdlrdata = SCIPeventhdlrGetData(eventhdlr);
eventtype = SCIPeventGetType(event);
if (eventtype & SCIP_EVENTTYPE_BESTSOLFOUND) {
eventhdlrdata->bestSolCount++;
printf("EVENT: BESTSOLFOUND #%d - primalbound=%.2f, time=%.4f\n",
eventhdlrdata->bestSolCount,
SCIPgetPrimalbound(scip),
SCIPgetSolvingTime(scip));
}
return SCIP_OKAY;
}
/* Callback when event handler is initialized for solving */
static SCIP_DECL_EVENTINITSOL(eventInitsolTest)
{
printf("EVENT HANDLER: eventinitsol called\n");
SCIP_CALL(SCIPcatchEvent(scip, SCIP_EVENTTYPE_BESTSOLFOUND, eventhdlr, NULL, NULL));
return SCIP_OKAY;
}
/* Callback when event handler exits solving */
static SCIP_DECL_EVENTEXITSOL(eventExitsolTest)
{
printf("EVENT HANDLER: eventexitsol called\n");
SCIP_CALL(SCIPdropEvent(scip, SCIP_EVENTTYPE_BESTSOLFOUND, eventhdlr, NULL, -1));
return SCIP_OKAY;
}
/* Include the event handler */
static SCIP_RETCODE includeEventHandler(SCIP* scip, SCIP_EVENTHDLRDATA* eventhdlrdata)
{
SCIP_EVENTHDLR* eventhdlr = NULL;
SCIP_CALL(SCIPincludeEventhdlrBasic(scip, &eventhdlr, "testevent", "test event handler",
eventExecTest, eventhdlrdata));
SCIP_CALL(SCIPsetEventhdlrInitsol(scip, eventhdlr, eventInitsolTest));
SCIP_CALL(SCIPsetEventhdlrExitsol(scip, eventhdlr, eventExitsolTest));
return SCIP_OKAY;
}
/* Create a simple MIP model:
* max x + 2y
* s.t. x + y <= 15
* 0 <= x <= 10 (integer)
* 0 <= y <= 10 (integer)
*
* Optimal: x=5, y=10, obj=25
*/
static SCIP_RETCODE createModel(SCIP* scip)
{
SCIP_VAR* x;
SCIP_VAR* y;
SCIP_CONS* cons;
SCIP_CALL(SCIPcreateProbBasic(scip, "test_presol_events"));
SCIP_CALL(SCIPsetObjsense(scip, SCIP_OBJSENSE_MAXIMIZE));
/* Create variables */
SCIP_CALL(SCIPcreateVarBasic(scip, &x, "x", 0.0, 10.0, 1.0, SCIP_VARTYPE_INTEGER));
SCIP_CALL(SCIPcreateVarBasic(scip, &y, "y", 0.0, 10.0, 2.0, SCIP_VARTYPE_INTEGER));
SCIP_CALL(SCIPaddVar(scip, x));
SCIP_CALL(SCIPaddVar(scip, y));
/* Create constraint: x + y <= 15 */
SCIP_CALL(SCIPcreateConsBasicLinear(scip, &cons, "c1", 0, NULL, NULL, -SCIPinfinity(scip), 15.0));
SCIP_CALL(SCIPaddCoefLinear(scip, cons, x, 1.0));
SCIP_CALL(SCIPaddCoefLinear(scip, cons, y, 1.0));
SCIP_CALL(SCIPaddCons(scip, cons));
/* Release */
SCIP_CALL(SCIPreleaseCons(scip, &cons));
SCIP_CALL(SCIPreleaseVar(scip, &y));
SCIP_CALL(SCIPreleaseVar(scip, &x));
return SCIP_OKAY;
}
int main(int argc, char** argv)
{
SCIP* scip = NULL;
SCIP_EVENTHDLRDATA eventhdlrdata = {0};
int disablePresol = 0;
if (argc > 1 && strcmp(argv[1], "--no-presol") == 0) {
disablePresol = 1;
}
printf("=== Testing SCIP events during presolving ===\n");
printf("Presolving: %s\n\n", disablePresol ? "DISABLED" : "ENABLED");
/* Initialize SCIP */
SCIP_CALL(SCIPcreate(&scip));
SCIP_CALL(SCIPincludeDefaultPlugins(scip));
/* Include our event handler */
SCIP_CALL(includeEventHandler(scip, &eventhdlrdata));
/* Create model */
SCIP_CALL(createModel(scip));
/* Optionally disable presolving */
if (disablePresol) {
SCIP_CALL(SCIPsetPresolving(scip, SCIP_PARAMSETTING_OFF, TRUE));
}
/* Solve */
printf("--- Starting solve ---\n");
SCIP_CALL(SCIPsolve(scip));
printf("--- Solve finished ---\n\n");
/* Print results */
printf("=== Results ===\n");
printf("Status: %d\n", SCIPgetStatus(scip));
printf("Final primalbound: %.2f\n", SCIPgetPrimalbound(scip));
printf("Final dualbound: %.2f\n", SCIPgetDualbound(scip));
printf("Gap: %.2f%%\n", 100.0 * SCIPgetGap(scip));
printf("\n");
printf("BESTSOLFOUND events caught: %d\n", eventhdlrdata.bestSolCount);
/* Check if final solution was captured */
if (SCIPgetPrimalbound(scip) == 25.0 && eventhdlrdata.bestSolCount == 0) {
printf("\n*** BUG: Optimal solution found but no BESTSOLFOUND event was triggered! ***\n");
}
/* Free SCIP */
SCIP_CALL(SCIPfree(&scip));
return 0;
} |
|
So @MySweetEden , the conclusion is that there is no problem with doing it as a recipe, it's the way to go :) |
|
Thank you both @Joao-Dionisio and @DominikKamp for the detailed feedback and for taking the time to review and improve the code! I agree with the recipe approach. To keep the PR clean:
This way, the PR will contain only the recipe file without any core file modifications. |
f421f79 to
5f22e2e
Compare
|
Hi! I've updated the PR to implement as a recipe following your suggestion. Changes:
Note: The existing primal_dual_evolution recipe has the same stubtest issue with GapEventhdlr (currently in stubs/todo). If you'd like, I can create a follow-up PR to rename it to _GapEventhdlr and remove the todo entry, for consistency. |
|
Thank you, @MySweetEden ! I think it's best to keep the recipe public and just update the stubs, but this is something I can do later, so we can merge now :) |
Summary
Adds
setTracefile()method for structured, machine-readable optimization progress logging.Closes #1147
Motivation
As discussed in #1147, structured progress logging is useful for:
This provides a simpler alternative to implementing custom event handlers.
Design Decisions
primalbound,dualbound,time,nodesfor consistencyChanges
setTracefile(path, mode="a")method to Model_write_trace_event()for centralized trace writingBESTSOLFOUNDeventsEvents Recorded
solution_update: when a new best solution is foundsolve_finish: when optimization terminatesFields
type,time,primalbound,dualbound,gap,nodes,nsolUsage
trace.jsonl contains JSONL records
Future Work
Open Questions
solve_startevent be added to distinguish multiple optimize() calls in append mode?