Skip to content
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

feat: Improve structure of data provided to event callbacks, extract more stats and provide type hints for callback data #57

Merged
merged 5 commits into from
Mar 29, 2024
Merged
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
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: default install-dev tests clean
.PHONY: default install-dev tests clean build docs

default:
pip install .
Expand All @@ -16,4 +16,7 @@ clean:
rm -rf ilpy/*.so

build:
python setup.py build_ext --inplace
python setup.py build_ext --inplace

docs:
cd docs && make html
7 changes: 7 additions & 0 deletions ilpy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from typing import TYPE_CHECKING

from . import wrapper
from ._functional import solve
from .expressions import Expression, Variable
from .wrapper import * # noqa: F403

if TYPE_CHECKING:
from .event_data import EventData as EventData
from .event_data import GurobiData as GurobiData
from .event_data import SCIPData as SCIPData

__version__ = "0.3.1"
__all__ = [ # noqa: F405
"Any",
Expand Down
29 changes: 26 additions & 3 deletions ilpy/_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def solve(
) -> list[float]:
"""Solve an objective subject to constraints.

This is a functional interface to the solver. It creates a solver instance
and sets the objective and constraints, then solves the problem and returns
the solution.

Parameters
----------
objective : Sequence[float] | Expression | Objective
Expand Down Expand Up @@ -58,9 +62,28 @@ def solve(
Backend preference, either an `ilpy.Preference` or a string in
{"any", "cplex", "gurobi", "scip"}. By default, `Preference.Any`.
on_event : Callable[[Mapping], None], optional
A callback function that is called when an event occurs, by default None.
The callback function should accept a dict which will contain event
metadata.
A callback function that is called when an event occurs, by default None. The
callback function should accept a dict which will contain statics about the
solving or presolving process. You can import `ilpy.EventData` from ilpy and use
it to provide dict key hints in your IDE, but `EventData` is not available at
runtime.
See SCIP and Gurobi documentation for details what each value means.

For example::

import ilpy

if TYPE_CHECKING:
from ilpy import EventData

def callback(data: EventData) -> None:
# backend and event_type are guaranteed to be present
# they will narrow down the available keys
if data["backend"] == "gurobi":
if data["event_type"] == "MIP":
print(data["gap"])

ilpy.solve(..., on_event=callback)

Returns
-------
Expand Down
99 changes: 99 additions & 0 deletions ilpy/event_data.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from typing import Literal, TypedDict

__all__ = ["EventData", "GurobiData", "SCIPData"]

class _GurobiData(TypedDict, total=False):
backend: Literal["gurobi"]
runtime: float # Elapsed solver runtime (seconds).
work: float # Elapsed solver work (work units).

class GurobiPresolve(_GurobiData):
event_type: Literal["PRESOLVE"]
pre_coldel: int
pre_rowdel: int
pre_senchg: int
pre_bndchg: int
pre_coechg: int

class GurobiSimplex(_GurobiData):
event_type: Literal["SIMPLEX"]
itrcnt: float
objval: float
priminf: float
dualinf: float
ispert: int

class _GurobiMipData(_GurobiData):
objbst: float
objbnd: float
nodcnt: float
solcnt: int
openscenarios: int
phase: int
primalbound: float # alias for objbst
dualbound: float # alias for objbnd
gap: float # calculated manually from objbst and objbnd

class GurobiMip(_GurobiMipData):
event_type: Literal["MIP"]
cutcnt: int
nodlft: float
itrcnt: float

class GurobiMipSol(_GurobiMipData):
event_type: Literal["MIPSOL"]
obj: float

class GurobiMipNode(_GurobiMipData):
event_type: Literal["MIPNODE"]
status: int

class GurobiMessage(_GurobiData):
event_type: Literal["MESSAGE"]
message: str

GurobiData = (
GurobiPresolve
| GurobiSimplex
| GurobiMip
| GurobiMipSol
| GurobiMipNode
| GurobiMessage
)

class _SCIPData(TypedDict, total=False):
backend: Literal["scip"]
deterministictime: float

class SCIPPresolve(_SCIPData):
event_type: Literal["PRESOLVEROUND"]
nativeconss: int
nbinvars: int
nintvars: int
nimplvars: int
nenabledconss: int
upperbound: float
nactiveconss: int
cutoffbound: float
nfixedvars: int

class SCIPBestSol(_SCIPData):
event_type: Literal["BESTSOLFOUND"]
avgdualbound: float
avglowerbound: float
dualbound: float
gap: float
lowerbound: float
nactiveconss: int
nbestsolsfound: int
nenabledconss: int
nlimsolsfound: int
nsolsfound: int
primalbound: float
transgap: float
nlps: int
nnzs: int

SCIPData = SCIPPresolve | SCIPBestSol

EventData = GurobiData | SCIPData
26 changes: 1 addition & 25 deletions ilpy/impl/solvers/GurobiBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <stdexcept>

#include "GurobiBackend.h"
#include "GurobiEventHandler.h"

#define GRB_CHECK(call) \
grbCheck(#call, __FILE__, __LINE__, call)
Expand Down Expand Up @@ -212,31 +213,6 @@ GurobiBackend::addConstraint(const Constraint& constraint) {
delete[] qvals;
}

// Static member function
int __stdcall GurobiBackend::eventCallback(CB_ARGS) {
// Cast usrdata back to a GurobiBackend pointer
GurobiBackend* backend = static_cast<GurobiBackend*>(usrdata);

// Only retrieve the primal and dual objective values in the GRB_CB_MIP and GRB_CB_MIPSOL states
if (where == GRB_CB_MIP) {
double bestbd, incumbent, gap;

GRBcbget(cbdata, where, GRB_CB_MIP_OBJBST, &incumbent);
GRBcbget(cbdata, where, GRB_CB_MIP_OBJBND, &bestbd);

// calculate the gap
gap = 100 * (fabs(bestbd - incumbent) / (std::numeric_limits<double>::epsilon() + fabs(incumbent)));

backend->emitEventData({
{"event_type", static_cast<double>(where)},
{"dualbound", bestbd},
{"primalbound", incumbent},
{"gap", gap}
});
}

return 0;
}

bool
GurobiBackend::solve(Solution& x, std::string& msg) {
Expand Down
2 changes: 0 additions & 2 deletions ilpy/impl/solvers/GurobiBackend.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ class GurobiBackend : public SolverBackend {

void setNumThreads(unsigned int numThreads);

static int __stdcall eventCallback(CB_ARGS);

bool solve(Solution& solution, std::string& message);

std::string solve(Solution& solution) {
Expand Down
169 changes: 169 additions & 0 deletions ilpy/impl/solvers/GurobiEventHandler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@

#include <gurobi_c.h>

#include "GurobiBackend.h"

const char* getEventTypeName(int where) {

Check warning on line 6 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L6

Added line #L6 was not covered by tests
switch (where) {
case GRB_CB_POLLING: return "POLLING";
case GRB_CB_PRESOLVE: return "PRESOLVE";
case GRB_CB_SIMPLEX: return "SIMPLEX";
case GRB_CB_MIP: return "MIP";
case GRB_CB_MIPSOL: return "MIPSOL";
case GRB_CB_MIPNODE: return "MIPNODE";
case GRB_CB_MESSAGE: return "MESSAGE";
case GRB_CB_BARRIER: return "BARRIER";
case GRB_CB_MULTIOBJ: return "MULTIOBJ";
case GRB_CB_IIS: return "IIS";

Check warning on line 17 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L8-L17

Added lines #L8 - L17 were not covered by tests
// Add other cases here
default: return "UNKNOWN";

Check warning on line 19 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L19

Added line #L19 was not covered by tests
}
}


/**
* Callback function for Gurobi events.
* Pulls out data as described in the Gurobi documentation.
* https://www.gurobi.com/documentation/current/refman/cb_codes.html
*/
int __stdcall eventCallback(CB_ARGS) {

Check warning on line 29 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L29

Added line #L29 was not covered by tests
if (where == GRB_CB_POLLING) {
// POLLING callback is an optional callback that is only invoked
// if other callbacks have not been called in a while.
// It does not allow any progress information to be retrieved.
// It is simply provided to allow interactive applications to
// regain control frequently, so that they can maintain application responsiveness.
return 0;

Check warning on line 36 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L36

Added line #L36 was not covered by tests
}

// Cast usrdata back to a GurobiBackend pointer
GurobiBackend* backend = static_cast<GurobiBackend*>(usrdata);

Check warning on line 40 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L40

Added line #L40 was not covered by tests
// don't bother collecting the data if no one is listening
if (!backend->hasEventCallback()) {
return 0;

Check warning on line 43 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L43

Added line #L43 was not covered by tests
}

// Create a map to store the event data
EventDataMap map;

Check warning on line 47 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L47

Added line #L47 was not covered by tests

// all events will have these fields
double runtime, work;

Check warning on line 50 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L50

Added line #L50 was not covered by tests
const char* event_name = getEventTypeName(where);
GRBcbget(cbdata, where, GRB_CB_RUNTIME, &runtime);
GRBcbget(cbdata, where, GRB_CB_WORK, &work);
map["event_type"] = event_name;
map["backend"] = "gurobi";
map["runtime"] = runtime;
map["work"] = work;

if (where == GRB_CB_PRESOLVE) {
// Currently performing presolve
int pre_coldel, pre_rowdel, pre_senchg, pre_bndchg, pre_coechg;

Check warning on line 61 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L61

Added line #L61 was not covered by tests
GRBcbget(cbdata, where, GRB_CB_PRE_COLDEL, &pre_coldel);
GRBcbget(cbdata, where, GRB_CB_PRE_ROWDEL, &pre_rowdel);
GRBcbget(cbdata, where, GRB_CB_PRE_SENCHG, &pre_senchg);
GRBcbget(cbdata, where, GRB_CB_PRE_BNDCHG, &pre_bndchg);
GRBcbget(cbdata, where, GRB_CB_PRE_COECHG, &pre_coechg);
map["pre_coldel"] = pre_coldel;
map["pre_rowdel"] = pre_rowdel;
map["pre_senchg"] = pre_senchg;
map["pre_bndchg"] = pre_bndchg;
map["pre_coechg"] = pre_coechg;
} else if (where == GRB_CB_SIMPLEX) {
// Currently in simplex
double objval, priminf, dualinf, itrcnt;
int ispert;

Check warning on line 75 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L74-L75

Added lines #L74 - L75 were not covered by tests
GRBcbget(cbdata, where, GRB_CB_SPX_ITRCNT, &itrcnt);
GRBcbget(cbdata, where, GRB_CB_SPX_OBJVAL, &objval);
GRBcbget(cbdata, where, GRB_CB_SPX_PRIMINF, &priminf);
GRBcbget(cbdata, where, GRB_CB_SPX_DUALINF, &dualinf);
GRBcbget(cbdata, where, GRB_CB_SPX_ISPERT, &ispert);
map["itrcnt"] = itrcnt;
map["objval"] = objval;
map["priminf"] = priminf;
map["dualinf"] = dualinf;
map["ispert"] = ispert;
} else if (where == GRB_CB_MIP) {
// Currently in MIP
double objbst, objbnd, nodcnt, solcnt, cutcnt, nodlft, itrcnt;
int openscenarios, phase;

Check warning on line 89 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L88-L89

Added lines #L88 - L89 were not covered by tests
GRBcbget(cbdata, where, GRB_CB_MIP_OBJBST, &objbst);
GRBcbget(cbdata, where, GRB_CB_MIP_OBJBND, &objbnd);
GRBcbget(cbdata, where, GRB_CB_MIP_NODCNT, &nodcnt);
GRBcbget(cbdata, where, GRB_CB_MIP_SOLCNT, &solcnt);
GRBcbget(cbdata, where, GRB_CB_MIP_CUTCNT, &cutcnt);
GRBcbget(cbdata, where, GRB_CB_MIP_NODLFT, &nodlft);
GRBcbget(cbdata, where, GRB_CB_MIP_ITRCNT, &itrcnt);
GRBcbget(cbdata, where, GRB_CB_MIP_OPENSCENARIOS, &openscenarios);
GRBcbget(cbdata, where, GRB_CB_MIP_PHASE, &phase);
map["objbst"] = objbst;
map["objbnd"] = objbnd;
map["nodcnt"] = nodcnt;
map["solcnt"] = solcnt;
map["cutcnt"] = cutcnt;
map["nodlft"] = nodlft;
map["itrcnt"] = itrcnt;
map["openscenarios"] = openscenarios;
map["phase"] = phase;
// special keys to match similar ones in SCIP.
map["primalbound"] = objbst;
map["dualbound"] = objbnd;
map["gap"] = 100 * (fabs(objbnd - objbst) /
(std::numeric_limits<double>::epsilon() + fabs(objbst)));
} else if (where == GRB_CB_MIPSOL) {
// Found a new MIP incumbent
double obj, objbst, objbnd;
int nodcnt, solcnt, openscenarios, phase;

Check warning on line 116 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L115-L116

Added lines #L115 - L116 were not covered by tests
// GRBcbget(cbdata, where, GRB_CB_MIPSOL_SOL, &obj); ... this is a vector
GRBcbget(cbdata, where, GRB_CB_MIPSOL_OBJ, &obj);
GRBcbget(cbdata, where, GRB_CB_MIPSOL_OBJBST, &objbst);
GRBcbget(cbdata, where, GRB_CB_MIPSOL_OBJBND, &objbnd);
GRBcbget(cbdata, where, GRB_CB_MIPSOL_NODCNT, &nodcnt);
GRBcbget(cbdata, where, GRB_CB_MIPSOL_SOLCNT, &solcnt);
GRBcbget(cbdata, where, GRB_CB_MIPSOL_OPENSCENARIOS, &openscenarios);
GRBcbget(cbdata, where, GRB_CB_MIPSOL_PHASE, &phase);
map["obj"] = obj;
map["objbst"] = objbst;
map["objbnd"] = objbnd;
map["nodcnt"] = nodcnt;
map["solcnt"] = solcnt;
map["openscenarios"] = openscenarios;
map["phase"] = phase;
// special keys to match similar ones in SCIP.
map["primalbound"] = objbst;
map["dualbound"] = objbnd;
map["gap"] = 100 * (fabs(objbnd - objbst) /
(std::numeric_limits<double>::epsilon() + fabs(objbst)));
} else if (where == GRB_CB_MIPNODE) {
// Currently exploring a MIP node
double objbst, objbnd, nodcnt;
int solcnt, openscenarios, phase, status;

Check warning on line 140 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L139-L140

Added lines #L139 - L140 were not covered by tests
GRBcbget(cbdata, where, GRB_CB_MIPNODE_STATUS, &status);
GRBcbget(cbdata, where, GRB_CB_MIPNODE_OBJBST, &objbst);
GRBcbget(cbdata, where, GRB_CB_MIPNODE_OBJBND, &objbnd);
GRBcbget(cbdata, where, GRB_CB_MIPNODE_NODCNT, &nodcnt);
GRBcbget(cbdata, where, GRB_CB_MIPNODE_SOLCNT, &solcnt);
GRBcbget(cbdata, where, GRB_CB_MIPNODE_OPENSCENARIOS, &openscenarios);
GRBcbget(cbdata, where, GRB_CB_MIPNODE_PHASE, &phase);
map["status"] = status;
map["objbst"] = objbst;
map["objbnd"] = objbnd;
map["nodcnt"] = nodcnt;
map["solcnt"] = solcnt;
map["openscenarios"] = openscenarios;
map["phase"] = phase;
// special keys to match similar ones in SCIP.
map["primalbound"] = objbst;
map["dualbound"] = objbnd;
map["gap"] = 100 * (fabs(objbnd - objbst) /
(std::numeric_limits<double>::epsilon() + fabs(objbst)));
} else if (where == GRB_CB_MESSAGE) {
// Printing a log message
char* msg;

Check warning on line 162 in ilpy/impl/solvers/GurobiEventHandler.h

View check run for this annotation

Codecov / codecov/patch

ilpy/impl/solvers/GurobiEventHandler.h#L162

Added line #L162 was not covered by tests
GRBcbget(cbdata, where, GRB_CB_MSG_STRING, &msg);
map["message"] = msg;
}

backend->emitEventData(map);
return 0;
}
Loading
Loading