# 1. Getting started (Pyomo) — MIPLearn 0.3

# 1. Getting started (Pyomo)

## 1.1. Introduction

MIPLearn is an open source framework that uses machine learning (ML) to accelerate the performance of mixed-integer programming solvers (e.g. Gurobi, CPLEX, XPRESS). In this tutorial, we will:

1. Install the Python/Pyomo version of MIPLearn

2. Model a simple optimization problem using Pyomo

3. Generate training data and train the ML models

4. Use the ML models together Gurobi to solve new instances

Note

The Python/Pyomo version of MIPLearn is currently only compatible with Pyomo persistent solvers (Gurobi, CPLEX and XPRESS). For broader solver compatibility, see the Julia/JuMP version of the package.

Warning

MIPLearn is still in early development stage. If run into any bugs or issues, please submit a bug report in our GitHub repository. Comments, suggestions and pull requests are also very welcome!

## 1.2. Installation

MIPLearn is available in two versions:

- Python version, compatible with the Pyomo and Gurobipy modeling languages,

- Julia version, compatible with the JuMP modeling language.

In this tutorial, we will demonstrate how to use and install the Python/Pyomo version of the package. The first step is to install Python 3.8+ in your computer. See the official Python website for more instructions. After Python is installed, we proceed to install MIPLearn using `pip`:

    $ pip install MIPLearn==0.3

In addition to MIPLearn itself, we will also install Gurobi 10.0, a state-of-the-art commercial MILP solver. This step also install a demo license for Gurobi, which should able to solve the small optimization problems in this tutorial. A license is required for solving larger-scale problems.

    $ pip install 'gurobipy>=10,<10.1'

Note

In the code above, we install specific version of all packages to ensure that this tutorial keeps running in the future, even when newer (and possibly incompatible) versions of the packages are released. This is usually a recommended practice for all Python projects.

## 1.3. Modeling a simple optimization problem

To illustrate how can MIPLearn be used, we will model and solve a small optimization problem related to power systems optimization. The problem we discuss below is a simplification of the unit commitment problem, a practical optimization problem solved daily by electric grid operators around the world.

Suppose that a utility company needs to decide which electrical generators should be online at each hour of the day, as well as how much power should each generator produce. More specifically, assume that the company owns \(n\) generators, denoted by \(g_1, \ldots, g_n\). Each generator can either be online or offline. An online generator \(g_i\) can produce between \(p^\text{min}_i\) to \(p^\text{max}_i\) megawatts of power, and it costs the company \(c^\text{fix}_i + c^\text{var}_i y_i\), where \(y_i\) is the amount of power produced. An offline generator produces nothing and costs nothing. The total amount of power to be produced needs to be exactly equal to the total demand \(d\) (in megawatts).

This simple problem can be modeled as a mixed-integer linear optimization problem as follows. For each generator \(g_i\), let \(x_i \in \{0,1\}\) be a decision variable indicating whether \(g_i\) is online, and let \(y_i \geq 0\) be a decision variable indicating how much power does \(g_i\) produce. The problem is then given by:

\[\begin{split}\begin{align} \text{minimize } \quad & \sum_{i=1}^n \left( c^\text{fix}_i x_i + c^\text{var}_i y_i \right) \\ \text{subject to } \quad & y_i \leq p^\text{max}_i x_i & i=1,\ldots,n \\ & y_i \geq p^\text{min}_i x_i & i=1,\ldots,n \\ & \sum_{i=1}^n y_i = d \\ & x_i \in \{0,1\} & i=1,\ldots,n \\ & y_i \geq 0 & i=1,\ldots,n \end{align}\end{split}\]

Note

We use a simplified version of the unit commitment problem in this tutorial just to make it easier to follow. MIPLearn can also handle realistic, large-scale versions of this problem.

Next, let us convert this abstract mathematical formulation into a concrete optimization model, using Python and Pyomo. We start by defining a data class `UnitCommitmentData`, which holds all the input data.

    [3]:

    from dataclasses import dataclass
    from typing import List

    import numpy as np

    @dataclass
    class UnitCommitmentData:
        demand: float
        pmin: List[float]
        pmax: List[float]
        cfix: List[float]
        cvar: List[float]

