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

Primal-dual evolution event handler recipe #916

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased
### Added
- Added primal_dual_evolution recipe and a plot recipe
- Expanded Statistics class to more problems.
- Created Statistics class
- Added parser to read .stats file
Expand Down
50 changes: 50 additions & 0 deletions src/pyscipopt/recipes/primal_dual_evolution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from pyscipopt import Model, Eventhdlr, SCIP_EVENTTYPE, Eventhdlr

Check warning on line 1 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L1

Added line #L1 was not covered by tests

def get_primal_dual_evolution(model: Model):

Check warning on line 3 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L3

Added line #L3 was not covered by tests

class gapEventhdlr(Eventhdlr):

Check warning on line 5 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L5

Added line #L5 was not covered by tests

def eventinit(self): # we want to collect best primal solutions and best dual solutions
self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self)
self.model.catchEvent(SCIP_EVENTTYPE.LPSOLVED, self)
self.model.catchEvent(SCIP_EVENTTYPE.NODESOLVED, self)

Check warning on line 10 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L7-L10

Added lines #L7 - L10 were not covered by tests


def eventexec(self, event):

Check warning on line 13 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L13

Added line #L13 was not covered by tests
# if a new best primal solution was found, we save when it was found and also its objective
if event.getType() == SCIP_EVENTTYPE.BESTSOLFOUND:
self.model.data["primal_solutions"].append((self.model.getSolvingTime(), self.model.getPrimalbound()))

Check warning on line 16 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L15-L16

Added lines #L15 - L16 were not covered by tests

if not self.model.data["dual_solutions"]:
self.model.data["dual_solutions"].append((self.model.getSolvingTime(), self.model.getDualbound()))

Check warning on line 19 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L18-L19

Added lines #L18 - L19 were not covered by tests

if self.model.getObjectiveSense() == "minimize":
if self.model.isGT(self.model.getDualbound(), self.model.data["dual_solutions"][-1][1]):
self.model.data["dual_solutions"].append((self.model.getSolvingTime(), self.model.getDualbound()))

Check warning on line 23 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L21-L23

Added lines #L21 - L23 were not covered by tests
else:
if self.model.isLT(self.model.getDualbound(), self.model.data["dual_solutions"][-1][1]):
self.model.data["dual_solutions"].append((self.model.getSolvingTime(), self.model.getDualbound()))

Check warning on line 26 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L25-L26

Added lines #L25 - L26 were not covered by tests

if not hasattr(model, "data"):
model.data = {}

Check warning on line 29 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L28-L29

Added lines #L28 - L29 were not covered by tests

model.data["primal_solutions"] = []
model.data["dual_solutions"] = []
hdlr = gapEventhdlr()
model.includeEventhdlr(hdlr, "gapEventHandler", "Event handler which collects primal and dual solution evolution")

Check warning on line 34 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L31-L34

Added lines #L31 - L34 were not covered by tests

return model

Check warning on line 36 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L36

Added line #L36 was not covered by tests

def plot_primal_dual_evolution(model: Model):
try:
from matplotlib import pyplot as plt
except ImportError:
raise("matplotlib is required to plot the solution. Try running `pip install matplotlib` in the command line.")

Check warning on line 42 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L38-L42

Added lines #L38 - L42 were not covered by tests

time_primal, val_primal = zip(*model.data["primal_solutions"])
plt.plot(time_primal, val_primal, label="Primal bound")
time_dual, val_dual = zip(*model.data["dual_solutions"])
plt.plot(time_dual, val_dual, label="Dual bound")

Check warning on line 47 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L44-L47

Added lines #L44 - L47 were not covered by tests

plt.legend(loc="best")
plt.show()

Check warning on line 50 in src/pyscipopt/recipes/primal_dual_evolution.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/primal_dual_evolution.py#L49-L50

Added lines #L49 - L50 were not covered by tests
6 changes: 1 addition & 5 deletions tests/helpers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,15 @@ def random_nlp_1():
return model


def knapsack_model(weights=[4, 2, 6, 3, 7, 5], costs=[7, 2, 5, 4, 3, 4]):
def knapsack_model(weights=[4, 2, 6, 3, 7, 5], costs=[7, 2, 5, 4, 3, 4], knapsackSize = 15):
# create solver instance
s = Model("Knapsack")
s.hideOutput()

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

assert len(weights) == len(costs)

# knapsack size
knapsackSize = 15

# adding the knapsack variables
knapsackVars = []
varNames = []
Expand Down
2 changes: 1 addition & 1 deletion tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,4 +476,4 @@ def test_getObjVal():
assert m.getVal(x) == 0

assert m.getObjVal() == 16
assert m.getVal(x) == 0
assert m.getVal(x) == 0
36 changes: 36 additions & 0 deletions tests/test_recipe_primal_dual_evolution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from pyscipopt.recipes.primal_dual_evolution import get_primal_dual_evolution
from helpers.utils import bin_packing_model

def test_primal_dual_evolution():
from random import randint

model = bin_packing_model(sizes=[randint(1,40) for _ in range(120)], capacity=50)
model.setParam("limits/time",5)

model.data = {"test": True}
model = get_primal_dual_evolution(model)

assert "test" in model.data
assert "primal_solutions" in model.data

model.optimize()

# these are required because the event handler doesn't capture the final state
model.data["primal_solutions"].append((model.getSolvingTime(), model.getPrimalbound()))
model.data["dual_solutions"].append((model.getSolvingTime(), model.getDualbound()))

for i in range(1, len(model.data["primal_solutions"])):
if model.getObjectiveSense() == "minimize":
assert model.data["primal_solutions"][i][1] <= model.data["primal_solutions"][i-1][1]
else:
assert model.data["primal_solutions"][i][1] >= model.data["primal_solutions"][i-1][1]

for i in range(1, len(model.data["dual_solutions"])):
if model.getObjectiveSense() == "minimize":
assert model.data["dual_solutions"][i][1] >= model.data["dual_solutions"][i-1][1]
else:
assert model.data["dual_solutions"][i][1] <= model.data["dual_solutions"][i-1][1]

# how to get a simple plot of the data
#from pyscipopt.recipes.primal_dual_evolution import plot_primal_dual_evolution
#plot_primal_dual_evolution(model)
Loading