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) | |
|---|---|---|
| Horizon | 24 hours (hourly intervals) | 1–4 hours (5–15 min intervals) |
| When run | Day before delivery | Rolling throughout the operating day |
| Purpose | Commit generators, set startup schedule | Adjust committed fleet to actual conditions |
| Key inputs | Load forecast, fuel prices, generator offers | Real-time load, actual outages |
| Output | Commitment schedule + LMP forecast | Adjusted 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:
- 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.
- 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}
gindexes generators (e.g.,"107_CC_1"for a 355 MW Gas CC unit in the RTS-GMLC dataset)hindexes hours in the planning horizon (1 through 24 for a day-ahead solve)is_on[g, h] = 1means generatorgis committed (running or in standby) at hourhis_on[g, h] = 0means generatorgis offline at hourh
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: generatorgstarts up at hourh(was offline ath-1, is online ath)shutdown[g, h] = 1: generatorgshuts down at hourh(was online ath-1, is offline ath)
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_ongoes 0 → 1):startup[g, h] = 1,shutdown[g, h] = 0 - If a unit turns off (
is_ongoes 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:
| Generator | Type | Ramp (MW/min) | Ramp (MW/h — use this) |
|---|---|---|---|
107_CC_1 | Gas CC | 4.14 | 248.4 |
113_CT_1 | Gas CT | 3.70 | 222.0 |
101_STEAM_3 | Coal | 2.00 | 120.0 |
121_NUCLEAR_1 | Nuclear | fixed output | n/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:
| Generator | Type | MinUp (h) | MinDown (h) |
|---|---|---|---|
107_CC_1 | Gas CC | 4 | 2 |
113_CT_1 | Gas CT | 2.2 | 1 |
101_STEAM_3 | Coal | 8 | 4 |
121_NUCLEAR_1 | Nuclear | 24 | 48 |
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 PminNLC[g]= no-load cost ($/h): fuel burned keeping the unit in standby at Pminstartup_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/kWhfuel_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 forabs(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:
| Generator | Fuel | Pmin | Pmax | MC | NLC | SU_cold |
|---|---|---|---|---|---|---|
| G1 | Coal | 100 MW | 300 MW | $22/MWh | $500/h | $2,000 |
| G2 | Gas CT | 50 MW | 150 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:
| Field | Type | Required | Description |
|---|---|---|---|
generators[].pmin | float (MW) | Yes | Minimum stable generation |
generators[].pmax | float (MW) | Yes | Nameplate capacity |
generators[].marginal_cost | float ($/MWh) | Yes | Incremental fuel cost per MWh |
generators[].ramp_up | float (MW/h) | Yes | Always MW/h — never MW/min |
generators[].min_uptime | int (h) | Yes | Minimum consecutive online hours after startup |
generators[].initial_status_h | int | Yes | Hours online (> 0) or offline (≤ 0) at start of horizon |
config.time_limit_s | int (s) | Yes | Solver wall-clock timeout |
Key response fields:
| Field | Type | Description |
|---|---|---|
status | string | "OPTIMAL", "FEASIBLE", "INFEASIBLE", "TIMEOUT" |
objective_value | float ($) | Total production cost over the horizon |
commitment[g][h] | int 1 | Commitment 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_s | float (s) | Wall-clock solve time |
Error codes:
| Code | Meaning | Action |
|---|---|---|
| 422 | Input validation failed | Check detail field for the specific field and constraint |
| 504 | Solver timeout | Increase config.time_limit_s or reduce problem size |
| 400 | Infeasible problem | Check: 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
pminexceedspmax(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_upis not unrealistically tight (very small ramp rates force many binary transitions per hour) - Check that
min_up_hoursandmin_down_hoursare 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.