Next, we write a `build_uc_model` function, which converts the input data into a concrete Pyomo model. The function accepts `UnitCommitmentData`, the data structure we previously defined, or the path to a compressed pickle file containing this data.

    [4]:

    import pyomo.environ as pe
    from typing import Union
    from miplearn.io import read_pkl_gz
    from miplearn.solvers.pyomo import PyomoModel

    def build_uc_model(data: Union[str, UnitCommitmentData]) -> PyomoModel:
        if isinstance(data, str):
            data = read_pkl_gz(data)

        model = pe.ConcreteModel()
        n = len(data.pmin)
        model.x = pe.Var(range(n), domain=pe.Binary)
        model.y = pe.Var(range(n), domain=pe.NonNegativeReals)
        model.obj = pe.Objective(
            expr=sum(
                data.cfix[i] * model.x[i] + data.cvar[i] * model.y[i] for i in range(n)
            )
        )
        model.eq_max_power = pe.ConstraintList()
        model.eq_min_power = pe.ConstraintList()
        for i in range(n):
            model.eq_max_power.add(model.y[i] <= data.pmax[i] * model.x[i])
            model.eq_min_power.add(model.y[i] >= data.pmin[i] * model.x[i])
        model.eq_demand = pe.Constraint(
            expr=sum(model.y[i] for i in range(n)) == data.demand,
        )
        return PyomoModel(model, "gurobi_persistent")

At this point, we can already use Pyomo and any mixed-integer linear programming solver to find optimal solutions to any instance of this problem. To illustrate this, let us solve a small instance with three generators:

    [5]:

    model = build_uc_model(
        UnitCommitmentData(
            demand=100.0,
            pmin=[10, 20, 30],
            pmax=[50, 60, 70],
            cfix=[700, 600, 500],
            cvar=[1.5, 2.0, 2.5],
        )
    )

    model.optimize()
    print("obj =", model.inner.obj())
    print("x =", [model.inner.x[i].value for i in range(3)])
    print("y =", [model.inner.y[i].value for i in range(3)])

    Restricted license - for non-production use only - expires 2024-10-28
    Set parameter QCPDual to value 1
    Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)

    CPU model: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz, instruction set [SSE2|AVX|AVX2]
    Thread count: 6 physical cores, 12 logical processors, using up to 12 threads

    Optimize a model with 7 rows, 6 columns and 15 nonzeros
    Model fingerprint: 0x15c7a953
    Variable types: 3 continuous, 3 integer (3 binary)
    Coefficient statistics:
      Matrix range     [1e+00, 7e+01]
      Objective range  [2e+00, 7e+02]
      Bounds range     [1e+00, 1e+00]
      RHS range        [1e+02, 1e+02]
    Presolve removed 2 rows and 1 columns
    Presolve time: 0.00s
    Presolved: 5 rows, 5 columns, 13 nonzeros
    Variable types: 0 continuous, 5 integer (3 binary)
    Found heuristic solution: objective 1400.0000000

    Root relaxation: objective 1.035000e+03, 3 iterations, 0.00 seconds (0.00 work units)

        Nodes    |    Current Node    |     Objective Bounds      |     Work
     Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

         0     0 1035.00000    0    1 1400.00000 1035.00000  26.1%     -    0s
         0     0 1105.71429    0    1 1400.00000 1105.71429  21.0%     -    0s
    *    0     0               0    1320.0000000 1320.00000  0.00%     -    0s

    Explored 1 nodes (5 simplex iterations) in 0.01 seconds (0.00 work units)
    Thread count was 12 (of 12 available processors)

    Solution count 2: 1320 1400

    Optimal solution found (tolerance 1.00e-04)
    Best objective 1.320000000000e+03, best bound 1.320000000000e+03, gap 0.0000%
    WARNING: Cannot get reduced costs for MIP.
    WARNING: Cannot get duals for MIP.
    obj = 1320.0
    x = [-0.0, 1.0, 1.0]
    y = [0.0, 60.0, 40.0]

Running the code above, we found that the optimal solution for our small problem instance costs $1320. It is achieve by keeping generators 2 and 3 online and producing, respectively, 60 MW and 40 MW of power.

Notes

- In the example above, `PyomoModel` is just a thin wrapper around a standard Pyomo model. This wrapper allows MIPLearn to be solver- and modeling-language-agnostic. The wrapper provides only a few basic methods, such as `optimize`. For more control, and to query the solution, the original Pyomo model can be accessed through `model.inner`, as illustrated above.

