-
Notifications
You must be signed in to change notification settings - Fork 275
Add setTracefile() method for structured optimization progress loggingAdd settracefile api #1158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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?