Unit Commitment Problem with AMPL and Python - Power Grid Lib#
Description: Generic notebook to solve Unit Commitment problems with AMPL and Python using the Power Grid Lib model and test instances.
Tags: AMPL, amplpy, Python, Power Grid Lib, Unit Commitment Problem
Notebook author: Nicolau Santos <nicolau@ampl.com>
# Install dependencies
%pip install -q amplpy pandas
# Google Colab & Kaggle integration
from amplpy import AMPL, ampl_notebook
ampl = ampl_notebook(
modules=["highs", "gurobi"], # modules to install
license_uuid="default", # license to use
) # instantiate AMPL object and register magics
import json
import time
import pandas as pd
import urllib.request
Introduction#
The Unit Commitment (UC) problem is a mathematical optimization problem in power systems that aims to determine the optimal schedule of power generators to meet electricity demand while minimizing a given objective function subject to various constraints.
In this notebook we provide an AMPL model to solve the Unit Commitment problem variant described in Power Grid Lib - Unit Commitment in Python with amplpy.
Power Grid Lib - Unit Commitment is a colection of Unit Commitment problems curated and maintained by the IEEE PES Task Force on Benchmarks for Validation of Emerging Power System Algorithms. The benchmark was designed to evaluate a version of the the Unit Commitment problem decribed in [1]. The features of the model are:
A global load requirement with time series
An optional global spinning reserve requirement with time series
Thermal generators with technical parameters, including
Minimum and maximum power output
Hourly ramp-up and ramp-down rates
Start-up and shut-down ramp rates
Minimum run-times and off-times
Off time dependent start-up costs
Piecewise linear convex production costs
No-load costs
Optional renewable generators with time series for minimum and maximum production.
The test instances are divided in three groups where the sets of generators have varying load and reserve profiles: /ca [1], /ferc [2] and /rts_gmlc [3].
In this notebook we provide an AMPL model for the Unit Commitment problem with the above mentioned characteristics. The model is acompanied by a generic function that converts the original json data into Python data structures, such as Dictionaries and Pandas Data Frames, and a function that loads the converted data into AMPL with amplpy.
At the end of the notebook example runs with the open source solver HiGHS and the Gurobi solver are provided.
Problem description#
Indices and Sets#
\(g \in \mathcal G\) Set of thermal generators
\(g \in {\mathcal G}_{on}^0\) Set of thermal generators which are initially committed (on)
\(g \in {\mathcal G}_{off}^0\) Set of thermal generators which are not initially committed (off)
\(w \in {\mathcal W}\) set off renewable generators
\(t \in {\mathcal T}\) Hourly time steps: \(1..T, T=time\_periods\)
\(l \in {\mathcal L}_g\) Piecewise production cost intervals for thermal generator \(g: 1..L_g\).
\(s \in {\mathcal S}_g\) Startup categories for thermal generator \(g\), from hottest(1) to coldest (\(S_g\)): \(1..S_g\).
System Parameters#
\(D(t)\) Load (demand) at time \(t\) (MW), demand
\(R(t)\) Spinning reserve at time \(t\) (MW), reserves
Thermal Generator Parameters#
\(CS_g^s\) Startup cost in category \(s\) for generator \(g\) ().
\(CP_g^l\) Cost of operating at piecewise generation point \(l\) for generator \(g\) (MW).
\(DT_g\) Minimum downtime for generator \(g\) (h), timedown minimum.
\(DT_g^0\) Number of time periods the unit has been off prior to the first time period for generator \(g\), timedownt0.
\(\overline{P}_g\) Maximum power output for generator \(g\) (MW), poweroutput maximum.
\(\underline{P}_g\) Minimum power output for generator \(g\) (MW), poweroutput minimum.
\(P_g^0\) Power output for generator \(g\) (MW) in the time period prior to \(t=1\), poweroutputt0
\(P_g^l\) Power level for piecewise generation point \(l\) for generator \(g\) (MW); \(P_g^1=P_g\) andPLg g =Pg,piecewiseproduction[mw ]
\(RD_g\) Ramp-down rate for generator \(g\) (MW/h), rampdownlimit
.
\(RU_g\) Ramp-up rate for generator \(g\) (MW/h), rampuplimit
.
\(SD_g\) Shutdown capability for generator \(g\) (MW), rampshutdownlimit
\(SU_g\) Startup capability for generator \(g\) (MW), rampstartuplimit
\(TS_g^s\) Time offline after which the startup category \(s\) becomes active(h), startup[lag]
.
\(UT_g\) Minimum uptime for generator \(g\) (h), timeupminimum
.
\(UT_g^0\) Number of time periods the unit has been on prior to the first time period for generator \(g\), timeupt0
.
\(U_g^0\) Initial on/off status for generator \(g\), \(U_g^0=1\) for \(g \in \mathcal{G}_{on}^0\), \(U_g^0=0\) for \(g \in \mathcal{G}_{off}^0\) unitont0
.
\(U_g\) Must-run status for generator \(g\), mustrun
.
AMPL model#
%%writefile uc.mod
set thermal_gens;
set renewable_gens;
param S {thermal_gens};
set gen_startup_categories {g in thermal_gens} := 1..S[g];
param startup_lag {g in thermal_gens, gen_startup_categories[g]};
param startup_cost {g in thermal_gens, gen_startup_categories[g]};
param L {thermal_gens};
set gen_pwl_points {g in thermal_gens} := 1..L[g];
param piecewise_mw {g in thermal_gens, gen_pwl_points[g]};
param piecewise_cost {g in thermal_gens, gen_pwl_points[g]};
param T;
set time_periods := 1..T;
param demand {time_periods};
param reserves {time_periods};
param must_run {thermal_gens};
param power_output_minimum {thermal_gens};
param power_output_maximum {thermal_gens};
param ramp_up_limit {thermal_gens};
param ramp_down_limit {thermal_gens};
param ramp_startup_limit {thermal_gens};
param ramp_shutdown_limit {thermal_gens};
param time_up_minimum {thermal_gens};
param time_down_minimum {thermal_gens};
param power_output_t0 {thermal_gens};
param unit_on_t0 {thermal_gens};
param time_down_t0 {thermal_gens};
param time_up_t0 {thermal_gens};
# Renewable Generator Parameters
param ren_power_output_minimum {renewable_gens, time_periods};
param ren_power_output_maximum {renewable_gens, time_periods};
# Variables
var cg {thermal_gens, time_periods};
var pg {thermal_gens, time_periods} >= 0;
var rg {thermal_gens, time_periods} >= 0;
var pw {renewable_gens, time_periods} >= 0;
var ug {thermal_gens, time_periods} binary;
var vg {thermal_gens, time_periods} binary;
var wg {thermal_gens, time_periods} binary;
var dg {g in thermal_gens, gen_startup_categories[g], time_periods} binary;
var lg {g in thermal_gens, gen_pwl_points[g], time_periods} >= 0, <= 1;
# Objective
#(1)
minimize obj:
sum{g in thermal_gens, t in time_periods}(
cg[g,t] +
piecewise_cost[g, 1] * ug[g,t] +
sum{s in gen_startup_categories[g]}(
startup_cost[g, s] * dg[g,s,t]
)
);
# Constraints
#(2)
s.t. UCDemand {t in time_periods}:
sum{g in thermal_gens}(pg[g,t] + power_output_minimum[g] * ug[g,t]) + sum{w in renewable_gens} pw[w,t] == demand[t];
#(3)
s.t. UCReserves {t in time_periods}:
sum{g in thermal_gens} rg[g,t] >= reserves[t];
#(4)
s.t. initialUpRequirement {g in thermal_gens: unit_on_t0[g] == 1}:
sum{t in 1 .. min(time_up_minimum[g] - time_up_t0[g], T)} (ug[g,t] - 1) == 0;
#(5)
s.t. initialDownRequirement {g in thermal_gens: unit_on_t0[g] == 0}:
sum{t in 1 .. min(time_down_minimum[g] - time_down_t0[g], T)} ug[g,t] == 0;
#(6)
s.t. LogicalInitial {g in thermal_gens}:
ug[g,1] - unit_on_t0[g] == vg[g,1] - wg[g,1];
#(7)
s.t. STIInit {g in thermal_gens}:
sum{
s in 1..(S[g]-1),
t in
(max(1, startup_lag[g, s+1] - time_down_t0[g] + 1)) ..
(min(startup_lag[g, s+1]-1, T))
} dg[g,s,t] == 0;
#(8)
s.t. RampUpInit {g in thermal_gens}:
pg[g,1] + rg[g,1] - unit_on_t0[g] * (power_output_t0[g] - power_output_minimum[g]) <= ramp_up_limit[g];
#(9)
s.t. RampDownInit {g in thermal_gens}:
unit_on_t0[g] * (power_output_t0[g] - power_output_minimum[g]) - pg[g,1] <= ramp_down_limit[g];
#(10)
s.t. MaxOutput2Init {g in thermal_gens}:
unit_on_t0[g] * (power_output_t0[g] - power_output_minimum[g]) <=
unit_on_t0[g] *(power_output_maximum[g] - power_output_minimum[g]) - max((power_output_maximum[g] - ramp_shutdown_limit[g]),0) * wg[g,1];
#(11)
s.t. MustRun {g in thermal_gens, t in time_periods}:
ug[g,t] >= must_run[g];
#(12)
s.t. Logical {g in thermal_gens, t in time_periods: t != 1}:
ug[g,t] - ug[g,t-1] == vg[g,t] - wg[g,t];
#(13)
s.t. Startup {g in thermal_gens, t in min(time_up_minimum[g], T) .. T}:
sum{i in (t - min(time_up_minimum[g], T) + 1).. t} vg[g,i] <= ug[g,t];
#(14)
s.t. Shutdown {g in thermal_gens, t in min(time_down_minimum[g], T) .. T}:
sum{i in (t - min(time_down_minimum[g], T) + 1) .. t} wg[g,i] <= 1 - ug[g,t];
#(15)
s.t. STISelect {
g in thermal_gens,
s in gen_startup_categories[g],
t in startup_lag[g, s+1] .. T:
s != S[g]
}:
dg[g,s,t] <= sum{i in startup_lag[g, s] .. (startup_lag[g, s+1]-1)} wg[g,t-i];
#(16)
s.t. STILink {g in thermal_gens, t in time_periods}:
vg[g,t] == sum{s in 1..S[g]} dg[g,s,t];
#(17)
s.t. MaxOutput1 {g in thermal_gens, t in time_periods}:
pg[g,t] + rg[g,t] <=
(power_output_maximum[g] - power_output_minimum[g]) * ug[g,t] -
max((power_output_maximum[g] - ramp_startup_limit[g]),0) * vg[g,t];
#(18)
s.t. MaxOutput2 {g in thermal_gens, t in time_periods: t != T}:
pg[g,t] + rg[g,t] <=
(power_output_maximum[g] - power_output_minimum[g]) * ug[g,t] -
max((power_output_maximum[g] - ramp_shutdown_limit[g]),0) * wg[g,t+1];
#(19)
s.t. RampUp {g in thermal_gens, t in time_periods: t != 1}:
pg[g,t] + rg[g,t] - pg[g,t-1] <= ramp_up_limit[g];
#(20)
s.t. RampDown {g in thermal_gens, t in time_periods: t != 1}:
pg[g,t-1] - pg[g,t] <= ramp_down_limit[g];
#(21)
s.t. PiecewiseParts {g in thermal_gens, t in time_periods}:
pg[g,t] == sum{l in gen_pwl_points[g]}(piecewise_mw[g,l] - piecewise_mw[g,1]) * lg[g,l,t];
#(22)
s.t. PiecewisePartsCost {g in thermal_gens, t in time_periods}:
cg[g,t] == sum{l in gen_pwl_points[g]}((piecewise_cost[g,l] - piecewise_cost[g,1]) * lg[g,l,t]);
#(23)
s.t. PiecewiseLimits {g in thermal_gens, t in time_periods}:
ug[g,t] == sum{l in gen_pwl_points[g]} lg[g,l,t];
#(24)
s.t. WindLimit {w in renewable_gens, t in time_periods}:
ren_power_output_minimum[w,t] <= pw[w,t] <= ren_power_output_maximum[w,t];
Data preparation#
def prepare_pglib_uc(data_file, log=True):
data = json.load(open(data_file, "r"))
thermal_gens_data = data["thermal_generators"]
renewable_gens_data = data["renewable_generators"]
startup_info = []
piecewise_production_info = []
T = data["time_periods"]
S = {}
L = {}
for k, v in thermal_gens_data.items():
for i, val in enumerate(v["startup"]):
startup_info.append([k, i + 1, val["lag"], val["cost"]])
S[k] = len(v["startup"])
for i, val in enumerate(v["piecewise_production"]):
piecewise_production_info.append([k, i + 1, val["mw"], val["cost"]])
L[k] = len(v["piecewise_production"])
del v["startup"]
del v["piecewise_production"]
df_thermal_gens = pd.DataFrame(thermal_gens_data).transpose()
df_thermal_gens = df_thermal_gens.drop("name", axis=1)
df_startup = pd.DataFrame(
startup_info, columns=["gen", "cat", "startup_lag", "startup_cost"]
).set_index(["gen", "cat"])
df_piecewise_production = pd.DataFrame(
piecewise_production_info,
columns=["gen", "int", "piecewise_mw", "piecewise_cost"],
).set_index(["gen", "int"])
renewable_gens = list(renewable_gens_data)
ren_power_output_minimum = {}
ren_power_output_maximum = {}
for k, v in renewable_gens_data.items():
p_min = v["power_output_minimum"]
for i, val in enumerate(p_min):
ren_power_output_minimum[(k, i + 1)] = val
p_max = v["power_output_maximum"]
for i, val in enumerate(p_max):
ren_power_output_maximum[(k, i + 1)] = val
demand = data["demand"]
reserves = data["reserves"]
# pack everything in a dict and return data
ampl_data = {}
ampl_data["T"] = T
ampl_data["S"] = S
ampl_data["L"] = L
ampl_data["demand"] = demand
ampl_data["reserves"] = reserves
ampl_data["renewable_gens"] = renewable_gens
ampl_data["ren_power_output_minimum"] = ren_power_output_minimum
ampl_data["ren_power_output_maximum"] = ren_power_output_maximum
ampl_data["df_thermal_gens"] = df_thermal_gens
ampl_data["df_startup"] = df_startup
ampl_data["df_piecewise_production"] = df_piecewise_production
return ampl_data
Function wrapper#
def run_uc(data, solver="gurobi", solver_options=None, log=True):
start_time = time.time()
if log:
print("Starting run_uc")
# instantiate AMPL and load model
ampl = AMPL()
ampl.read("uc.mod")
# load data
if log:
print("Loading data")
ampl.set_data(data["df_thermal_gens"], "thermal_gens")
ampl.param["S"] = data["S"]
ampl.param["L"] = data["L"]
ampl.param["T"] = data["T"]
ampl.set["renewable_gens"] = data["renewable_gens"]
ampl.param["ren_power_output_minimum"] = data["ren_power_output_minimum"]
ampl.param["ren_power_output_maximum"] = data["ren_power_output_maximum"]
ampl.set_data(data["df_startup"])
ampl.set_data(data["df_piecewise_production"])
ampl.param["demand"] = data["demand"]
ampl.param["reserves"] = data["reserves"]
# set solver and options
if log:
print("Setting solver and options")
ampl.option["solver"] = solver
if solver_options is not None:
ampl.option[solver + "_options"] = solver_options
# solve
if log:
print("Solving")
ampl.solve()
else:
ampl.get_output("solve;")
# check solve result and time
solve_result = ampl.get_value("solve_result")
solve_time = ampl.get_value("_total_solve_elapsed_time")
assert ampl.solve_result in ["solved", "limit"], ampl.solve_result
if solve_result != "solved":
print("WARNING: solver returned '%s' status" % (solve_result,))
# get result info
# objective
objective = ampl.obj["obj"].value()
# dataframe with variables indexed by thermal_gens and time_periods
df_tg_tp = ampl.get_data("cg", "pg", "rg", "ug", "vg", "wg").to_pandas()
# dataframe with variables indexed by renewable_gens and time_periods
df_rg_tp = ampl.get_data("pw").to_pandas()
# dataframe with variables indexed by renewable_gens, gen_startup_categories and time_periods
df_dg = ampl.get_data("dg").to_pandas()
# dataframe with variables indexed by renewable_gens, gen_pwl_points and time_periods
df_lg = ampl.get_data("lg").to_pandas()
var_dict = {
"thermal_info": df_tg_tp,
"renewable_info": df_rg_tp,
"dg_df": df_dg,
"lg_df": df_lg,
}
end_time = time.time()
result = {
"nvars": ampl.get_value("_nvars"),
"ncons": ampl.get_value("_ncons"),
"objective": objective,
"solve_result": solve_result,
"solve_time": solve_time,
"total_time": end_time - start_time,
"vars": var_dict,
}
return result
Numerical example#
# download sample instance
url = "https://raw.githubusercontent.com/power-grid-lib/pglib-uc/refs/heads/master/rts_gmlc/2020-02-09.json"
file = "2020-02-09.json"
urllib.request.urlretrieve(url, file)
data = prepare_pglib_uc(file)
Solve with HiGHS#
result_highs = run_uc(
data, solver="highs", solver_options="outlev=1 timelim=30 threads=16"
)
print("objective:", result_highs["objective"])
assert result_highs["solve_result"] in ["solved", "limit"], ampl.solve_result
Starting run_uc
Loading data
Setting solver and options
Solving
HiGHS 1.8.1: tech:outlev = 1
lim:time = 30
tech:threads = 16
Running HiGHS 1.7.1 (git hash: 43329e5): Copyright (c) 2024 HiGHS under MIT licence terms
Coefficient ranges:
Matrix [1e+00, 5e+03]
Cost [1e+00, 4e+04]
Bound [1e-01, 2e+05]
RHS [1e+00, 4e+03]
Presolving model
28526 rows, 34765 cols, 131950 nonzeros 0s
23661 rows, 29890 cols, 155559 nonzeros 0s
22404 rows, 27425 cols, 157604 nonzeros 0s
Solving MIP model with:
22404 rows
27425 cols (10102 binary, 0 integer, 0 implied int., 17323 continuous)
157604 nonzeros
Nodes | B&B Tree | Objective Bounds | Dynamic Constraints | Work
Proc. InQueue | Leaves Expl. | BestBound BestSol Gap | Cuts InLp Confl. | LpIters Time
0 0 0 0.00% 20304.91 inf inf 0 0 0 0 1.0s
0 0 0 0.00% 2152735.998879 inf inf 0 0 4 7834 1.6s
C 0 0 0 0.00% 2156593.210756 4263233.91457 49.41% 3588 208 74 8904 4.8s
0 0 0 0.00% 2158903.490476 4263233.91457 49.36% 10296 605 74 11851 9.9s
L 0 0 0 0.00% 2159137.959582 2184945.343408 1.18% 10439 567 74 13194 24.4s
Solving report
Status Time limit reached
Primal bound 2184945.34341
Dual bound 2159151.95522
Gap 1.18% (tolerance: 0.01%)
Solution status feasible
2184945.34341 (objective)
0 (bound viol.)
2.22044604925e-16 (int. viol.)
0 (row viol.)
Timing 30.05 (total)
0.90 (presolve)
0.00 (postsolve)
Nodes 0
LP iterations 24207 (total)
0 (strong br.)
5501 (separation)
10872 (heuristics)
Warning code 1 for call Highs_run(lp())
HiGHS 1.8.1: time limit, feasible solution; objective 2184945.343
24207 simplex iterations
0 branching nodes
absmipgap=25793.4, relmipgap=0.011805
"option abs_boundtol 1.1102230246251565e-16;"
or "option rel_boundtol 1.2335811384723962e-16;"
will change deduced dual values.
WARNING: solver returned 'limit' status
objective: 2184945.343407785
Solve with Gurobi#
result_gurobi = run_uc(data, solver="gurobi", solver_options="outlev=1")
print("objective:", result_gurobi["objective"])
assert result_gurobi["solve_result"] in ["solved", "limit"], ampl.solve_result
Starting run_uc
Loading data
Setting solver and options
Solving
Gurobi 12.0.0: Set parameter LogToConsole to value 1
tech:outlev = 1
Set parameter InfUnbdInfo to value 1
Gurobi Optimizer version 12.0.0 build v12.0.0rc1 (win64 - Windows 11.0 (26100.2))
CPU model: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz, instruction set [SSE2|AVX|AVX2|AVX512]
Thread count: 8 physical cores, 16 logical processors, using up to 16 threads
Non-default parameters:
InfUnbdInfo 1
Optimize a model with 34617 rows, 40986 columns and 151682 nonzeros
Model fingerprint: 0x75bda483
Variable types: 25197 continuous, 15789 integer (0 binary)
Coefficient statistics:
Matrix range [1e+00, 5e+03]
Objective range [1e+00, 4e+04]
Bounds range [1e-01, 2e+05]
RHS range [1e+00, 4e+03]
Presolve removed 7614 rows and 12523 columns
Presolve time: 0.30s
Presolved: 27003 rows, 28463 columns, 123106 nonzeros
Variable types: 17188 continuous, 11275 integer (11275 binary)
Found heuristic solution: objective 5261581.3042
Deterministic concurrent LP optimizer: primal and dual simplex
Showing primal log only...
Concurrent spin time: 0.02s
Solved with dual simplex
Use crossover to convert LP symmetric solution to basic solution...
Root relaxation: objective 2.154209e+06, 9281 iterations, 0.31 seconds (0.36 work units)
Nodes | Current Node | Objective Bounds | Work
Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time
0 0 2154208.94 0 125 5261581.30 2154208.94 59.1% - 0s
0 0 2157306.44 0 104 5261581.30 2157306.44 59.0% - 1s
0 0 2157577.64 0 97 5261581.30 2157577.64 59.0% - 1s
0 0 2158421.61 0 121 5261581.30 2158421.61 59.0% - 1s
0 0 2158685.04 0 156 5261581.30 2158685.04 59.0% - 1s
0 0 2158714.85 0 157 5261581.30 2158714.85 59.0% - 1s
0 0 2158720.47 0 157 5261581.30 2158720.47 59.0% - 1s
0 0 2159237.55 0 133 5261581.30 2159237.55 59.0% - 2s
0 0 2159407.60 0 136 5261581.30 2159407.60 59.0% - 2s
0 0 2159422.17 0 165 5261581.30 2159422.17 59.0% - 2s
0 0 2159424.47 0 165 5261581.30 2159424.47 59.0% - 2s
H 0 0 2260324.8753 2159526.48 4.46% - 2s
0 0 2159526.48 0 154 2260324.88 2159526.48 4.46% - 2s
0 0 2159594.27 0 112 2260324.88 2159594.27 4.46% - 2s
0 0 2159608.61 0 111 2260324.88 2159608.61 4.46% - 2s
0 0 2159616.25 0 137 2260324.88 2159616.25 4.46% - 2s
0 0 2159617.23 0 137 2260324.88 2159617.23 4.46% - 2s
0 0 2159664.87 0 148 2260324.88 2159664.87 4.45% - 3s
H 0 0 2260021.2808 2159665.37 4.44% - 3s
H 0 0 2259094.3444 2159665.37 4.40% - 3s
0 0 2159665.37 0 148 2259094.34 2159665.37 4.40% - 3s
H 0 0 2176005.5444 2159697.73 0.75% - 3s
0 0 2159697.73 0 127 2176005.54 2159697.73 0.75% - 3s
H 0 0 2175934.0944 2159699.41 0.75% - 4s
H 0 0 2172217.9501 2159699.41 0.58% - 4s
H 0 0 2171198.2338 2159699.41 0.53% - 4s
H 0 0 2170515.7018 2159699.41 0.50% - 4s
0 0 2159699.41 0 153 2170515.70 2159699.41 0.50% - 4s
0 0 2159700.02 0 154 2170515.70 2159700.02 0.50% - 4s
0 0 2159855.78 0 151 2170515.70 2159855.78 0.49% - 4s
0 0 2159890.62 0 194 2170515.70 2159890.62 0.49% - 5s
0 0 2159891.03 0 172 2170515.70 2159891.03 0.49% - 5s
0 0 2159921.68 0 179 2170515.70 2159921.68 0.49% - 5s
0 0 2159932.20 0 179 2170515.70 2159932.20 0.49% - 5s
0 0 2159933.10 0 181 2170515.70 2159933.10 0.49% - 5s
0 0 2159979.80 0 182 2170515.70 2159979.80 0.49% - 5s
0 0 2159994.53 0 205 2170515.70 2159994.53 0.48% - 5s
0 0 2160003.43 0 203 2170515.70 2160003.43 0.48% - 5s
0 0 2160003.55 0 203 2170515.70 2160003.55 0.48% - 5s
0 0 2160064.08 0 154 2170515.70 2160064.08 0.48% - 5s
H 0 0 2170398.9588 2160088.82 0.48% - 6s
0 0 2160088.82 0 196 2170398.96 2160088.82 0.48% - 6s
0 0 2160096.36 0 211 2170398.96 2160096.36 0.47% - 6s
0 0 2160097.50 0 201 2170398.96 2160097.50 0.47% - 6s
0 0 2160097.56 0 201 2170398.96 2160097.56 0.47% - 6s
0 0 2160148.44 0 185 2170398.96 2160148.44 0.47% - 6s
0 0 2160151.52 0 187 2170398.96 2160151.52 0.47% - 6s
0 0 2160153.42 0 210 2170398.96 2160153.42 0.47% - 6s
0 0 2160153.76 0 210 2170398.96 2160153.76 0.47% - 6s
0 0 2160161.86 0 197 2170398.96 2160161.86 0.47% - 6s
0 0 2160170.94 0 197 2170398.96 2160170.94 0.47% - 6s
0 0 2160170.94 0 197 2170398.96 2160170.94 0.47% - 6s
0 0 2160170.94 0 197 2170398.96 2160170.94 0.47% - 6s
0 0 2160173.49 0 197 2170398.96 2160173.49 0.47% - 6s
0 0 2160173.49 0 197 2170398.96 2160173.49 0.47% - 6s
0 0 2160177.01 0 197 2170398.96 2160177.01 0.47% - 7s
0 2 2160177.01 0 197 2170398.96 2160177.01 0.47% - 7s
290 158 cutoff 13 2170398.96 2161678.95 0.40% 124 10s
H 1211 427 2168112.2998 2163351.86 0.22% 86.2 15s
H 2376 445 2167872.6431 2166082.88 0.08% 60.2 20s
H 2395 445 2167859.6173 2166082.88 0.08% 59.9 20s
* 2665 350 48 2167849.3773 2166340.60 0.07% 55.9 21s
Cutting planes:
Gomory: 18
Lift-and-project: 9
Cover: 31
Implied bound: 60
MIR: 219
Flow cover: 147
Inf proof: 7
RLT: 3
Relax-and-lift: 87
Explored 3684 nodes (175059 simplex iterations) in 23.90 seconds (22.64 work units)
Thread count was 16 (of 16 available processors)
Solution count 10: 2.16785e+06 2.16786e+06 2.16787e+06 ... 2.17601e+06
Optimal solution found (tolerance 1.00e-04)
Best objective 2.167849377296e+06, best bound 2.167801812886e+06, gap 0.0022%
Gurobi 12.0.0: optimal solution; objective 2167849.377
175059 simplex iterations
3684 branching nodes
absmipgap=47.5644, relmipgap=2.19408e-05
objective: 2167849.3772958177
References#
[1] Knueven, Bernard and Ostrowski, James and Watson, Jean-Paul. On mixed integer programming formulations for the unit commitment problem. INFORMS Journal on Computing (2020).
[2] Krall, Eric and Higgins, Michael and O’Neill, Richard P. RTO unit commitment test system. Federal Energy Regulatory Commission (2012).
[3] Barrows, Clayton, Aaron Bloom, Ali Ehlen, Jussi Ikaheimo, Jennie Jorgenson, Dheepak Krishnamurthy, Jessica Lau et al. The IEEE Reliability Test System: A Proposed 2019 Update. IEEE Transactions on Power Systems (2019).
[4] Morales-España, Germán and Latorre, Jesus M and Ramos, Andres. Tight and compact MILP formulation for the thermal unit commitment problem. IEEE Transactions on Power Systems (2013).
[5] Sridhar, Srikrishna and Linderoth, Jeff and Luedtke, James Locally ideal formulations for piecewise linear functions with indicator variables. Operations Research Letters (2013).