Skip to main content

Unit Commitment

What you'll learn: What unit commitment is, why it matters, how the optimization problem is formulated, and how MangoGridStudio solves it.

Prerequisites: Basic understanding of power generation (generators, load). No optimization background required for Sections 1–3; Sections 4–6 assume familiarity with linear programming.

Related docs: LMP Decomposition | API Reference


1. What is Unit Commitment?

The appliance analogy

Think of industrial power generators as very large appliances — but appliances that cannot simply be "dimmed." A gas turbine cannot instantly go from off to producing 200 MW. It must warm up over 30–90 minutes, consuming fuel just to reach minimum stable output. And once started, it must run for a minimum number of hours before it can be shut down again — not because of an arbitrary rule, but because thermal cycling and mechanical stress damage the turbine if it is cycled on and off too frequently.

A coal unit is like a very large electric oven. You can set it to 200°C or 250°C, but you cannot set it to 0°C without turning it fully off — and once off, it takes 8 hours to get back up to temperature. During those 8 hours, it consumes fuel without producing electricity. That startup fuel cost is real and must be paid by whoever schedules the generator.

A nuclear unit is so expensive to shut down and restart that it essentially never turns off. Its "marginal cost" per MWh is low, but its cold-start cost (78,978 MMBTU × fuel price) is prohibitive. In practice, nuclear units are treated as must-run assets and are always committed.

Unit commitment (UC) is the optimization problem that decides which generators to turn on (commit), at which hours, and how much power each committed generator should produce — minimizing total cost over a multi-hour planning horizon, typically 24 hours for the day-ahead market.

Why the binary decision makes it hard

Most of the constraints in UC are linear. The load balance, ramp limits, and power bounds are all expressible as linear inequalities. The hard part is the on/off decision — a generator is either committed or it is not. There is no halfway state. This forces the UC problem to be a mixed-integer linear program (MILP), which is fundamentally harder than the pure-LP economic dispatch that only decides how to split load among already-committed generators.

For 100 generators over 24 hours, there are 2,400 binary variables. For PJM scale (1,000+ generators), there are 24,000+ binary variables. The theoretical search space is astronomically large; in practice, branch-and-bound with a good LP relaxation brings it to a tractable size.

DA-UC vs RT-UC

Day-Ahead UC (DA-UC)Real-Time UC (RT-UC)
Horizon24 hours (hourly intervals)1–4 hours (5–15 min intervals)
When runDay before deliveryRolling throughout the operating day
PurposeCommit generators, set startup scheduleAdjust committed fleet to actual conditions
Key inputsLoad forecast, fuel prices, generator offersReal-time load, actual outages
OutputCommitment schedule + LMP forecastAdjusted dispatch + real-time LMP

MangoGridStudio's Enhanced UC pipeline targets DA-UC. The solver operates on 24 hourly intervals for systems up to PJM scale.

Why getting it wrong is expensive

If the operator commits the wrong generators, two outcomes are possible:

  1. Over-commitment: too many generators online. No-load cost is paid even when the generator produces zero output. The committed generator burns fuel just to stay in standby.
  2. Under-commitment: too few generators. Load cannot be served, or expensive peakers must be emergency-started at short notice (paying a high-cost premium on top of the startup cost).

UC finds the schedule that minimizes total cost — startup costs + no-load costs + fuel costs — while ensuring every MW of load is served within every physical constraint.


2. Decision Variables

Binary commitment variable

is_on[g, h] ∈ {0, 1}
  • g indexes generators (e.g., "107_CC_1" for a 355 MW Gas CC unit in the RTS-GMLC dataset)
  • h indexes hours in the planning horizon (1 through 24 for a day-ahead solve)
  • is_on[g, h] = 1 means generator g is committed (running or in standby) at hour h
  • is_on[g, h] = 0 means generator g is offline at hour h

A committed generator must produce at least Pmin[g] MW and at most Pmax[g] MW. The Pmin constraint is what makes UC non-trivial: a gas turbine with Pmin = 50 MW cannot produce 1 MW. It either produces at least 50 MW or it is offline.

Continuous dispatch variable

p[g, h] ≥ 0

p[g, h] is the actual power output in MW of generator g at hour h. The link to the commitment variable:

