Warm start solvers with snapshot#
Description: We show how to warm start a solver with a previous solution. A nonlinear Unit Commitment problem is being used as example. We will use the “snapshot” feature for this matter.
Tags: warm-start, mp, gurobi, snapshot, electric-power-industry
Notebook author: Marcos Dominguez Velad <marcos@ampl.com>
# Install dependencies
%pip install -q amplpy pandas numpy
# Google Colab & Kaggle integration
from amplpy import AMPL, ampl_notebook
ampl = ampl_notebook(
modules=["gurobi"], # modules to install
license_uuid="default", # license to use
) # instantiate AMPL object and register magics
The snapshot command#
Sometimes we solve very large problems that take some time to solve. It can be a few minutes, hours, or until a certain huge timeout.
There is a way to make a snapshot of your model, so you can continue working on it later, and try to improve even further the solution found. You can also warm-start the solver with the previous solution you had, probably saving a lot of time.
The snapshot command allows you saving your optimization problem into a file containing your model, data, options, and also variable values from previous solutions.
The problem#
We are solving a version of Unit Commitment with generators with minimum and maximum outputs and ramp limits. There are linear and quadratic costs, so the problem becomes a MINLP.
For a model explanation, please take a look at the notebook: https://colab.ampl.com/notebooks/unit-commitment-minlp-with-knitro.html
We will focus on using the “snapshot” command to warm-start optimization problems, taking advantage of the most promising previous solution.
Warning: this notebook uses commercial solvers, so when running on Cloud you may need a license for these solvers (Knitro, Gurobi, Xpress), or run locally.
MINLP Unit Commitment model#
%%writefile unit_commitment.mod
set GENERATORS;
set TIME ordered;
param demand {TIME} >= 0; # Power demand at each time
param min_output {GENERATORS} >= 0; # Minimum power output
param max_output {g in GENERATORS} >= min_output[g]; # Maximum power output
param ramp_up_limit {GENERATORS} >= 0;
param ramp_down_limit {GENERATORS} >= 0;
param linear_cost {GENERATORS}; # Linear cost coefficient
param quadratic_cost {GENERATORS} >= 0; # Quadratic cost coefficient
param startup_cost {GENERATORS} >= 0; # Startup cost
param emission_rate {GENERATORS} >= 0; # Tons CO2 per MW produced
var is_committed {GENERATORS, TIME} binary; # 1 if generator is ON
var power_generated {gen in GENERATORS, TIME} >= 0 <= max_output[gen]; # MW produced
var is_startup {GENERATORS, TIME} binary; # 1 if generator starts up
# Generation only if committed
subject to Generation_Commitment {gen in GENERATORS, t in TIME}:
power_generated[gen,t] > 0 <==> is_committed[gen,t] = 1;
# Meet demand in each period
subject to Demand_Satisfaction {t in TIME}:
sum {gen in GENERATORS} power_generated[gen,t] >= demand[t];
# Startup in first period
subject to Startup_First {gen in GENERATORS}:
is_startup[gen, first(TIME)] == is_committed[gen, first(TIME)];
# Startup logic in subsequent periods
subject to Startup_Transition {gen in GENERATORS, t in TIME: ord(t) > 1}:
is_startup[gen,t] <==> (is_committed[gen,t] and !is_committed[gen,prev(t)]);
subject to Min_Gen_If_On {gen in GENERATORS, t in TIME}:
power_generated[gen,t] == 0 or power_generated[gen,t] >= min_output[gen];
# Ramp-up limits
subject to Ramp_Up {gen in GENERATORS, t in TIME: ord(t) > 1}:
power_generated[gen,t] - power_generated[gen,prev(t)] <= ramp_up_limit[gen];
# Ramp-down limits
subject to Ramp_Down {gen in GENERATORS, t in TIME: ord(t) > 1}:
power_generated[gen,prev(t)] - power_generated[gen,t] <= ramp_down_limit[gen];
# Minimize total operating + startup cost
minimize Total_Cost:
sum {gen in GENERATORS, t in TIME}
(linear_cost[gen] * power_generated[gen,t] +
quadratic_cost[gen] * power_generated[gen,t]^2)
+ sum {gen in GENERATORS, t in TIME} startup_cost[gen] * is_startup[gen,t];
Overwriting unit_commitment.mod
Solving the model#
Generate the problem data#
The AMPL model is isolated from the input data for more readability and maintainance.
The data of the problem includes generators stats and artificial energy demand.
import pandas as pd
import numpy as np
# Unit Commitment data
generators = [
"G1",
"G2",
"G3",
"G4",
"G5",
"G6",
"G7",
"G8",
"G9",
"G10",
"G11",
"G12",
"G13",
"G14",
"G15",
"G16",
"G17",
"G18",
"G19",
"G20",
]
# Generators data
generators_data = pd.DataFrame(
{
"min_output": [
20,
30,
25,
15,
10,
40,
0,
50,
20,
10,
0,
30,
60,
0,
0,
25,
0,
40,
10,
0,
],
"max_output": [
100,
120,
90,
60,
50,
150,
30,
200,
80,
70,
40,
110,
250,
50,
100,
140,
60,
180,
90,
120,
],
"ramp_up_limit": [
40,
50,
30,
25,
20,
60,
10,
70,
35,
30,
20,
45,
80,
25,
40,
45,
20,
65,
35,
50,
],
"ramp_down_limit": [
40,
50,
30,
25,
20,
60,
10,
70,
35,
30,
20,
45,
80,
25,
40,
45,
20,
65,
35,
50,
],
"linear_cost": [
20,
16,
18,
22,
24,
14,
12,
13,
21,
26,
10,
17,
11,
9,
15,
19,
8,
14,
23,
16,
],
"quadratic_cost": [
0.04,
0.05,
0.06,
0.03,
0.04,
0.036,
0.10,
0.035,
0.045,
0.055,
0.02,
0.05,
0.03,
0.01,
0.025,
0.04,
0.015,
0.032,
0.05,
0.028,
],
"startup_cost": [
400,
300,
360,
200,
160,
600,
160,
800,
280,
220,
100,
350,
1000,
80,
180,
320,
60,
700,
210,
260,
],
"emission_rate": [
0.7,
0.5,
0.6,
0.4,
0.3,
0.8,
0.0,
0.9,
0.55,
0.45,
0.0,
0.6,
0.85,
0.0,
0.2,
0.65,
0.0,
0.75,
0.35,
0.1,
],
},
index=generators,
)
# Generate random demand
num_time_periods = 24 * 50
time_periods = list(range(1, num_time_periods + 1))
np.random.seed(42)
base_demand = 150 + 40 * np.sin(np.linspace(0, 3 * np.pi, num_time_periods))
noise = np.random.normal(0, 10, num_time_periods)
demand = (base_demand + noise).clip(min=100).round().astype(int)
Running the solver#
Since it’s a hard problem, we are running the solver just for some time and saving the solution into a “snapshot” file.
We are setting timelim=45 to run the solver for 45 seconds.
max_seconds = 45
SOLVER = "gurobi"
ampl = AMPL()
ampl.read("unit_commitment.mod")
ampl.set["TIME"] = time_periods
ampl.set["GENERATORS"] = generators
ampl.param["demand"] = demand
ampl.set_data(generators_data, "GENERATORS")
ampl.solve(solver=SOLVER, mp_options="outlev=1 timelim=" + str(max_seconds))
snapshot_filename = f"{SOLVER}_snapshot.run"
ampl.snapshot(filename=snapshot_filename)
Gurobi 13.0.0: Set parameter LogToConsole to value 1
tech:outlev = 1
Set parameter TimeLimit to value 45
lim:time = 45
AMPL MP initial flat model has 72000 variables (0 integer, 48000 binary);
Objectives: 1 quadratic;
Constraints: 97160 linear;
Logical expressions: 40800 conditional (in)equalitie(s); 23980 and; 47980 not; 16800 or;
AMPL MP final model has 273541 variables (64780 integer, 184760 binary);
Objectives: 1 quadratic;
Constraints: 145140 linear;
Logical expressions: 23980 and; 24000 indeq; 40800 indge; 16800 or;
Set parameter InfUnbdInfo to value 1
Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (linux64 - "TUXEDO OS 2")
CPU model: 13th Gen Intel(R) Core(TM) i5-1340P, instruction set [SSE2|AVX|AVX2]
Thread count: 16 physical cores, 16 logical processors, using up to 16 threads
Non-default parameters:
TimeLimit 45
InfUnbdInfo 1
Optimize a model with 145140 rows, 273541 columns and 311880 nonzeros (Min)
Model fingerprint: 0x6dd94741
Model has 48000 linear objective coefficients
Model has 24000 quadratic objective terms
Model has 105580 simple general constraints
23980 AND, 16800 OR, 64800 INDICATOR
Variable types: 112781 continuous, 160760 integer (0 binary)
Coefficient statistics:
Matrix range [1e+00, 1e+00]
Objective range [8e+00, 1e+03]
QObjective range [2e-02, 2e-01]
Bounds range [1e+00, 2e+02]
RHS range [1e+00, 2e+02]
GenCon rhs range [1e-04, 6e+01]
GenCon coe range [1e+00, 1e+00]
Presolve removed 7188 rows and 201555 columns
Presolve time: 1.32s
Presolved: 163610 rows, 84815 columns, 385647 nonzeros
Presolved model has 24000 quadratic objective terms
Variable types: 36829 continuous, 47986 integer (47986 binary)
Found heuristic solution: objective 1930375.4137
Found heuristic solution: objective 1923958.9456
Performing another presolve...
Presolve removed 26771 rows and 5 columns
Presolve time: 0.69s
Root simplex log...
Iteration Objective Primal Inf. Dual Inf. Time
64660 1.8726326e+06 2.648510e+01 0.000000e+00 5s
Warning: 1 variables dropped from basis
79341 1.8743944e+06 0.000000e+00 4.548839e+02 10s
80623 1.8744675e+06 0.000000e+00 0.000000e+00 11s
Root relaxation: objective 1.874468e+06, 80623 iterations, 7.72 seconds (7.11 work units)
Nodes | Current Node | Objective Bounds | Work
Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time
0 0 1874467.50 0 2252 1923958.95 1874467.50 2.57% - 11s
H 0 0 1923294.0737 1888266.50 1.82% - 34s
0 0 1888266.50 0 3160 1923294.07 1888266.50 1.82% - 34s
H 0 0 1915639.1176 1889533.60 1.36% - 43s
H 0 0 1915431.4925 1889533.60 1.35% - 43s
H 0 0 1915362.0988 1889533.60 1.35% - 43s
0 0 1889533.60 0 4639 1915362.10 1889533.60 1.35% - 44s
0 0 1889645.69 0 4790 1915362.10 1889645.69 1.34% - 45s
Cutting planes:
Cover: 6
MIR: 629
Flow cover: 372
Explored 1 nodes (103567 simplex iterations) in 45.04 seconds (37.15 work units)
Thread count was 16 (of 16 available processors)
Solution count 6: 1.91536e+06 1.91543e+06 1.91564e+06 ... 1.93038e+06
Time limit reached
Best objective 1.915362098819e+06, best bound 1.889645691718e+06, gap 1.3426%
Gurobi 13.0.0: time limit, feasible solution; objective 1915362.099
103567 simplex iterations
1 branching node
absmipgap=25716.4, relmipgap=0.0134264
''
Warm-start AMPL#
We can warm-start AMPL with a previous solution from another process using the snapshot command.
The output from snapshot is a model readable by AMPL, so just calling ampl.read(snapshot_filename) will load the previous session into ampl.
ampl = AMPL()
ampl.read(snapshot_filename)
print(ampl.obj["Total_Cost"].value())
1915362.0988187802
From there, you can update values, options, try another solver, add a new model…
ampl.solve()
Gurobi 13.0.0: Set parameter LogToConsole to value 1
tech:outlev = 1
Set parameter TimeLimit to value 45
lim:time = 45
AMPL MP initial flat model has 72000 variables (0 integer, 48000 binary);
Objectives: 1 quadratic;
Constraints: 97160 linear;
Logical expressions: 40800 conditional (in)equalitie(s); 23980 and; 47980 not; 16800 or;
AMPL MP final model has 273541 variables (64780 integer, 184760 binary);
Objectives: 1 quadratic;
Constraints: 145140 linear;
Logical expressions: 23980 and; 24000 indeq; 40800 indge; 16800 or;
Set parameter InfUnbdInfo to value 1
Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (linux64 - "TUXEDO OS 2")
CPU model: 13th Gen Intel(R) Core(TM) i5-1340P, instruction set [SSE2|AVX|AVX2]
Thread count: 16 physical cores, 16 logical processors, using up to 16 threads
Non-default parameters:
TimeLimit 45
InfUnbdInfo 1
Optimize a model with 145140 rows, 273541 columns and 311880 nonzeros (Min)
Model fingerprint: 0xf6a89494
Model has 48000 linear objective coefficients
Model has 24000 quadratic objective terms
Model has 105580 simple general constraints
23980 AND, 16800 OR, 64800 INDICATOR
Variable types: 112781 continuous, 160760 integer (0 binary)
Coefficient statistics:
Matrix range [1e+00, 1e+00]
Objective range [8e+00, 1e+03]
QObjective range [2e-02, 2e-01]
Bounds range [1e+00, 2e+02]
RHS range [1e+00, 2e+02]
GenCon rhs range [1e-04, 6e+01]
GenCon coe range [1e+00, 1e+00]
User MIP start produced solution with objective 1.91536e+06 (0.23s)
Loaded user MIP start with objective 1.91536e+06
Presolve removed 7188 rows and 201555 columns
Presolve time: 1.30s
Presolved: 163610 rows, 84815 columns, 385647 nonzeros
Presolved model has 24000 quadratic objective terms
Variable types: 36829 continuous, 47986 integer (47986 binary)
Performing another presolve...
Presolve removed 26771 rows and 5 columns
Presolve time: 0.73s
Root simplex log...
Iteration Objective Primal Inf. Dual Inf. Time
64405 1.8725670e+06 3.143994e+01 0.000000e+00 5s
Warning: 1 variables dropped from basis
76475 -7.7336320e+06 1.901313e+00 0.000000e+00 10s
80623 1.8744675e+06 0.000000e+00 0.000000e+00 11s
Root relaxation: objective 1.874468e+06, 80623 iterations, 8.30 seconds (7.11 work units)
Nodes | Current Node | Objective Bounds | Work
Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time
0 0 1874467.50 0 2252 1915362.10 1874467.50 2.14% - 11s
0 0 1880133.52 0 3924 1915362.10 1880133.52 1.84% - 42s
0 0 - 0 1915362.10 1880133.52 1.84% - 45s
Cutting planes:
Cover: 9
MIR: 743
Flow cover: 566
Explored 1 nodes (96714 simplex iterations) in 45.01 seconds (37.22 work units)
Thread count was 16 (of 16 available processors)
Solution count 1: 1.91536e+06
Time limit reached
Best objective 1.915362098819e+06, best bound 1.880133520804e+06, gap 1.8393%
Gurobi 13.0.0: time limit, feasible solution; objective 1915362.099
96714 simplex iterations
1 branching node
absmipgap=35228.6, relmipgap=0.0183926
First reported incumbent solution is the one from the previous run, which is loaded in the snapshot (optimal value: 1915362.10).
The gap percentage may look counter-intuitive, since in the first run, the solver managed to reduce it to 1.34%, and in this second run, the gap only reaches 1.84%. This is due to the solver not remembering previous gaps, although the quality of the solution is the same.
We can try reducing the gap by using the mip:focus option from the solver. Setting mip:focus=2 will favor providing optimality.
mip:focus (mipfocus). MIP solution strategy:
0 - Balance finding good feasible solutions and proving optimality (default)
1 - Favor finding feasible solutions
2 - Favor providing optimality
3 - Focus on improving the best objective bound.
ampl = AMPL()
ampl.read(snapshot_filename)
ampl.solve(solver=SOLVER, mp_options="outlev=1 mip:focus=3 timelim=90")
Gurobi 13.0.0: Set parameter LogToConsole to value 1
tech:outlev = 1
Set parameter MIPFocus to value 3
mip:focus = 3
Set parameter TimeLimit to value 90
lim:time = 90
AMPL MP initial flat model has 72000 variables (0 integer, 48000 binary);
Objectives: 1 quadratic;
Constraints: 97160 linear;
Logical expressions: 40800 conditional (in)equalitie(s); 23980 and; 47980 not; 16800 or;
AMPL MP final model has 273541 variables (64780 integer, 184760 binary);
Objectives: 1 quadratic;
Constraints: 145140 linear;
Logical expressions: 23980 and; 24000 indeq; 40800 indge; 16800 or;
Set parameter InfUnbdInfo to value 1
Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (linux64 - "TUXEDO OS 2")
CPU model: 13th Gen Intel(R) Core(TM) i5-1340P, instruction set [SSE2|AVX|AVX2]
Thread count: 16 physical cores, 16 logical processors, using up to 16 threads
Non-default parameters:
TimeLimit 90
MIPFocus 3
InfUnbdInfo 1
Optimize a model with 145140 rows, 273541 columns and 311880 nonzeros (Min)
Model fingerprint: 0xf6a89494
Model has 48000 linear objective coefficients
Model has 24000 quadratic objective terms
Model has 105580 simple general constraints
23980 AND, 16800 OR, 64800 INDICATOR
Variable types: 112781 continuous, 160760 integer (0 binary)
Coefficient statistics:
Matrix range [1e+00, 1e+00]
Objective range [8e+00, 1e+03]
QObjective range [2e-02, 2e-01]
Bounds range [1e+00, 2e+02]
RHS range [1e+00, 2e+02]
GenCon rhs range [1e-04, 6e+01]
GenCon coe range [1e+00, 1e+00]
User MIP start produced solution with objective 1.91536e+06 (0.23s)
Loaded user MIP start with objective 1.91536e+06
Presolve removed 7172 rows and 201539 columns
Presolve time: 1.08s
Presolved: 152324 rows, 79180 columns, 357424 nonzeros
Presolved model has 24000 quadratic objective terms
Variable types: 31178 continuous, 48002 integer (48002 binary)
Performing another presolve...
Presolve removed 24006 rows and 22 columns
Presolve time: 0.83s
Root relaxation presolved: 128322 rows, 79171 columns, 357358 nonzeros
Root relaxation presolved model has 24000 quadratic objective terms
Root simplex log...
Iteration Objective Primal Inf. Dual Inf. Time
70400 1.8696892e+06 9.489444e+07 0.000000e+00 5s
78444 1.8699889e+06 0.000000e+00 0.000000e+00 9s
78444 1.8699889e+06 0.000000e+00 0.000000e+00 9s
Root relaxation: objective 1.869989e+06, 78444 iterations, 6.85 seconds (7.90 work units)
Nodes | Current Node | Objective Bounds | Work
Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time
0 0 1869988.87 0 3471 1915362.10 1869988.87 2.37% - 9s
0 0 1875841.76 0 2942 1915362.10 1875841.76 2.06% - 44s
0 0 1885893.45 0 3703 1915362.10 1885893.45 1.54% - 56s
0 0 1887926.51 0 5009 1915362.10 1887926.51 1.43% - 66s
0 0 1887927.55 0 5009 1915362.10 1887927.55 1.43% - 67s
0 0 1889815.89 0 5268 1915362.10 1889815.89 1.33% - 82s
0 0 1890562.66 0 5288 1915362.10 1890562.66 1.29% - 87s
0 0 - 0 1915362.10 1890562.66 1.29% - 90s
Cutting planes:
Cover: 4
MIR: 1533
Mixing: 3
Flow cover: 1413
Explored 1 nodes (142297 simplex iterations) in 90.10 seconds (85.87 work units)
Thread count was 16 (of 16 available processors)
Solution count 1: 1.91536e+06
Time limit reached
Best objective 1.915362098819e+06, best bound 1.890562659322e+06, gap 1.2948%
Gurobi 13.0.0: time limit, feasible solution; objective 1915362.099
142297 simplex iterations
1 branching node
absmipgap=24799.4, relmipgap=0.0129477