Skip to content

Commit c5de089

Browse files
Merge branch 'master' into feature-gil
2 parents 0d5960a + c24e9df commit c5de089

File tree

11 files changed

+334
-18
lines changed

11 files changed

+334
-18
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44
### Added
5+
- Added stage checks to presolve, freereoptsolve, freetransform
6+
- Added primal_dual_evolution recipe and a plot recipe
57
### Fixed
68
### Changed
79
### Removed

examples/finished/__init__.py

Whitespace-only changes.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""
2+
This example show how to retrieve the primal and dual solutions during the optimization process
3+
and plot them as a function of time. The model is about gas transportation and can be found in
4+
PySCIPOpt/tests/helpers/utils.py
5+
6+
It makes use of the attach_primal_dual_evolution_eventhdlr recipe.
7+
8+
Requires matplotlib, and may require PyQt6 to show the plot.
9+
"""
10+
11+
from pyscipopt import Model
12+
13+
def plot_primal_dual_evolution(model: Model):
14+
try:
15+
from matplotlib import pyplot as plt
16+
except ImportError:
17+
raise ImportError("matplotlib is required to plot the solution. Try running `pip install matplotlib` in the command line.\
18+
You may also need to install PyQt6 to show the plot.")
19+
20+
assert model.data["primal_log"], "Could not find any feasible solutions"
21+
time_primal, val_primal = map(list,zip(*model.data["primal_log"]))
22+
time_dual, val_dual = map(list,zip(*model.data["dual_log"]))
23+
24+
25+
if time_primal[-1] < time_dual[-1]:
26+
time_primal.append(time_dual[-1])
27+
val_primal.append(val_primal[-1])
28+
29+
if time_primal[-1] > time_dual[-1]:
30+
time_dual.append(time_primal[-1])
31+
val_dual.append(val_dual[-1])
32+
33+
plt.plot(time_primal, val_primal, label="Primal bound")
34+
plt.plot(time_dual, val_dual, label="Dual bound")
35+
36+
plt.legend(loc="best")
37+
plt.show()
38+
39+
if __name__=="__main__":
40+
from pyscipopt.recipes.primal_dual_evolution import attach_primal_dual_evolution_eventhdlr
41+
import os
42+
import sys
43+
44+
# just a way to import files from different folders, not important
45+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../tests/helpers')))
46+
47+
from utils import gastrans_model
48+
49+
model = gastrans_model()
50+
model.data = {}
51+
attach_primal_dual_evolution_eventhdlr(model)
52+
53+
model.optimize()
54+
plot_primal_dual_evolution(model)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from pyscipopt import Model, Eventhdlr, SCIP_EVENTTYPE, Eventhdlr
2+
3+
def attach_primal_dual_evolution_eventhdlr(model: Model):
4+
"""
5+
Attaches an event handler to a given SCIP model that collects primal and dual solutions,
6+
along with the solving time when they were found.
7+
The data is saved in model.data["primal_log"] and model.data["dual_log"]. They consist of
8+
a list of tuples, each tuple containing the solving time and the corresponding solution.
9+
10+
A usage example can be found in examples/finished/plot_primal_dual_evolution.py. The
11+
example takes the information provided by this recipe and uses it to plot the evolution
12+
of the dual and primal bounds over time.
13+
"""
14+
class GapEventhdlr(Eventhdlr):
15+
16+
def eventinit(self): # we want to collect best primal solutions and best dual solutions
17+
self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self)
18+
self.model.catchEvent(SCIP_EVENTTYPE.LPSOLVED, self)
19+
self.model.catchEvent(SCIP_EVENTTYPE.NODESOLVED, self)
20+
21+
22+
def eventexec(self, event):
23+
# if a new best primal solution was found, we save when it was found and also its objective
24+
if event.getType() == SCIP_EVENTTYPE.BESTSOLFOUND:
25+
self.model.data["primal_log"].append([self.model.getSolvingTime(), self.model.getPrimalbound()])
26+
27+
if not self.model.data["dual_log"]:
28+
self.model.data["dual_log"].append([self.model.getSolvingTime(), self.model.getDualbound()])
29+
30+
if self.model.getObjectiveSense() == "minimize":
31+
if self.model.isGT(self.model.getDualbound(), self.model.data["dual_log"][-1][1]):
32+
self.model.data["dual_log"].append([self.model.getSolvingTime(), self.model.getDualbound()])
33+
else:
34+
if self.model.isLT(self.model.getDualbound(), self.model.data["dual_log"][-1][1]):
35+
self.model.data["dual_log"].append([self.model.getSolvingTime(), self.model.getDualbound()])
36+
37+
38+
if not hasattr(model, "data") or model.data==None:
39+
model.data = {}
40+
41+
model.data["primal_log"] = []
42+
model.data["dual_log"] = []
43+
hdlr = GapEventhdlr()
44+
model.includeEventhdlr(hdlr, "gapEventHandler", "Event handler which collects primal and dual solution evolution")
45+
46+
return model

src/pyscipopt/scip.pxi

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2101,6 +2101,15 @@ cdef class Model:
21012101
def freeTransform(self):
21022102
"""Frees all solution process data including presolving and
21032103
transformed problem, only original problem is kept."""
2104+
if self.getStage() not in [SCIP_STAGE_INIT,
2105+
SCIP_STAGE_PROBLEM,
2106+
SCIP_STAGE_TRANSFORMED,
2107+
SCIP_STAGE_PRESOLVING,
2108+
SCIP_STAGE_PRESOLVED,
2109+
SCIP_STAGE_SOLVING,
2110+
SCIP_STAGE_SOLVED]:
2111+
raise Warning("method cannot be called in stage %i." % self.getStage())
2112+
21042113
self._modelvars = {
21052114
var: value
21062115
for var, value in self._modelvars.items()
@@ -6227,6 +6236,11 @@ cdef class Model:
62276236

62286237
def presolve(self):
62296238
"""Presolve the problem."""
6239+
if self.getStage() not in [SCIP_STAGE_PROBLEM, SCIP_STAGE_TRANSFORMED,\
6240+
SCIP_STAGE_PRESOLVING, SCIP_STAGE_PRESOLVED, \
6241+
SCIP_STAGE_SOLVED]:
6242+
raise Warning("method cannot be called in stage %i." % self.getStage())
6243+
62306244
PY_SCIP_CALL(SCIPpresolve(self._scip))
62316245
self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip))
62326246

@@ -9029,6 +9043,15 @@ cdef class Model:
90299043

90309044
def freeReoptSolve(self):
90319045
"""Frees all solution process data and prepares for reoptimization."""
9046+
9047+
if self.getStage() not in [SCIP_STAGE_INIT,
9048+
SCIP_STAGE_PROBLEM,
9049+
SCIP_STAGE_TRANSFORMED,
9050+
SCIP_STAGE_PRESOLVING,
9051+
SCIP_STAGE_PRESOLVED,
9052+
SCIP_STAGE_SOLVING,
9053+
SCIP_STAGE_SOLVED]:
9054+
raise Warning("method cannot be called in stage %i." % self.getStage())
90329055
PY_SCIP_CALL(SCIPfreeReoptSolve(self._scip))
90339056

90349057
def chgReoptObjective(self, coeffs, sense = 'minimize'):

0 commit comments

Comments
 (0)