Pmin[g] × is_on[g, h] ≤ p[g, h] ≤ Pmax[g] × is_on[g, h] for all g, h

When is_on[g, h] = 0, this forces p[g, h] = 0. When is_on[g, h] = 1, output must be between Pmin and Pmax.

Startup and shutdown indicator variables

startup[g, h] ∈ {0, 1}
shutdown[g, h] ∈ {0, 1}
  • startup[g, h] = 1: generator g starts up at hour h (was offline at h-1, is online at h)
  • shutdown[g, h] = 1: generator g shuts down at hour h (was online at h-1, is offline at h)

These variables are necessary because startup costs must be paid exactly once, at the moment the unit transitions from off to on. Without explicit startup variables, the objective function has no mechanism to account for this cost.


3. Core Constraints

3.1 Commitment transition

The logical relationship between consecutive commitment decisions:

is_on[g, h] - is_on[g, h-1] = startup[g, h] - shutdown[g, h] for all g, h ≥ 2

Reading this:

  • If a unit turns on (is_on goes 0 → 1): startup[g, h] = 1, shutdown[g, h] = 0
  • If a unit turns off (is_on goes 1 → 0): shutdown[g, h] = 1, startup[g, h] = 0
  • If the unit stays on or stays off: both indicators are 0

At most one of startup or shutdown can be 1 at any given hour. This constraint is what forces startup costs to appear exactly when a unit changes state.

3.2 Operating limits

Output is bounded by the commitment state:

Pmin[g] × is_on[g, h] ≤ p[g, h] ≤ Pmax[g] × is_on[g, h] for all g, h

When is_on[g, h] = 0, both sides collapse to 0, forcing p[g, h] = 0. When is_on[g, h] = 1, output must be between minimum stable generation (Pmin) and nameplate capacity (Pmax).

3.3 Ramp constraints

A turbine's output cannot change faster than its mechanical ramp rate.

Important: MangoGridStudio stores all ramp rates in MW/h (megawatts per hour). Some datasets — including the RTS-GMLC gen.csv — publish ramp rates in MW/min. Always multiply by 60 before passing to MangoGridStudio. Never divide by 60 inside a constraint.

Ramp-up:

p[g, h] - p[g, h-1] ≤ ramp_up[g] for all g, h ≥ 2

Ramp-down:

p[g, h-1] - p[g, h] ≤ ramp_down[g] for all g, h ≥ 2

Example from RTS-GMLC — 107_CC_1 (Gas Combined Cycle):

  • Raw ramp in gen.csv: 4.14 MW/min
  • MangoGridStudio ramp_up: 4.14 × 60 = 248.4 MW/h

Reference values for common RTS-GMLC generators:

GeneratorTypeRamp (MW/min)Ramp (MW/h — use this)
107_CC_1Gas CC4.14248.4
113_CT_1Gas CT3.70222.0
101_STEAM_3Coal2.00120.0
121_NUCLEAR_1Nuclearfixed outputn/a (must-run)

3.4 Minimum uptime

Once a generator starts, it must remain online for at least min_up_hours[g] hours. The sliding-window constraint:

