Skip to content

Commit c7ecb2a

Browse files
authored
Merge pull request #916 from scipopt/plot-pd-evolution
Primal-dual evolution event handler recipe
2 parents 1db095e + ea9ab18 commit c7ecb2a

File tree

10 files changed

+310
-18
lines changed

10 files changed

+310
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44
### Added
5+
- Added primal_dual_evolution recipe and a plot recipe
56
### Fixed
67
### Changed
78
### 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

tests/data/readStatistics.stats

Lines changed: 172 additions & 0 deletions
Large diffs are not rendered by default.

tests/helpers/utils.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
from pyscipopt import Model, quicksum, SCIP_PARAMSETTING, exp, log, sqrt, sin
22
from typing import List
33

4-
from pyscipopt.scip import is_memory_freed
5-
6-
7-
def is_optimized_mode():
8-
model = Model()
9-
return is_memory_freed()
10-
11-
124
def random_mip_1(disable_sepa=True, disable_huer=True, disable_presolve=True, node_lim=2000, small=False):
135
model = Model()
146

@@ -77,19 +69,15 @@ def random_nlp_1():
7769
return model
7870

7971

80-
def knapsack_model(weights=[4, 2, 6, 3, 7, 5], costs=[7, 2, 5, 4, 3, 4]):
72+
def knapsack_model(weights=[4, 2, 6, 3, 7, 5], costs=[7, 2, 5, 4, 3, 4], knapsack_size = 15):
8173
# create solver instance
8274
s = Model("Knapsack")
83-
s.hideOutput()
8475

8576
# setting the objective sense to maximise
8677
s.setMaximize()
8778

8879
assert len(weights) == len(costs)
8980

90-
# knapsack size
91-
knapsackSize = 15
92-
9381
# adding the knapsack variables
9482
knapsackVars = []
9583
varNames = []

tests/test_heur.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import pytest
55

66
from pyscipopt import Model, Heur, SCIP_RESULT, SCIP_PARAMSETTING, SCIP_HEURTIMING, SCIP_LPSOLSTAT
7-
from pyscipopt.scip import is_memory_freed
7+
from test_memory import is_optimized_mode
88

9-
from helpers.utils import random_mip_1, is_optimized_mode
9+
from helpers.utils import random_mip_1
1010

1111
class MyHeur(Heur):
1212

tests/test_memory.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import pytest
22
from pyscipopt.scip import Model, is_memory_freed, print_memory_in_use
3-
from helpers.utils import is_optimized_mode
43

54
def test_not_freed():
65
if is_optimized_mode():
@@ -16,4 +15,8 @@ def test_freed():
1615
assert is_memory_freed()
1716

1817
def test_print_memory_in_use():
19-
print_memory_in_use()
18+
print_memory_in_use()
19+
20+
def is_optimized_mode():
21+
model = Model()
22+
return is_memory_freed()

tests/test_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,4 +476,4 @@ def test_getObjVal():
476476
assert m.getVal(x) == 0
477477

478478
assert m.getObjVal() == 16
479-
assert m.getVal(x) == 0
479+
assert m.getVal(x) == 0
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from pyscipopt.recipes.primal_dual_evolution import attach_primal_dual_evolution_eventhdlr
2+
from helpers.utils import bin_packing_model
3+
4+
def test_primal_dual_evolution():
5+
from random import randint
6+
7+
model = bin_packing_model(sizes=[randint(1,40) for _ in range(120)], capacity=50)
8+
model.setParam("limits/time",5)
9+
10+
model.data = {"test": True}
11+
model = attach_primal_dual_evolution_eventhdlr(model)
12+
13+
assert "test" in model.data
14+
assert "primal_log" in model.data
15+
16+
model.optimize()
17+
18+
for i in range(1, len(model.data["primal_log"])):
19+
if model.getObjectiveSense() == "minimize":
20+
assert model.data["primal_log"][i][1] <= model.data["primal_log"][i-1][1]
21+
else:
22+
assert model.data["primal_log"][i][1] >= model.data["primal_log"][i-1][1]
23+
24+
for i in range(1, len(model.data["dual_log"])):
25+
if model.getObjectiveSense() == "minimize":
26+
assert model.data["dual_log"][i][1] >= model.data["dual_log"][i-1][1]
27+
else:
28+
assert model.data["dual_log"][i][1] <= model.data["dual_log"][i-1][1]

0 commit comments

Comments
 (0)