Unit Commitment Problem with AMPL and Python - Power Grid Lib#

pglib_uc.ipynb Open In Colab Kaggle Gradient Open In SageMaker Studio Lab Hits

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.

\[ \text{min } \sum_{g \in {\mathcal G}} \sum_{t \in {\mathcal T}} \left( c_g(t) + CP_g^1 \, u_g(t) + \sum_{s = 1}^{S_g} \left( CS^s_g \delta^s(t) \right) \right) \hspace{1cm} (1) \]
\[\begin{split} \begin{split} \text{subject to:}\\ %\label{eq:UCDemand} & \sum_{g \in {\mathcal G}} \left( p_g(t) + \underline{P}_g u_g(t) \right) + \sum_{w\in {\mathcal W}} p_w(t) = D(t) & \hspace{5cm} \forall t \in {\mathcal T} & \hspace{1cm} (2) \\ %\label{eq:UCReserves} & \sum_{g \in {\mathcal G}} r_g(t) \geq R(t) & \forall t \in {\mathcal T} & \hspace{1cm} (3) \\ %\label{eq:initialUpRequirement} & \sum_{t=1}^{\min\{UT_g - UT_g^0, T\}} (u_g(t) - 1) = 0 & \hspace{3cm} \forall g \in {\mathcal G}_{\textit{on}}^0 & \hspace{1cm} (4) \\ % \label{eq:initialDownRequirement} & \sum_{t=1}^{\min\{DT_g - DT_g^0, T\}} u_g(t) = 0 & \forall g \in {\mathcal G}_{\textit{off}}^0 & \hspace{1cm} (5) \\ % \label{eq:LogicalInitial} & u_g(1) - U_g^0 = v_g(1) - w_g(1) & \forall g \in {\mathcal G} & \hspace{1cm} (6) \\ % \label{eq:STIInit} & \sum_{s=1}^{S_g-1} \sum_{t=\max\{1, TS^{s+1}_g - DT^0_g + 1\}}^{\min\{TS^{s+1}_g -1,T\}} \delta^s_g(t) = 0 & \forall g \in {\mathcal G}& \hspace{1cm} (7) \\ % \label{eq:RampUpInit} & p_g(1) + r_g(1) - U_g^0(P_g^0-\underline{P}_g) \leq RU_g & \forall g \in {\mathcal G} & \hspace{1cm} (8) \\ % \label{eq:RampDownInit} & U_g^0(P_g^0-\underline{P}_g) - p_g(1) \leq RD_g & \forall g \in {\mathcal G} & \hspace{1cm} (9) \\ % \label{eq:MaxOutput2Init} & U_g^0(P_g^0-\underline{P}_g) \leq (\overline{P}_g - \underline{P}_g) U_g^0 - \max\{(\overline{P}_g - SD_g),0\} w_g(1) & \forall g \in {\mathcal G} & \hspace{1cm} (10) \\ % \label{eq:MustRun} & u_g(t) \geq U_g & \hspace{1cm} \forall t \in {\mathcal T}, \, \forall g \in {\mathcal G} & \hspace{1cm} (11) \\ % \label{eq:Logical} & u_g(t) - u_g(t-1) = v_g(t) - w_g(t) & \forall t \in {\mathcal T}\setminus\{1\}, \, \forall g \in {\mathcal G} & \hspace{1cm} (12) \\ % \label{eq:Startup} & \sum_{i= t-\min\{UT_g,T\} + 1}^t v_g(i) \leq u_g(t) & \forall t \in \{\min\{UT_g,T\} \ldots, T\}, \, \forall g \in {\mathcal G} & \hspace{1cm} (13) \\ % \label{eq:Shutdown} & \sum_{i= t-\min\{DT_g,T\} + 1}^t w_g(i) \leq 1 - u_g(t) & \forall t \in \{\min\{DT_g, T\}, \ldots, T\}, \, \forall g \in {\mathcal G} & \hspace{1cm} (14) \\ % \label{eq:STISelect} & \delta^s_g(t) \leq \sum_{i = TS^s_g}^{TS^{s+1}_g-1} w_g(t-i) & \forall t \in \{TS^{s+1}_g,\ldots,T\},\,\forall s \in {\mathcal T}_g\!\setminus\!\{S_g\},\, \forall g \in {\mathcal G} & \hspace{1cm} (15) \\ % \label{eq:STILink} & v_g(t) = \sum_{s = 1}^{S_g} \delta^s_g(t) & \forall t \in {\mathcal T},\, \forall g \in {\mathcal G} & \hspace{1cm} (16) \\ % \label{eq:MaxOutput1} & p_g(t) + r_g(t) \leq (\overline{P}_g - \underline{P}_g) u_g(t) - \max\{(\overline{P}_g - SU_g),0\} v_g(t) & \forall t \in {\mathcal T}, \, \forall g \in {\mathcal G} & \hspace{1cm} (17) \\ %\label{eq:MaxOutput2} & p_g(t) + r_g(t) \leq (\overline{P}_g - \underline{P}_g) u_g(t) - \max\{(\overline{P}_g - SD_g),0\} w_g(t+1) & \forall t \in {\mathcal T}\setminus \{T\}, \,\forall g \in {\mathcal G} & \hspace{1cm} (18) \\ % \label{eq:RampUp} & p_g(t) + r_g(t) - p_g(t-1) \leq RU_g & \forall t \in {\mathcal T}\setminus\{1\}, \, \forall g \in {\mathcal G} & \hspace{1cm} (19) \\ % \label{eq:RampDown} & p_g(t-1) - p_g(t) \leq RD_g & \forall t \in {\mathcal T}\setminus\{1\}, \, \forall g \in {\mathcal G} & \hspace{1cm} (20) \\ % \label{eq:PiecewiseParts} & \hspace{1cm} (2) \\ & p_g(t) = \sum_{l \in {\mathcal L}_g} (P_g^l - P_g^1) \lambda_g^l(t) &\hspace{5cm} \forall t \in {\mathcal T}, \, \forall g \in {\mathcal G} & \hspace{1cm} (21) \\ % \label{eq:PiecewisePartsCost} & c_g(t) = \sum_{l \in {\mathcal L}_g} (CP_g^l - CP_g^1) \lambda_g^l(t) & \forall t \in {\mathcal T}, \, \forall g \in {\mathcal G} & \hspace{1cm} (22) \\ % \label{eq:PiecewiseLimits} & u_g(t) = \sum_{l \in {\mathcal L}_g} \lambda_g^l(t) & \forall t \in {\mathcal T}, \forall g \in {\mathcal G} & \hspace{1cm} (23) \\ % \label{eq:WindLimit} & \underline{P}_w(t) \leq p_w(t) \leq \overline{P}_w(t) &\hspace{6cm} \forall t \in {\mathcal T}, \, \forall w \in {\mathcal W} & \hspace{1cm} (24) \end{split} \end{split}\]

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).