- To use CPLEX or XPRESS, instead of Gurobi, replace `gurobi_persistent` by `cplex_persistent` or `xpress_persistent` in the `build_uc_model`. Note that only persistent Pyomo solvers are currently supported. Pull requests adding support for other types of solver are very welcome.

## 1.4. Generating training data

Although Gurobi could solve the small example above in a fraction of a second, it gets slower for larger and more complex versions of the problem. If this is a problem that needs to be solved frequently, as it is often the case in practice, it could make sense to spend some time upfront generating a trained solver, which can optimize new instances (similar to the ones it was trained on) faster.

In the following, we will use MIPLearn to train machine learning models that is able to predict the optimal solution for instances that follow a given probability distribution, then it will provide this predicted solution to Gurobi as a warm start. Before we can train the model, we need to collect training data by solving a large number of instances. In real-world situations, we may construct these training instances based on historical data. In this tutorial, we will construct them using a random instance generator:

    [6]:

    from scipy.stats import uniform
    from typing import List
    import random

    def random_uc_data(samples: int, n: int, seed: int = 42) -> List[UnitCommitmentData]:
        random.seed(seed)
        np.random.seed(seed)
        pmin = uniform(loc=100_000.0, scale=400_000.0).rvs(n)
        pmax = pmin * uniform(loc=2.0, scale=2.5).rvs(n)
        cfix = pmin * uniform(loc=100.0, scale=25.0).rvs(n)
        cvar = uniform(loc=1.25, scale=0.25).rvs(n)
        return [
            UnitCommitmentData(
                demand=pmax.sum() * uniform(loc=0.5, scale=0.25).rvs(),
                pmin=pmin,
                pmax=pmax,
                cfix=cfix,
                cvar=cvar,
            )
            for _ in range(samples)
        ]

In this example, for simplicity, only the demands change from one instance to the next. We could also have randomized the costs, production limits or even the number of units. The more randomization we have in the training data, however, the more challenging it is for the machine learning models to learn solution patterns.

Now we generate 500 instances of this problem, each one with 50 generators, and we use 450 of these instances for training. After generating the instances, we write them to individual files. MIPLearn uses files during the training process because, for large-scale optimization problems, it is often impractical to hold in memory the entire training data, as well as the concrete Pyomo models. Files also make it much easier to solve multiple instances simultaneously, potentially on multiple machines. The code below generates the files `uc/train/00000.pkl.gz`, `uc/train/00001.pkl.gz`, etc., which contain the input data in compressed (gzipped) pickle format.

    [7]:

    from miplearn.io import write_pkl_gz

    data = random_uc_data(samples=500, n=500)
    train_data = write_pkl_gz(data[0:450], "uc/train")
    test_data = write_pkl_gz(data[450:500], "uc/test")

Finally, we use `BasicCollector` to collect the optimal solutions and other useful training data for all training instances. The data is stored in HDF5 files `uc/train/00000.h5`, `uc/train/00001.h5`, etc. The optimization models are also exported to compressed MPS files `uc/train/00000.mps.gz`, `uc/train/00001.mps.gz`, etc.

    [8]:

    from miplearn.collectors.basic import BasicCollector

    bc = BasicCollector()
    bc.collect(train_data, build_uc_model, n_jobs=4)

## 1.5. Training and solving test instances

With training data in hand, we can now design and train a machine learning model to accelerate solver performance. In this tutorial, for illustration purposes, we will use ML to generate a good warm start using \(k\)-nearest neighbors. More specifically, the strategy is to:

1. Memorize the optimal solutions of all training instances;

2. Given a test instance, find the 25 most similar training instances, based on constraint right-hand sides;

3. Merge their optimal solutions into a single partial solution; specifically, only assign values to the binary variables that agree unanimously.

4. Provide this partial solution to the solver as a warm start.

