How to implement an optimality gap plot?
AnsweredHi, I came across this plot earlier and was wondering what exactly it says and how to implement it with Gurobi?
I think with a callback function (as seen here), but I don't know which values I have to store exactly. I would like to build it into such a model framework.
import gurobipy as gp
from gurobipy import GRB
class Problem:
def __init__(self):
self.model = gp.Model("mip1")
self.val_c1 = 4
self.val_c2 = 1
def genVars(self):
self.x = self.model.addVar(vtype=GRB.BINARY, name="x")
self.y = self.model.addVar(vtype=GRB.BINARY, name="y")
self.z = self.model.addVar(vtype=GRB.BINARY, name="z")
def genCons(self):
self.model.addConstr(self.x + 2 * self.y + 3 * self.z <= self.val_c1)
self.model.addConstr(self.x + self.y >= self.val_c2)
def genObjective(self):
self.model.setObjective(self.x + self.y + 2 * self.z, GRB.MAXIMIZE)
def solve(self):
self.model.update()
self.model.optimize(self.callback)
def callback(self):
....
def build(self):
self.genVars()
self.genCons()
self.genObjective()
self.solve()
p = Problem()
p.build()
How would the correct code look like?
-
Hi Lorenz,
If you take a look at callback.py there is code under
where == GRB.Callback.MIP:
in which best bound and best objective are retrieved. You can also retrieve the current run time via GRB.Callback.RUNTIME.
In saying this, this is not best practice. You can use gurobi-logtools to parse data from your logfiles and produce plots like these.
If you clone the gurobi-logtools repo you can find both example notebook and example data (which the notebook uses).
- Riley
0 -
Hi Riley Clement. Thank you for your answer. I tried using gurobi-logtools, but i always get this error:
Traceback (most recent call last):
File "G:\M..\Model.py", line 502, in <module>
summary = glt.parse("/log.log").summary()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\....\AppData\Local\Programs\Python\Python312\Lib\site-packages\gurobi_logtools\api.py", line 113, in summary
fill_default_parameters_nosuffix(parameters.join(summary["Version"]))
~~~~~~~^^^^^^^^^^^
File "C:\Users\....\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py", line 4061, in __getitem__
indexer = self.columns.get_loc(key)
^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\...\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\range.py", line 417, in get_loc
raise KeyError(key)
KeyError: 'Version'0 -
Hi Lorenz,
It's a bit of a misleading error message.
It usually indicates that the path to the log is not valid. I'd try
summary = glt.parse("./log.log").summary()
or provide the full path to the log file.
- Riley
0 -
Hi Riley Clement, thanks. Did the trick. I have now used your MIP example and tried to create the plot for it (even though you probably won't see anything there). I have followed this video. I ended up with this code.
from gurobipy import *
import plotly.graph_objects as go
import gurobi_logtools as glt
import pandas as pd
# Create a new model
m = Model("mip1")
# Create variables
x = m.addVar(vtype=GRB.BINARY, name="x")
y = m.addVar(vtype=GRB.BINARY, name="y")
z = m.addVar(vtype=GRB.BINARY, name="z")
# Set objective
m.setObjective(x + y + 2 * z, GRB.MAXIMIZE)
m.addConstr(x + 2 * y + 3 * z <= 4, "c0")
m.addConstr(x + y >= 1, "c1")
# Optimize model
m.Params.LogFile = "test_log.log"
m.update()
m.optimize()
pd.set_option('display.max_columns', None)
summary = glt.parse("./test.log").summary()
results, timeline, rootlp = glt.get_dataframe(["./test.log"], timelines=True)
# Plot
default_run = timeline
fig = go.Figure
fig.add_trace(go.Scatter(x=default_run["Time"], y=default_run["Incumbent"], name="Primal Bound"))
fig.add_trace(go.Scatter(x=default_run["Time"], y=default_run["BestBd"], name="Dual Bound"))
fig.add_trace(go.Scatter(x=default_run["Time"], y=default_run["Gap"], name="Gap"))
fig.update_xaxes(title="Runtime")
fig.update_yaxes(title="Obj Val")
fig.show()Unfortunately, I always get this error.
ValueError: not enough values to unpack (expected 3, got 2)
If I remove , rootlp, I can see the timeline dataframe, but then the DataFrame looks very different from the one in the video. Something like Current Node, BestNode or Gap are all missing. What could be the reason for this?
0 -
Hi Lorenz,
An error in isolation is usually not enough to understand what the problem is. If there is a stacktrace and indication of the line where the error occurs then please include it.
I'm guessing the error is produced by
results, timeline, rootlp = glt.get_dataframe(["./test.log"], timelines=True)
and removing rootlp is indeed the fix.
The video is probably a couple of years out of date now. The timeline variable in your code will be assigned a dictionary. You can see this by executing type(timeline) in an interactive Python session. The keys of the dict will be 'norel', 'rootlp', 'nodelog', and correspond to 3 different dataframes. Each dataframe corresponds to timeline data from different phases of the solve.
For your case, you will want to use "nodelog", so in your code I suspect you can use
default_run = timeline["nodelog"]
to get what you are after. You can use
timelines["nodelog"].keys()
in an interactive Python session to see the columns available in this dataframe, which should give the following result:
Index(['CurrentNode', 'RemainingNodes', 'Obj', 'Depth', 'IntInf', 'Incumbent',
'BestBd', 'Gap', 'ItPerNode', 'Time', 'NewSolution', 'Pruned',
'LogFilePath', 'LogNumber', 'ModelFilePath', 'Seed', 'Version',
'ModelFile', 'Model', 'Log'],
dtype='object')- Riley
0 -
Hi Riley Clement , thank you. It works now. The plot can be output. But unfortunately, the scales are wrong. In the initial plot above, the values of the objective function are shown on the left, but the percentages are shown on the right. How do I have to modify the plot for this? This is what my plot currently looks like. As you can see, the gap is at 0.77 and therefore starts very far down. How can I make it look like this plot?
0 -
Hi Lorenz,
I'm guessing this plot may have been created from scratch using Plotly from the data in the dataframe. I'm "Team Matplotlib" so I could create the equivalent example with that library if that is suitable?
Note however that Plotly is interactive, Matplotlib is not. You can select a region to zoom into with Plotly.
- Riley
0 -
Riley Clement That would be perfect, as I prefer matplotlib too. Thank you so much
0 -
Hi Lorenz,
Here we go:
import matplotlib.pyplot as plt
from matplotlib.ticker import PercentFormatter
import itertools
def combine_legends(*axes):
handles = list(itertools.chain(*[ax.get_legend_handles_labels()[0] for ax in axes]))
labels = list(
itertools.chain(*[ax.get_legend_handles_labels()[1] for ax in axes])
)
return handles, labels
def set_obj_axes_labels(ax):
ax.set_ylabel("objective value")
ax.set_xlabel("time")
def plot_incumbent(df, ax):
ax.step(
df["Time"],
df["Incumbent"],
where="post",
color="b",
label="Incumbent",
)
set_obj_axes_labels(ax)
def plot_bestbd(df, ax):
ax.step(
df["Time"],
df["BestBd"],
where="post",
color="r",
label="BestBd",
)
set_obj_axes_labels(ax)
def plot_fillabsgap(df, ax):
ax.fill_between(
df["Time"],
df["BestBd"],
df["Incumbent"],
step="post",
color="grey",
alpha=0.3,
)
set_obj_axes_labels(ax)
def plot_relgap(df, ax):
ax.step(
df["Time"],
df["Gap"],
where="post",
color="green",
label="Gap",
)
ax.set_ylabel("gap in %")
ax.set_ylim(0, 1)
formatter = PercentFormatter(1)
ax.yaxis.set_major_formatter(formatter)
def plot(df):
with plt.style.context("seaborn-v0_8"):
_, ax = plt.subplots(figsize=(8, 5))
plot_incumbent(df, ax)
plot_bestbd(df, ax)
plot_fillabsgap(df, ax)
ax2 = ax.twinx()
plot_relgap(df, ax2)
ax.set_xlim(1,40)
ax.legend(*combine_legends(ax, ax2))
plot(default_run)Result:
0 -
Thank you so much!
0
Please sign in to leave a comment.
Comments
10 comments