Code
| Generator | Fixed Cost ($) | Variable Cost ($/MW) |
|---|---|---|
| Geothermal | 450000 | 0 |
| Coal | 220000 | 21 |
| NG CCGT | 82000 | 25 |
| NG CT | 65000 | 35 |
Lecture 13
October 27, 2025

Text: VSRIKRISH to 22333
\[ \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} \]
| Generator | Fixed Cost ($) | Variable Cost ($/MW) |
|---|---|---|
| Geothermal | 450000 | 0 |
| Coal | 220000 | 21 |
| NG CCGT | 82000 | 25 |
| NG CT | 65000 | 35 |
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))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)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 |
\(\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?
Renewables make this problem more complicated because their capacity is variable:
How would this change our existing capacity expansion formulation?
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\).
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 constraintWednesday: Economic Dispatch
Next Week: Mixed-Integer Linear Programming and Applications