Prelim Review and Wrapping up Capacity Expansion


Lecture 13

October 27, 2025

Prelim 1 Review

Prelim 1 Statistics

  • Median: 88% (44/50)
  • Mean: 85% (42.5/50)
  • Standard Dev: 10% (4.8)

Questions

Poll Everywhere QR Code

Text: VSRIKRISH to 22333

URL: https://pollev.com/vsrikrish

See Results

Wrapping up Capacity Expansion

Problem Formulation

\[ \begin{align} \min_{x, y, NSE} \quad & \sum_{g \in \mathcal{G}} \text{FixedCost}_g \times x_g + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \text{VarCost}_g \times y_{g,t} & \\ & \quad + \sum_{t \in \mathcal{T}} \text{NSECost} \times NSE_t & \\[0.5em] \text {subject to:} \quad & \sum_{g \in \mathcal{G}} y_{g,t} + NSE_t \geq d_t \qquad \forall t \in \mathcal{T} \\[0.5em] & y_{g,t} \leq x_g \qquad \qquad \qquad\qquad \forall g \in {G}, \forall t \in \mathcal{T} \\[0.5em] & x_g, y_{g,t}, NSE_t \geq 0 \qquad \qquad \forall g \in {G}, \forall t \in \mathcal{T} \end{align} \]

Problem Data: Generators

Code
gens = DataFrame(CSV.File("data/capacity_expansion/generators.csv"))
gens_display = rename(gens, Symbol.([:Generator, :"Fixed Cost (\\\$)", :"Variable Cost (\\\$/MW)"]))
markdown_table(gens_display[1:end-2, :])
Generator Fixed Cost ($) Variable Cost ($/MW)
Geothermal 450000 0
Coal 220000 21
NG CCGT 82000 25
NG CT 65000 35

Problem Data: Demand

Code
NY_demand = DataFrame(CSV.File("data/capacity_expansion/2020_hourly_load_NY.csv"))
rename!(NY_demand, :"Time Stamp" => :Date)
demand = NY_demand[:, [:Date, :C]]
rename!(demand, :C => :Demand)
@df demand plot(:Date, :Demand, xlabel="Date", ylabel="Demand (MWh)", label=:false, xrot=45, bottom_margin=15mm)
plot!(size=(1200, 500))
Figure 1: Demand for 2020 in NYISO Zone C

Implementation in JUMP.jl

# define sets
G = 1:nrow(gens[1:end-2, :])
T = 1:nrow(demand)
NSECost = 9000

gencap = Model(HiGHS.Optimizer)
# define variables
@variables(gencap, begin
    x[g in G] >= 0
    y[g in G, t in T] >= 0
    NSE[t in T] >= 0
end)
@objective(gencap, Min, 
    sum(gens[G, :FixedCost] .* x) + sum(gens[G, :VarCost] .* sum(y[:, t] for t in T)) + NSECost * sum(NSE)
)
@constraint(gencap, load[t in T], sum(y[:, t]) + NSE[t] >= demand.Demand[t])
@constraint(gencap, availability[g in G, t in T], y[g, t] <= x[g])
optimize!(gencap)

Capacity Expansion Example Solution

Code
generation = zeros(size(G,1))
for i in 1:size(G,1) 
    generation[i] = sum(value.(y)[G[i],:].data) 
end
MWh_share = generation./sum(demand.Demand).*100
cap_share = value.(x).data ./ maximum(demand.Demand) .* 100
results = DataFrame(
    Resource = gens.Plant[G], 
    Capacity = value.(x).data,
    Percent_Cap = cap_share,
    Generated = generation/1000,
    Percent_Gen = MWh_share
)
rename!(results, 
    [:Capacity => :"Installed (MW)", 
    :Percent_Cap => :"Percent  (%)", 
    :Generated => "Generation (GWh)",
    :Percent_Gen => "Percent (%)"])
results[:, 2:end] = round.(results[:, 2:end], digits=1)

NSE_MW = maximum(value.(NSE).data) 
NSE_MWh = sum(value.(NSE).data)
push!(results, ["NSE" NSE_MW NSE_MW/maximum(demand.Demand)*100 NSE_MWh/1000 NSE_MWh/sum(demand.Demand)*100])

results[:, 2:end] = round.(results[:, 2:end], digits=1)
markdown_table(results)
Resource Installed (MW) Percent (%) Generation (GWh) Percent (%)
Geothermal -0.0 -0.0 0.0 0.0
Coal 0.0 0.0 -0.0 -0.0
NG CCGT 2016.9 72.5 15157.7 98.1
NG CT 733.0 26.4 292.2 1.9
NSE 31.2 1.1 0.1 0.0

When Might Generators Operate?

Code
p2 = areaplot(value.(y).data', 
    label=permutedims(gens.Plant), 
    xlabel = "Hour", 
    ylabel ="Generated Electricity (MWh)", 
    color_palette=:seaborn_colorblind,
    grid=:false,
    legend = :bottomleft)
ylims!(p2, (0, 3200))
plot!(p2, size=(1200, 500))
Figure 2: Results of Generating Capacity Expansion Example

How Often Is There Non-Served Energy?

\(\text{NSE}\) is non-zero for 7 hours and a total of 94 MWh.

Why do we think there is any NSE given the high NSE Cost?

What Does This Problem Neglect?

  1. Discrete decisions: is a plant on or off (this is called unit commitment)?
  2. Intertemporal constraints: Power plants can’t just “ramp” from producing low levels of power to high levels of power; there are real engineering limits.
  3. Transmission: We can generate all the power we want, but what if we can’t get it to where the demand is
  4. Retirements: We might have existing generators that we want to retire (“brownfield” scenarios).

What About Renewables?

Renewables make this problem more complicated because their capacity is variable:

  • resource availability not constant across time;
  • need to consider a capacity factor.

How would this change our existing capacity expansion formulation?

Renewable Variability Impact

This will change the capacity constraint from \[y_{g,t} \leq x_g \qquad \forall g \in {G}, \forall t \in \mathcal{T}\] to \[y_{g,t} \leq x_g \times c_{g,t} \qquad \forall g \in {G}, \forall t \in \mathcal{T}\] where \(c_{g,t}\) is the capacity factor in time period \(t\) for generator type \(g\).

Renewable Capacity Factors

  • Linked to resource availability: how much wind, solar, hydro, geothermal, etc can we rely on in a given hour?
  • Often derived from Typical Meteorological Years (TMY) or select Actual Meteorological Years (AMY) and may only use select periods.
  • These approaches will underrepresent renewable variability but may be used for computational tractability.
  • Always look for details on what meteorological dataset was used!

Implementing Constraints in JuMP

I recommend using vector notation in JuMP to specify these constraints, e.g. for capacity:

# define sets
G = 1:num_gen # num_gen is the number of generators
T = 1:num_times # number of time periods

c = ... # G x T matrix of capacity factors  
@variable(..., x[g in G] >= 0) # installed capacity
@variable(..., y[g in G, t in T] >= 0) # generated power
@constraint(..., capacity[g in G, t in T], 
    y[g,t] <= x[g] * c[g,t]) # capacity constraint

Key Takeaways

Key Takeaways

  • We looked at a “greenfield” example: no existing plants.
  • Decision problem becomes more complex with renewables (HW4) or “brownfield” (expanding existing fleet, possibly with retirements).

Upcoming Schedule

Next Classes

Wednesday: Economic Dispatch

Next Week: Mixed-Integer Linear Programming and Applications