From 32b3836daa995af78f14c16c1e86d5ed7aa011b3 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Wed, 22 Nov 2023 12:15:29 -0500 Subject: [PATCH] Add method to calculate locational marginal prices (#582) --- src/write_outputs/write_charging_cost.jl | 16 +++++++++------- src/write_outputs/write_energy_revenue.jl | 9 +++++---- src/write_outputs/write_price.jl | 22 ++++++++++++++++++++-- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/write_outputs/write_charging_cost.jl b/src/write_outputs/write_charging_cost.jl index 46c79f7969..3f87f64549 100644 --- a/src/write_outputs/write_charging_cost.jl +++ b/src/write_outputs/write_charging_cost.jl @@ -7,25 +7,27 @@ function write_charging_cost(path::AbstractString, inputs::Dict, setup::Dict, EP ELECTROLYZER = inputs["ELECTROLYZER"] VRE_STOR = inputs["VRE_STOR"] VS_STOR = !isempty(VRE_STOR) ? inputs["VS_STOR"] : [] - + + price = locational_marginal_price(EP, inputs, setup) + dfChargingcost = DataFrame(Region = dfGen[!, :region], Resource = inputs["RESOURCES"], Zone = dfGen[!, :Zone], Cluster = dfGen[!, :cluster], AnnualSum = Array{Float64}(undef, G),) chargecost = zeros(G, T) if !isempty(STOR_ALL) - chargecost[STOR_ALL, :] .= (value.(EP[:vCHARGE][STOR_ALL, :]).data) .* transpose(dual.(EP[:cPowerBalance]) ./ inputs["omega"])[dfGen[STOR_ALL, :Zone], :] + chargecost[STOR_ALL, :] .= (value.(EP[:vCHARGE][STOR_ALL, :]).data) .* transpose(price)[dfGen[STOR_ALL, :Zone], :] end if !isempty(FLEX) - chargecost[FLEX, :] .= value.(EP[:vP][FLEX, :]) .* transpose(dual.(EP[:cPowerBalance]) ./ inputs["omega"])[dfGen[FLEX, :Zone], :] + chargecost[FLEX, :] .= value.(EP[:vP][FLEX, :]) .* transpose(price)[dfGen[FLEX, :Zone], :] end if !isempty(ELECTROLYZER) - chargecost[ELECTROLYZER, :] .= (value.(EP[:vUSE][ELECTROLYZER, :]).data) .* transpose(dual.(EP[:cPowerBalance]) ./ inputs["omega"])[dfGen[ELECTROLYZER, :Zone], :] + chargecost[ELECTROLYZER, :] .= (value.(EP[:vUSE][ELECTROLYZER, :]).data) .* transpose(price)[dfGen[ELECTROLYZER, :Zone], :] end if !isempty(VS_STOR) - chargecost[VS_STOR, :] .= value.(EP[:vCHARGE_VRE_STOR][VS_STOR, :].data) .* transpose(dual.(EP[:cPowerBalance]) ./ inputs["omega"])[dfGen[VS_STOR, :Zone], :] + chargecost[VS_STOR, :] .= value.(EP[:vCHARGE_VRE_STOR][VS_STOR, :].data) .* transpose(price)[dfGen[VS_STOR, :Zone], :] end if setup["ParameterScale"] == 1 - chargecost *= ModelScalingFactor^2 + chargecost *= ModelScalingFactor end dfChargingcost.AnnualSum .= chargecost * inputs["omega"] - CSV.write(joinpath(path, "ChargingCost.csv"), dfChargingcost) + write_simple_csv(joinpath(path, "ChargingCost.csv"), dfChargingcost) return dfChargingcost end diff --git a/src/write_outputs/write_energy_revenue.jl b/src/write_outputs/write_energy_revenue.jl index d566d3c79c..f53abbaa5b 100644 --- a/src/write_outputs/write_energy_revenue.jl +++ b/src/write_outputs/write_energy_revenue.jl @@ -11,14 +11,15 @@ function write_energy_revenue(path::AbstractString, inputs::Dict, setup::Dict, E NONFLEX = setdiff(collect(1:G), FLEX) dfEnergyRevenue = DataFrame(Region = dfGen.region, Resource = inputs["RESOURCES"], Zone = dfGen.Zone, Cluster = dfGen.cluster, AnnualSum = Array{Float64}(undef, G),) energyrevenue = zeros(G, T) - energyrevenue[NONFLEX, :] = value.(EP[:vP][NONFLEX, :]) .* transpose(dual.(EP[:cPowerBalance]) ./ inputs["omega"])[dfGen[NONFLEX, :Zone], :] + price = locational_marginal_price(EP, inputs, setup) + energyrevenue[NONFLEX, :] = value.(EP[:vP][NONFLEX, :]) .* transpose(price)[dfGen[NONFLEX, :Zone], :] if !isempty(FLEX) - energyrevenue[FLEX, :] = value.(EP[:vCHARGE_FLEX][FLEX, :]).data .* transpose(dual.(EP[:cPowerBalance]) ./ inputs["omega"])[dfGen[FLEX, :Zone], :] + energyrevenue[FLEX, :] = value.(EP[:vCHARGE_FLEX][FLEX, :]).data .* transpose(price)[dfGen[FLEX, :Zone], :] end if setup["ParameterScale"] == 1 - energyrevenue *= ModelScalingFactor^2 + energyrevenue *= ModelScalingFactor end dfEnergyRevenue.AnnualSum .= energyrevenue * inputs["omega"] - CSV.write(joinpath(path, "EnergyRevenue.csv"), dfEnergyRevenue) + write_simple_csv(joinpath(path, "EnergyRevenue.csv"), dfEnergyRevenue) return dfEnergyRevenue end diff --git a/src/write_outputs/write_price.jl b/src/write_outputs/write_price.jl index 1228744918..0a2f743ff9 100644 --- a/src/write_outputs/write_price.jl +++ b/src/write_outputs/write_price.jl @@ -10,9 +10,9 @@ function write_price(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) ## Extract dual variables of constraints # Electricity price: Dual variable of hourly power balance constraint = hourly price dfPrice = DataFrame(Zone = 1:Z) # The unit is $/MWh - scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 # Dividing dual variable for each hour with corresponding hourly weight to retrieve marginal cost of generation - dfPrice = hcat(dfPrice, DataFrame(transpose(dual.(EP[:cPowerBalance])./inputs["omega"]*scale_factor), :auto)) + price = locational_marginal_price(EP, inputs, setup) + dfPrice = hcat(dfPrice, DataFrame(transpose(price), :auto)) auxNew_Names=[Symbol("Zone");[Symbol("t$t") for t in 1:T]] rename!(dfPrice,auxNew_Names) @@ -21,3 +21,21 @@ function write_price(path::AbstractString, inputs::Dict, setup::Dict, EP::Model) CSV.write(joinpath(path, "prices.csv"), dftranspose(dfPrice, false), writeheader=false) return dfPrice end + +@doc raw""" + locational_marginal_price(EP::Model, inputs::Dict, setup::Dict) + +Marginal electricity price for each model zone and time step. +This is equal to the dual variable of the power balance constraint. +When solving a linear program (i.e. linearized unit commitment or economic dispatch) +this output is always available; when solving a mixed integer linear program, this can +be calculated only if `WriteShadowPrices` is activated. + + Returns a matrix of size (T, Z). + Values have units of $/MWh +""" +function locational_marginal_price(EP::Model, inputs::Dict, setup::Dict)::Matrix{Float64} + ω = inputs["omega"] + scale_factor = setup["ParameterScale"] == 1 ? ModelScalingFactor : 1 + return dual.(EP[:cPowerBalance]) ./ ω * scale_factor +end