sum(shutdown[g, h'] for h' = h - min_up_hours[g] + 1 : h) ≤ is_on[g, h]
for all g, h

This says: if any shutdown has occurred in the last min_up_hours periods, the generator cannot be off now. MangoGridStudio clips the window at the horizon boundary — if the minimum uptime window extends past hour 24, the constraint applies only up to hour 24.

3.5 Minimum downtime

Symmetric constraint for minimum offline periods:

sum(startup[g, h'] for h' = h - min_down_hours[g] + 1 : h) ≤ 1 - is_on[g, h]
for all g, h

Reference minimum times from RTS-GMLC:

GeneratorTypeMinUp (h)MinDown (h)
107_CC_1Gas CC42
113_CT_1Gas CT2.21
101_STEAM_3Coal84
121_NUCLEAR_1Nuclear2448

3.6 Load balance

At every hour, total generation must equal total load (ignoring losses in the DC power flow approximation):

sum(p[g, h] for all generators g) = load[h] for all h

The shadow price (dual variable) of this constraint is the system energy price — the starting point for LMP decomposition. See LMP Decomposition.


4. Objective Function

Minimize total production cost over all generators and hours:

minimize Σ_g Σ_h [
MC[g] × p[g, h] (fuel cost: incremental cost per MWh of output)
+ NLC[g] × is_on[g, h] (no-load cost: paid every hour the unit is online)
+ startup_cost[g] × startup[g, h] (startup cost: paid once at each startup event)
]

Where:

  • MC[g] = marginal cost ($/MWh): incremental fuel cost per MWh produced above Pmin
  • NLC[g] = no-load cost ($/h): fuel burned keeping the unit in standby at Pmin
  • startup_cost[g] = cold-start cost ($): one-time cost at each commitment

PJM Manual 28 §4 — Default Energy Offer formula

PJM requires generators to offer at their computed default cost if they do not file their own offer. The formula:

MC [$/MWh] = heat_rate_avg [BTU/kWh] × fuel_price [$/MMBTU] / 1000

Example — 107_CC_1 (Gas Combined Cycle, from RTS-GMLC / PJM M28 §4):

  • heat_rate_avg = 7,222 BTU/kWh
  • fuel_price = $3.887/MMBTU (2017 reference from RTS-GMLC)
  • MC = 7,222 × 3.887 / 1,000 = $28.09/MWh

Startup cost (cold start):

SU_cost = Start_Heat_Cold [MMBTU] × fuel_price [$/MMBTU]
= 7,215 × $3.887
= $28,044

No-load cost:

NLC [$/h] = HR_avg_0 [BTU/kWh] × Pmin [MW] × fuel_price [$/MMBTU] / 1,000

Three-tier startup costs

Different startup costs apply depending on how long the unit has been offline: a warm turbine is cheaper to restart than a cold one. MangoGridStudio models three startup cost tiers (hot, warm, cold) based on how long the unit has been offline.

In the Python payload, pass startup costs as a tiered array:

"startup_costs": [
{"hours_offline": 1, "cost": 2800.0},
{"hours_offline": 8, "cost": 14000.0},
{"hours_offline": 24, "cost": 28044.0}
]

MangoGridStudio picks the applicable tier at each startup event based on the current offline duration tracked in the generator state record.


5. Initial Condition Carry-Over

At the start of the planning horizon (hour 1), each generator has an initial status: how many consecutive hours it has been on or off going into the first period. This determines whether minimum uptime or downtime constraints are still active at hour 1.

MangoGridStudio applies initial conditions by fixing the commitment variable for any generator still subject to a binding minimum uptime or downtime obligation at the start of the horizon. A generator that has been online for 6 hours with min_uptime = 8 must remain online for 2 more hours at the start of the next day. A generator offline for 3 hours with min_downtime = 6 must remain offline for 3 more hours.

End-of-day state — the IC chain record

MangoGridStudio carries generator state forward across days in multi-day runs using an end-of-day state record. The record stores:

  • whether the generator is online or offline at the end of the day
  • how many consecutive hours it has been in that state (positive = hours online, negative = hours offline)
  • final power output at end of day (MW)

This record is used as the initial condition for the next day's solve.

IEEE 118-bus IC extraction

For the IEEE 118-bus test network, IC values come from the JSON field "Initial status (h)":

  • "Initial status (h)" > 0 → generator has been on for that many hours → is_on = 1.0
  • "Initial status (h)" ≤ 0 → generator has been off for abs(h) hours → is_on = 0.0

121_NUCLEAR_1 always has initial_status_h = 24 (must-run). Its cold-start cost (78,978 MMBTU × fuel price) makes shutdown economically impossible.


6. PJM Manual 11 §3 Worked Example

This 2-generator, 1-hour example from PJM's DA-UC methodology shows how the marginal unit sets the system LMP.

System setup:

GeneratorFuelPminPmaxMCNLCSU_cold
G1Coal100 MW300 MW$22/MWh$500/h$2,000
G2Gas CT50 MW150 MW$45/MWh$150/h$800

Load: 350 MW (single hour, no network constraints)

Optimal solution:

  • G1: is_on=1, dispatched at 300 MW (Pmax — fully loaded at lower cost)
  • G2: is_on=1, dispatched at 50 MW (Pmin — minimum stable output)
  • Total generation: 300 + 50 = 350 MW = load (balance satisfied)

Why G2 at Pmin? G1's Pmax is 300 MW. The remaining 50 MW must come from G2. Once committed, G2 must produce at least Pmin = 50 MW. Dispatching G2 above 50 MW would cost $45/MWh per additional MW and is not necessary.

LMP = $45/MWh: If load increased by 1 MW to 351 MW, G2 would have to produce 51 MW — the additional cost is $45/MWh. G2 at Pmin is the marginal unit. G1 is inframarginal (its cost is below the LMP).

Both generators are paid the system LMP = $45/MWh regardless of their individual costs. G1's economic rent: ($45 - $22) × 300 MW = $6,900/h.

Python assertion pattern:

# PJM M11 §3 — 2-generator example
assert result["status"] == "OPTIMAL"
assert abs(result["lmp"]["bus_reference"] - 45.0) < 0.01
assert abs(result["dispatch"]["G2"] - 50.0) < 0.1 # G2 at Pmin
assert abs(result["dispatch"]["G1"] - 300.0) < 0.1 # G1 at Pmax

7. Enhanced UC Pipeline Phases

MangoGridStudio's Enhanced UC pipeline solves the 24-hour DA-UC problem in four phases. Each phase feeds the next.

Phase 1 (P1): Single-day baseline

Goal: Solve a single 24-hour UC problem from scratch. Target: Feasible solution in ≤ 30 seconds on the IEEE 118-bus test network (54 generators, 118 buses, 186 lines). Solver: HiGHS (default open-source solver)

The raw MILP is solved with full branch-and-bound. No warm-start hints are provided. P1 establishes the baseline objective value and validates that the solver produces a physically consistent commitment schedule.

Phase 2 (P2): Warm-start acceleration

Goal: Use an LP relaxation to pre-fix a large fraction of binary variables before the MILP solve, achieving ≥15% solve time reduction. Target: A significant fraction of binary variables resolved before the MILP solve; warm solve materially faster than cold solve.

The LP relaxation identifies commitment decisions that are highly likely to be correct in the MILP solution. Variables that are very close to 0 or 1 in the LP relaxation are fixed before the MILP solve, reducing the search space. See the Glossary for related terminology.

Phase 3 (P3): 7-day rolling backtest

Goal: Validate that IC chaining across days produces feasible, quality solutions throughout a week-long horizon. Target: All 7 days feasible; per-day objective within 2% of independent (no IC chain) solves.

Each day's end-of-day state record (commitment status + consecutive hours per generator) is passed as the initial condition to the next day. This prevents boundary infeasibilities at midnight caused by unresolved minimum uptime or downtime obligations.

Phase 4 (P4): Monthly PCM with Benders decomposition

Goal: Solve a full 30-day Production Cost Model using Generalized Benders Decomposition (GBD). Target: Convergence within a bounded number of iterations; seeded runs achieve ≥15% speedup vs unseeded.

GBD decomposes the monthly problem into:

  • Master problem: unit commitment decisions across all days
  • Subproblems: one per day, economic dispatch LP conditioned on the master's commitment schedule

Subproblem duals (Benders cuts) flow back into the master problem, tightening the lower bound at each iteration. The master problem converges when the gap between upper and lower bounds falls below the tolerance.


8. MangoGridStudio API

Submit a UC solve

POST /api/v1/uc/solve
Authorization: Bearer <token>
Content-Type: application/json

Minimal request body:

{
"network": {
"generators": [
{
"id": "107_CC_1",
"pmin": 170.0,
"pmax": 355.0,
"marginal_cost": 28.09,
"no_load_cost": 1400.0,
"startup_cost": 28044.0,
"ramp_up": 248.4,
"ramp_down": 248.4,
"min_uptime": 4,
"min_downtime": 2,
"initial_status_h": 8
}
],
"load_profile": [350.0, 360.0, 370.0, 380.0, 390.0, 400.0,
410.0, 420.0, 415.0, 410.0, 405.0, 400.0,
395.0, 390.0, 385.0, 380.0, 390.0, 400.0,
410.0, 420.0, 415.0, 405.0, 395.0, 370.0]
},
"config": {
"horizon_h": 24,
"time_limit_s": 120,
"solver": "HiGHS"
}
}

Key request fields:

FieldTypeRequiredDescription
generators[].pminfloat (MW)YesMinimum stable generation
generators[].pmaxfloat (MW)YesNameplate capacity
generators[].marginal_costfloat ($/MWh)YesIncremental fuel cost per MWh
generators[].ramp_upfloat (MW/h)YesAlways MW/h — never MW/min
generators[].min_uptimeint (h)YesMinimum consecutive online hours after startup
generators[].initial_status_hintYesHours online (> 0) or offline (≤ 0) at start of horizon
config.time_limit_sint (s)YesSolver wall-clock timeout

Key response fields:

FieldTypeDescription
statusstring"OPTIMAL", "FEASIBLE", "INFEASIBLE", "TIMEOUT"
objective_valuefloat ($)Total production cost over the horizon
commitment[g][h]int 1Commitment schedule per generator per hour
dispatch[g][h]float (MW)Power output per generator per hour
lmp[h]float ($/MWh)System LMP per hour
solve_time_sfloat (s)Wall-clock solve time

Error codes:

CodeMeaningAction
422Input validation failedCheck detail field for the specific field and constraint
504Solver timeoutIncrease config.time_limit_s or reduce problem size
400Infeasible problemCheck: load > sum(Pmax), Pmin/ramp conflicts, or IC contradictions

Full curl example:

curl -X POST http://localhost:58000/api/v1/uc/solve \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @examples/uc_input_ieee118_24h.json

Expected response time: ~4s for the IEEE 118-bus, 24-hour horizon with HiGHS.


9. Troubleshooting

Status: INFEASIBLE

The most common cause is load exceeding total available generation capacity:

total_pmax = sum(g["pmax"] for g in generators)
peak_load = max(load_profile)
if peak_load > total_pmax:
raise ValueError(
f"Peak load {peak_load:.1f} MW exceeds total Pmax {total_pmax:.1f} MW. "
"Add more generators or reduce load."
)

Other causes:

  • Minimum uptime/downtime constraints conflict with a short horizon (a generator must run longer than the horizon)
  • A generator's pmin exceeds pmax (data entry error)
  • Ramp constraints make load balance infeasible at a specific hour — check that generators can ramp fast enough to track the load profile

Status: TIMEOUT

On the IEEE 118-bus 24-hour horizon, a cold solve with HiGHS completes in 4–8 seconds. If you see 30+ second solves:

  • Verify that ramp_up is not unrealistically tight (very small ramp rates force many binary transitions per hour)
  • Check that min_up_hours and min_down_hours are correct — overly long minimums can cause infeasibility or degenerate the MIP

Ramp rates look wrong

MangoGridStudio expects MW/h. The RTS-GMLC gen.csv stores MW/min. Multiply by 60 before passing to the API. A Gas CC at 4.14 MW/min must be submitted as 248.4 MW/h.

LMP is zero or unrealistically low

Check that marginal_cost is non-zero for all generators. If MC = 0, the optimizer has no cost signal to distinguish cheap from expensive generation and may produce valid but economically meaningless results. Also check that pmin constraints are not forcing excess committed generation beyond load — this can drive the shadow price of the load balance constraint to zero.

IC chain causes infeasibility on Day 2

If a day 2 solve becomes infeasible after chaining from day 1, the most likely cause is a generator initial condition that forces a long must-run or must-off period at the start of day 2, leaving insufficient generation flexibility to serve load. Check that the fixed generators collectively have enough Pmax to serve day 2's load before the must-run period expires.


10. Summary

Unit Commitment is a mixed-integer linear program that decides which generators to commit and how much power each produces, minimizing total cost (marginal energy cost + no-load cost + startup cost) subject to ramp constraints, minimum uptime/downtime sliding windows, and load balance. The binary commitment variables — is_on[g, h], startup[g, h], shutdown[g, h] — are linked by the transition constraint and carry the startup cost signal into the objective.

MangoGridStudio's Enhanced UC pipeline solves this problem in four phases: single-day baseline (≤30s on IEEE 118), LP relaxation warm-start (≥15% speedup), 7-day rolling backtest with IC chaining, and monthly Benders decomposition for Production Cost Model studies.

Next: LMP Decomposition — how bus prices are computed from the UC/ED shadow prices.