This simple strategy can be implemented as shown below, using `MemorizingPrimalComponent`. For more advanced strategies, and for the usage of more advanced classifiers, see the user guide.

    [9]:

    from sklearn.neighbors import KNeighborsClassifier
    from miplearn.components.primal.actions import SetWarmStart
    from miplearn.components.primal.mem import (
        MemorizingPrimalComponent,
        MergeTopSolutions,
    )
    from miplearn.extractors.fields import H5FieldsExtractor

    comp = MemorizingPrimalComponent(
        clf=KNeighborsClassifier(n_neighbors=25),
        extractor=H5FieldsExtractor(
            instance_fields=["static_constr_rhs"],
        ),
        constructor=MergeTopSolutions(25, [0.0, 1.0]),
        action=SetWarmStart(),
    )

Having defined the ML strategy, we next construct `LearningSolver`, train the ML component and optimize one of the test instances.

    [10]:

    from miplearn.solvers.learning import LearningSolver

    solver_ml = LearningSolver(components=[comp])
    solver_ml.fit(train_data)
    solver_ml.optimize(test_data[0], build_uc_model)

    Set parameter QCPDual to value 1
    Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)

    CPU model: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz, instruction set [SSE2|AVX|AVX2]
    Thread count: 6 physical cores, 12 logical processors, using up to 12 threads

    Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    Model fingerprint: 0x5e67c6ee
    Coefficient statistics:
      Matrix range     [1e+00, 2e+06]
      Objective range  [1e+00, 6e+07]
      Bounds range     [1e+00, 1e+00]
      RHS range        [3e+08, 3e+08]
    Presolve removed 1000 rows and 500 columns
    Presolve time: 0.00s
    Presolved: 1 rows, 500 columns, 500 nonzeros

    Iteration    Objective       Primal Inf.    Dual Inf.      Time
           0    6.6166537e+09   5.648803e+04   0.000000e+00      0s
           1    8.2906219e+09   0.000000e+00   0.000000e+00      0s

    Solved in 1 iterations and 0.01 seconds (0.00 work units)
    Optimal objective  8.290621916e+09
    Set parameter QCPDual to value 1
    Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)

    CPU model: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz, instruction set [SSE2|AVX|AVX2]
    Thread count: 6 physical cores, 12 logical processors, using up to 12 threads

    Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    Model fingerprint: 0xa4a7961e
    Variable types: 500 continuous, 500 integer (500 binary)
    Coefficient statistics:
      Matrix range     [1e+00, 2e+06]
      Objective range  [1e+00, 6e+07]
      Bounds range     [1e+00, 1e+00]
      RHS range        [3e+08, 3e+08]

    User MIP start produced solution with objective 8.30129e+09 (0.01s)
    User MIP start produced solution with objective 8.29184e+09 (0.01s)
    User MIP start produced solution with objective 8.29146e+09 (0.01s)
    User MIP start produced solution with objective 8.29146e+09 (0.02s)
    Loaded user MIP start with objective 8.29146e+09

    Presolve time: 0.01s
    Presolved: 1001 rows, 1000 columns, 2500 nonzeros
    Variable types: 500 continuous, 500 integer (500 binary)

    Root relaxation: objective 8.290622e+09, 512 iterations, 0.01 seconds (0.00 work units)

        Nodes    |    Current Node    |     Objective Bounds      |     Work
     Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

         0     0 8.2906e+09    0    1 8.2915e+09 8.2906e+09  0.01%     -    0s

    Cutting planes:
      Cover: 1
      Flow cover: 2

    Explored 1 nodes (512 simplex iterations) in 0.09 seconds (0.01 work units)
    Thread count was 12 (of 12 available processors)

    Solution count 3: 8.29146e+09 8.29184e+09 8.30129e+09

    Optimal solution found (tolerance 1.00e-04)
    Best objective 8.291459497797e+09, best bound 8.290645029670e+09, gap 0.0098%
    WARNING: Cannot get reduced costs for MIP.
    WARNING: Cannot get duals for MIP.

