Skip to main content

How to implement an optimality gap plot?

Answered

Comments

10 comments

  • Riley Clement
    Gurobi Staff Gurobi Staff

    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
  • Lorenz Wagner
    Investigator
    Gurobi-versary
    Conversationalist

    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
  • Riley Clement
    Gurobi Staff Gurobi Staff

    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
  • Lorenz Wagner
    Investigator
    Gurobi-versary
    Conversationalist

    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
  • Riley Clement
    Gurobi Staff Gurobi Staff

    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
  • Lorenz Wagner
    Investigator
    Gurobi-versary
    Conversationalist

    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
  • Riley Clement
    Gurobi Staff Gurobi Staff

    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
  • Lorenz Wagner
    Investigator
    Gurobi-versary
    Conversationalist

    Riley Clement That would be perfect, as I prefer matplotlib too. Thank you so much 

    0
  • Riley Clement
    Gurobi Staff Gurobi Staff

    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
  • Lorenz Wagner
    Investigator
    Gurobi-versary
    Conversationalist

    Thank you so much!

    0

Please sign in to leave a comment.