By examining the solve log above, specifically the line `Loaded user MIP start with objective...`, we can see that MIPLearn was able to construct an initial solution which turned out to be very close to the optimal solution to the problem. Now let us repeat the code above, but a solver which does not apply any ML strategies. Note that our previously-defined component is not provided.

    [11]:

    solver_baseline = LearningSolver(components=[])
    solver_baseline.fit(train_data)
    solver_baseline.optimize(test_data[0], build_uc_model)

    Set parameter QCPDual to value 1
    Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)

    CPU model: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz, instruction set [SSE2|AVX|AVX2]
    Thread count: 6 physical cores, 12 logical processors, using up to 12 threads

    Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    Model fingerprint: 0x5e67c6ee
    Coefficient statistics:
      Matrix range     [1e+00, 2e+06]
      Objective range  [1e+00, 6e+07]
      Bounds range     [1e+00, 1e+00]
      RHS range        [3e+08, 3e+08]
    Presolve removed 1000 rows and 500 columns
    Presolve time: 0.01s
    Presolved: 1 rows, 500 columns, 500 nonzeros

    Iteration    Objective       Primal Inf.    Dual Inf.      Time
           0    6.6166537e+09   5.648803e+04   0.000000e+00      0s
           1    8.2906219e+09   0.000000e+00   0.000000e+00      0s

    Solved in 1 iterations and 0.01 seconds (0.00 work units)
    Optimal objective  8.290621916e+09
    Set parameter QCPDual to value 1
    Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)

    CPU model: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz, instruction set [SSE2|AVX|AVX2]
    Thread count: 6 physical cores, 12 logical processors, using up to 12 threads

    Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    Model fingerprint: 0x8a0f9587
    Variable types: 500 continuous, 500 integer (500 binary)
    Coefficient statistics:
      Matrix range     [1e+00, 2e+06]
      Objective range  [1e+00, 6e+07]
      Bounds range     [1e+00, 1e+00]
      RHS range        [3e+08, 3e+08]
    Presolve time: 0.00s
    Presolved: 1001 rows, 1000 columns, 2500 nonzeros
    Variable types: 500 continuous, 500 integer (500 binary)
    Found heuristic solution: objective 9.757128e+09

    Root relaxation: objective 8.290622e+09, 512 iterations, 0.00 seconds (0.00 work units)

        Nodes    |    Current Node    |     Objective Bounds      |     Work
     Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

         0     0 8.2906e+09    0    1 9.7571e+09 8.2906e+09  15.0%     -    0s
    H    0     0                    8.298273e+09 8.2906e+09  0.09%     -    0s
         0     0 8.2907e+09    0    4 8.2983e+09 8.2907e+09  0.09%     -    0s
         0     0 8.2907e+09    0    1 8.2983e+09 8.2907e+09  0.09%     -    0s
         0     0 8.2907e+09    0    4 8.2983e+09 8.2907e+09  0.09%     -    0s
    H    0     0                    8.293980e+09 8.2907e+09  0.04%     -    0s
         0     0 8.2907e+09    0    5 8.2940e+09 8.2907e+09  0.04%     -    0s
         0     0 8.2907e+09    0    1 8.2940e+09 8.2907e+09  0.04%     -    0s
         0     0 8.2907e+09    0    2 8.2940e+09 8.2907e+09  0.04%     -    0s
         0     0 8.2908e+09    0    1 8.2940e+09 8.2908e+09  0.04%     -    0s
         0     0 8.2908e+09    0    4 8.2940e+09 8.2908e+09  0.04%     -    0s
         0     0 8.2908e+09    0    4 8.2940e+09 8.2908e+09  0.04%     -    0s
    H    0     0                    8.291465e+09 8.2908e+09  0.01%     -    0s

    Cutting planes:
      Gomory: 2
      MIR: 1

    Explored 1 nodes (1025 simplex iterations) in 0.08 seconds (0.03 work units)
    Thread count was 12 (of 12 available processors)

    Solution count 4: 8.29147e+09 8.29398e+09 8.29827e+09 9.75713e+09

    Optimal solution found (tolerance 1.00e-04)
    Best objective 8.291465302389e+09, best bound 8.290781665333e+09, gap 0.0082%
    WARNING: Cannot get reduced costs for MIP.
    WARNING: Cannot get duals for MIP.

In the log above, the `MIP start` line is missing, and Gurobi had to start with a significantly inferior initial solution. The solver was still able to find the optimal solution at the end, but it required using its own internal heuristic procedures. In this example, because we solve very small optimization problems, there was almost no difference in terms of running time, but the difference can be significant for larger problems.

## 1.6. Accessing the solution

In the example above, we used `LearningSolver.solve` together with data files to solve both the training and the test instances. The optimal solutions were saved to HDF5 files in the train/test folders, and could be retrieved by reading theses files, but that is not very convenient. In the following example, we show how to build and solve a Pyomo model entirely in-memory, using our trained solver.

    [12]:

    data = random_uc_data(samples=1, n=500)[0]
    model = build_uc_model(data)
    solver_ml.optimize(model)
    print("obj =", model.inner.obj())
    print(" x =", [model.inner.x[i].value for i in range(5)])
    print(" y =", [model.inner.y[i].value for i in range(5)])

    Set parameter QCPDual to value 1
    Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)

    CPU model: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz, instruction set [SSE2|AVX|AVX2]
    Thread count: 6 physical cores, 12 logical processors, using up to 12 threads

    Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    Model fingerprint: 0x2dfe4e1c
    Coefficient statistics:
      Matrix range     [1e+00, 2e+06]
      Objective range  [1e+00, 6e+07]
      Bounds range     [1e+00, 1e+00]
      RHS range        [3e+08, 3e+08]
    Presolve removed 1000 rows and 500 columns
    Presolve time: 0.01s
    Presolved: 1 rows, 500 columns, 500 nonzeros

    Iteration    Objective       Primal Inf.    Dual Inf.      Time
           0    6.5917580e+09   5.627453e+04   0.000000e+00      0s
           1    8.2535968e+09   0.000000e+00   0.000000e+00      0s

    Solved in 1 iterations and 0.01 seconds (0.00 work units)
    Optimal objective  8.253596777e+09
    Set parameter QCPDual to value 1
    Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)

    CPU model: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz, instruction set [SSE2|AVX|AVX2]
    Thread count: 6 physical cores, 12 logical processors, using up to 12 threads

    Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    Model fingerprint: 0x20637200
    Variable types: 500 continuous, 500 integer (500 binary)
    Coefficient statistics:
      Matrix range     [1e+00, 2e+06]
      Objective range  [1e+00, 6e+07]
      Bounds range     [1e+00, 1e+00]
      RHS range        [3e+08, 3e+08]

    User MIP start produced solution with objective 8.25814e+09 (0.01s)
    User MIP start produced solution with objective 8.25512e+09 (0.01s)
    User MIP start produced solution with objective 8.25459e+09 (0.04s)
    User MIP start produced solution with objective 8.25459e+09 (0.04s)
    Loaded user MIP start with objective 8.25459e+09

    Presolve time: 0.01s
    Presolved: 1001 rows, 1000 columns, 2500 nonzeros
    Variable types: 500 continuous, 500 integer (500 binary)

    Root relaxation: objective 8.253597e+09, 512 iterations, 0.00 seconds (0.00 work units)

        Nodes    |    Current Node    |     Objective Bounds      |     Work
     Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

         0     0 8.2536e+09    0    1 8.2546e+09 8.2536e+09  0.01%     -    0s
         0     0 8.2537e+09    0    3 8.2546e+09 8.2537e+09  0.01%     -    0s
         0     0 8.2537e+09    0    1 8.2546e+09 8.2537e+09  0.01%     -    0s
         0     0 8.2537e+09    0    4 8.2546e+09 8.2537e+09  0.01%     -    0s
         0     0 8.2537e+09    0    4 8.2546e+09 8.2537e+09  0.01%     -    0s
         0     0 8.2538e+09    0    4 8.2546e+09 8.2538e+09  0.01%     -    0s
         0     0 8.2538e+09    0    5 8.2546e+09 8.2538e+09  0.01%     -    0s
         0     0 8.2538e+09    0    6 8.2546e+09 8.2538e+09  0.01%     -    0s

    Cutting planes:
      Cover: 1
      MIR: 2
      StrongCG: 1
      Flow cover: 1

    Explored 1 nodes (575 simplex iterations) in 0.11 seconds (0.01 work units)
    Thread count was 12 (of 12 available processors)

    Solution count 3: 8.25459e+09 8.25512e+09 8.25814e+09

    Optimal solution found (tolerance 1.00e-04)
    Best objective 8.254590409970e+09, best bound 8.253768093811e+09, gap 0.0100%
    WARNING: Cannot get reduced costs for MIP.
    WARNING: Cannot get duals for MIP.
    obj = 8254590409.96973
     x = [1.0, 1.0, 0.0, 1.0, 1.0]
     y = [935662.0949263407, 1604270.0218116897, 0.0, 1369560.835229226, 602828.5321028307]

    [ ]:

MIPLearn 2. Getting started (Gurobipy)