Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat 105 reserve requirements #254

Merged
merged 6 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/src/results/formulas.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ MaxYearly
SumHourly
SumHourlyWeighted
AverageHourly
AverageHourlyWeighted
MinHourly
MaxHourly
SumMinHourly
SumMaxHourly
CostOfServiceRebate
```
100 changes: 100 additions & 0 deletions src/results/formulas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
# push!(results_formulas_table, (;table_name, result_name, formula, unit, description, dependent_columns, fn))
results_formulas[table_name, result_name] = rf
end
function add_results_formula!(data, table_name, result_name, formula::String, unit::Type{<:Unit}, description::String)
return add_results_formula!(data, table_name |> Symbol, result_name |> Symbol, formula, unit, description)
end
export add_results_formula!

"""
Expand Down Expand Up @@ -479,6 +482,8 @@
minimum(_sum_hourly(v1, idxs, y, h) for h in hr_idxs for y in yr_idxs)
end



@doc raw"""
MaxHourly(cols...) <: Function

Expand All @@ -501,6 +506,62 @@
maximum(_sum_hourly(v1, idxs, y, h) for h in hr_idxs for y in yr_idxs)
end

@doc raw"""
SumMaxHourly(cols...) <: Function

This function returns the sum of each of the maximum hourly values.

```math
\sum_{i \in \text{idxs}} \max_{y \in \text{yr\_idxs}, h \in \text{hr\_idxs}} \prod_{c \in \text{cols}} \text{table}[i, c][y, h]
```
"""
struct SumMaxHourly{N} <: Function
cols::NTuple{N, Symbol}
end
SumMaxHourly(cols::Symbol...) = SumMaxHourly(cols)
export SumMaxHourly

function (f::SumMaxHourly{1})(data, table, idxs, yr_idxs, hr_idxs)
col1, = f.cols
v1 = col_or_container(data, table, col1)
return sum(maximum(_getindex(v1, i, y, h) for h in hr_idxs, y in yr_idxs) for i in idxs)
end

function (f::SumMaxHourly{2})(data, table, idxs, yr_idxs, hr_idxs)
col1, col2 = f.cols
v1 = col_or_container(data, table, col1)
v2 = col_or_container(data, table, col2)
return sum(maximum(_getindex(v1, i, y, h) * _getindex(v2, i, y, h) for h in hr_idxs, y in yr_idxs) for i in idxs)

Check warning on line 534 in src/results/formulas.jl

View check run for this annotation

Codecov / codecov/patch

src/results/formulas.jl#L530-L534

Added lines #L530 - L534 were not covered by tests
end

@doc raw"""
SumMinHourly(cols...) <: Function

This function returns the sum of each of the minimum hourly values.

```math
\sum_{i \in \text{idxs}} \min_{y \in \text{yr\_idxs}, h \in \text{hr\_idxs}} \prod_{c \in \text{cols}} \text{table}[i, c][y, h]
```
"""
struct SumMinHourly{N} <: Function
cols::NTuple{N, Symbol}
end
SumMinHourly(cols::Symbol...) = SumMinHourly(cols)
export SumMinHourly

function (f::SumMinHourly{1})(data, table, idxs, yr_idxs, hr_idxs)
col1, = f.cols
v1 = col_or_container(data, table, col1)
return sum(minimum(_getindex(v1, i, y, h) for h in hr_idxs, y in yr_idxs) for i in idxs)
end

function (f::SumMinHourly{2})(data, table, idxs, yr_idxs, hr_idxs)
col1, col2 = f.cols
v1 = col_or_container(data, table, col1)
v2 = col_or_container(data, table, col2)
return sum(minimum(_getindex(v1, i, y, h) * _getindex(v2, i, y, h) for h in hr_idxs, y in yr_idxs) for i in idxs)

Check warning on line 562 in src/results/formulas.jl

View check run for this annotation

Codecov / codecov/patch

src/results/formulas.jl#L558-L562

Added lines #L558 - L562 were not covered by tests
end

"""
MaxValue()

Expand Down Expand Up @@ -638,6 +699,45 @@
end



@doc raw"""
AverageHourlyWeighted(cols...) <: Function

Function used in results formulas. Computes the sum of the products of the columns for each index in idxs for each year and hour weighted by the number of hours, divided by the total number of hours.

```math
\frac{\sum_{i \in \text{idxs}} \sum_{y \in \text{yr\_idxs}} \sum_{h \in \text{hr\_idxs}} \prod_{c \in \text{cols}} \text{table}[i, c][y]}{\sum_{y \in \text{yr\_idxs}}\sum{h \in \text{hr\_idxs}} w_{h}}
```
"""
struct AverageHourlyWeighted{N} <: Function
cols::NTuple{N, Symbol}
end
AverageHourlyWeighted(cols::Symbol...) = AverageHourlyWeighted(cols)
export AverageHourlyWeighted

function (f::AverageHourlyWeighted{1})(data, table, idxs, yr_idxs, hr_idxs)
col1, = f.cols
hour_weights = get_hour_weights(data)
hc = data[:hours_container]::HoursContainer
_sum_hourly(col_or_container(data, table, col1), hc, idxs, yr_idxs, hr_idxs) / sum(hour_weights[hr_idx] for hr_idx in hr_idxs, yr_idx in yr_idxs)
end

function (f::AverageHourlyWeighted{2})(data, table, idxs, yr_idxs, hr_idxs)
col1,col2 = f.cols
hour_weights = get_hour_weights(data)
hc = data[:hours_container]::HoursContainer
_sum_hourly(col_or_container(data, table, col1), col_or_container(data, table, col2), hc, idxs, yr_idxs, hr_idxs) / sum(hour_weights[hr_idx] for hr_idx in hr_idxs, yr_idx in yr_idxs)
end

function (f::AverageHourlyWeighted{3})(data, table, idxs, yr_idxs, hr_idxs)
col1,col2,col3 = f.cols
hour_weights = get_hour_weights(data)
hc = data[:hours_container]::HoursContainer
_sum_hourly(col_or_container(data, table, col1), col_or_container(data, table, col2), col_or_container(data, table, col3), hc, idxs, yr_idxs, hr_idxs) / sum(hour_weights[hr_idx] for hr_idx in hr_idxs, yr_idx in yr_idxs)

Check warning on line 736 in src/results/formulas.jl

View check run for this annotation

Codecov / codecov/patch

src/results/formulas.jl#L732-L736

Added lines #L732 - L736 were not covered by tests
end



"""
CostOfServiceRebate(table_name) <: Function

Expand Down
2 changes: 1 addition & 1 deletion src/results/results_formulas.csv
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
table_name,result_name,formula,unit,description
gen,egen_total,SumHourlyWeighted(pgen),MWhGenerated,"Total energy generated, in MWh"
gen,pgen_avg,AverageHourly(pgen),MWGenerated,Average power generation
gen,pgen_avg,AverageHourlyWeighted(pgen),MWGenerated,Average power generation
gen,pgen_min,MinHourly(pgen),MWGenerated,Minimum power generation in an hour
gen,pgen_max,MaxHourly(pgen),MWGenerated,Maximum power generation in an hour
gen,ecap_total,SumHourlyWeighted(pcap),MWhCapacity,"Total energy capacity, in MWh. This is equal to the power generation capacity multiplied by the number of hours."
Expand Down
11 changes: 10 additions & 1 deletion src/types/modifications/AggregationTemplate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
if col_to_expand == :filter_hours
area = row.filter_hours
hours_table_col = get_table_col(data, :hours, area)
subareas = sort!(unique(hours_table_col))
subareas = Base.sort!(String.(string.(unique(hours_table_col))), by=hours_sortby)
else
area = row[col_to_expand]
data_table_col = get_table_col(data, row.table_name, area)
Expand Down Expand Up @@ -89,6 +89,15 @@
return
end

function hours_sortby(s::T) where T
if endswith(s, r"h\d+")
m = match(r"h(\d+)", s)
return lpad(m.captures[1], 4, '0') |> T

Check warning on line 95 in src/types/modifications/AggregationTemplate.jl

View check run for this annotation

Codecov / codecov/patch

src/types/modifications/AggregationTemplate.jl#L94-L95

Added lines #L94 - L95 were not covered by tests
else
return s |> T
end
end

function extract_results(m::AggregationTemplate, config, data)
results = get_results(data)
haskey(results, m.name) || modify_results!(m, config, data)
Expand Down
2 changes: 1 addition & 1 deletion src/types/modifications/CO2eCalc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function modify_results!(mod::CO2eCalc, config, data)
add_results_formula!(data, :gen, :emis_upstream_ch4_rate, "emis_upstream_ch4_total/egen_total", ShortTonsPerMWhGenerated, "Average rate of upstream methane emissions")
if haskey(data, :dam_co2)
add_results_formula!(data, :gen, :climate_damages_co2e_total, "SumHourlyWeighted(emis_co2e, pgen, dam_co2)", Dollars, "Total climate damages from CO2e")
add_results_formula!(data, :gen, :climate_damages_co2e_per_mwh, "climate_damages_total / egen_total", Dollars, "Climate damages from CO2e, per MWh of power generation")
add_results_formula!(data, :gen, :climate_damages_co2e_per_mwh, "climate_damages_co2e_total / egen_total", Dollars, "Climate damages from CO2e, per MWh of power generation")
add_welfare_term!(data, :climate, :gen, :climate_damages_co2e_total, -)
else
@warn "CO2eCalc found no damages rate `dam_co2` inside `data`, not adding results formulas and welfare terms for climate damages"
Expand Down
2 changes: 1 addition & 1 deletion src/types/modifications/InterfaceLimit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ function modify_results!(mod::InterfaceLimit, config, data)
add_table_col!(data, :interface_limit, :eflow, eflow_if, MWhFlow, "MWh of power flowing from `f` to `t` in each weighted representative hour.")
add_results_formula!(data, :interface_limit, :pflow_if_max, "MaxHourly(pflow)", MWFlow, "Maximum net hourly directional flow")
add_results_formula!(data, :interface_limit, :pflow_if_min, "MinHourly(pflow)", MWFlow, "Minimum net hourly directional flow")
add_results_formula!(data, :interface_limit, :pflow_if_avg, "AverageHourly(pflow)", MWFlow, "Average net hourly directional flow")
add_results_formula!(data, :interface_limit, :pflow_if_avg, "AverageHourlyWeighted(pflow)", MWFlow, "Average net hourly directional flow")
add_results_formula!(data, :interface_limit, :eflow_if_total, "SumHourlyWeighted(pflow)", MWhFlow, "Total net MWh of energy flow across the interface")
add_results_formula!(data, :interface_limit, :pflow_line_max, "MaxSingleLineHourly()", MWFlow, "Maximum net hourly directional flow for a single line")

Expand Down
43 changes: 37 additions & 6 deletions src/types/modifications/ReserveRequirement.jl
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ function modify_model!(mod::ReserveRequirement, config, data, model)

credits_stor = Container[ByNothing(0.0) for i in 1:nrow(stor)]
add_table_col!(data, :storage, mod.name, credits_stor, CreditsPerMWCapacity,
"Credit level for reserve requirement $(mod.name). This gets multiplied by the power capacity in the reserve requirement constraint.")
"Credit level for reserve requirement $(mod.name). This gets multiplied by the power capacity in the reserve requirement constraint.")

for stor_idx in axes(stor, 1)
credits_stor[stor_idx] = Container(get_credit(c_stor, data, stor[stor_idx, :]))
Expand Down Expand Up @@ -342,6 +342,32 @@ function modify_results!(mod::ReserveRequirement, config, data)
pres_req_bus = get_raw_result(data, Symbol("pres_req_bus_$(mod.name)"))::Array{Float64, 3} # nbus x nyr x nhr
add_table_col!(data, :bus, pres_req_name, pres_req_bus, MWReserve, "The required reserve power in from $(mod.name), in MW.")

### Pull out and add results to the requirements table
add_table_col!(data, Symbol("$(mod.name)_requirements"), :price, hourly_price_per_credit, DollarsPerMWCapacityPerHour, "Shadow price on the capacity constraint, in dollars per MW reserve capacity supplied per hour")
pres_supply_subarea = get_raw_result(data, Symbol("pres_total_subarea_$(mod.name)"))::Array{Float64, 3}
add_table_col!(data, Symbol("$(mod.name)_requirements"), :pres_supply, pres_supply_subarea, MWCapacity, "Supplied reserve capacity in the region")
pres_req_subarea = get_raw_result(data, Symbol("pres_req_subarea_$(mod.name)"))::Array{Float64, 3}
add_table_col!(data, Symbol("$(mod.name)_requirements"), :pres_req, pres_req_subarea, MWReserve, "Required reserve power in the region")
pres_flow_subarea = get_raw_result(data, Symbol("pres_flow_subarea_$(mod.name)"))::Array{Float64, 3}
add_table_col!(data, Symbol("$(mod.name)_requirements"), :pres_flow, pres_flow_subarea, MWReserve, "Reserve power capacity flowing out of the region")

# Add results formulas for aggregating supply and demand of reserve requirements by region
add_results_formula!(data, "$(mod.name)_requirements", :pres_req_max, "MaxHourly(pres_req)", MWReserve, "Maximum hourly required reserve power for the region")
add_results_formula!(data, "$(mod.name)_requirements", :pres_req_min, "MinHourly(pres_req)", MWReserve, "Minimum hourly required reserve power for the region")
add_results_formula!(data, "$(mod.name)_requirements", :pres_req_sum_max, "SumMaxHourly(pres_req)", MWReserve, "Sum of maximum hourly required reserve power for each region")
add_results_formula!(data, "$(mod.name)_requirements", :pres_req_sum_min, "SumMinHourly(pres_req)", MWReserve, "Sum of minimum hourly required reserve power for each region")
add_results_formula!(data, "$(mod.name)_requirements", :pres_supply_max, "MaxHourly(pres_supply)", MWReserve, "Maximum hourly supplied reserve power for the region")
add_results_formula!(data, "$(mod.name)_requirements", :pres_supply_min, "MinHourly(pres_supply)", MWReserve, "Minimum hourly supplied reserve power for the region")
add_results_formula!(data, "$(mod.name)_requirements", :pres_supply_sum_max, "SumMaxHourly(pres_supply)", MWReserve, "Sum of maximum hourly supplied reserve power for each region")
add_results_formula!(data, "$(mod.name)_requirements", :pres_supply_sum_min, "SumMinHourly(pres_supply)", MWReserve, "Sum of minimum hourly supplied reserve power for each region")
add_results_formula!(data, "$(mod.name)_requirements", :eres_supply, "SumHourlyWeighted(pres_supply)", MWhCapacity, "Reserve energy supplied. Could be a misleading result, but useful for calculations.")
add_results_formula!(data, "$(mod.name)_requirements", :cost_total, "SumHourlyWeighted(price, pres_supply)", DollarsPerMWCapacityPerHour, "Cost paid for the supplied reserve power.")
add_results_formula!(data, "$(mod.name)_requirements", :price_avg, "cost_total / eres_supply", DollarsPerMWCapacityPerHour, "Shadow price on the capacity constraint, in dollars per MW reserve capacity supplied per hour")
add_results_formula!(data, "$(mod.name)_requirements", :pres_flow_max, "MaxHourly(pres_flow)", MWReserve, "Maximum hourly power flowing out of the region. Note this could be misleading when aggregating above the subarea level")
add_results_formula!(data, "$(mod.name)_requirements", :pres_flow_min, "MinHourly(pres_flow)", MWReserve, "Minimum hourly power flowing out of the region. Note this could be misleading when aggregating above the subarea level")



# Make a gen table column the price for bus and gen tables
price_per_mw_per_hr_gen = Container[ByNothing(0.0) for _ in axes(gen,1)]
price_per_mw_per_hr_bus = Container[ByNothing(0.0) for _ in axes(bus,1)]
Expand All @@ -363,14 +389,18 @@ function modify_results!(mod::ReserveRequirement, config, data)
end

add_table_col!(data, :gen, pres_name, pres_gen, MWCapacity, "The power reserve capacity used to fill $(mod.name)")
add_table_col!(data, :gen, rebate_col_name, price_per_mw_per_hr_gen, DollarsPerMWCapacityPerHour, "This is the rebate recieved by EGU's for each qualifying MW of reserves for each hour from the $(mod.name) reserve requirement.")
add_table_col!(data, :gen, rebate_col_name, price_per_mw_per_hr_gen, DollarsPerMWCapacityPerHour, "This is the rebate recieved by EGU's for each MW of capacity for each hour from the $(mod.name) reserve requirement.")
add_table_col!(data, :bus, cost_col_name, price_per_mw_per_hr_bus, DollarsPerMWCapacityPerHour, "This is the rebate payed by users to EGU's for each MW of demand for each hour from the $(mod.name) reserve requirement.")

# Make a results formula
add_results_formula!(data, :gen, rebate_result_name, "SumHourlyWeighted(pcap, $rebate_col_name)", Dollars, "This is the total rebate recieved by EGU's from the $(mod.name) reserve requirement.")
# Make results formulas for bus table
add_results_formula!(data, :bus, cost_result_name, "SumHourlyWeighted($pres_req_name, $cost_col_name)", Dollars, "This is the total rebate paid by users to EGU's from the $(mod.name) reserve requirement, not including merchandising surplus.")
add_results_formula!(data, :gen, rebate_price_result_name, "$(rebate_result_name)/pcap_total",DollarsPerMWCapacity, "The per MW price of the rebate receive by EGU's from the $(mod.name) reserve requirement.")
add_results_formula!(data, :bus, "pres_req_sum_max_$(mod.name)", "SumMaxHourly($pres_req_name)", MWCapacity, "The sum of hourly maximum power capacity required at each of the buses provided.")

# Make results formulas for the gen table
add_results_formula!(data, :gen, rebate_result_name, "SumHourlyWeighted(pcap, $rebate_col_name)", Dollars, "This is the total rebate recieved by EGU's from the $(mod.name) reserve requirement.")
add_results_formula!(data, :gen, "pcap_qual_$(mod.name)", "AverageHourlyWeighted(pcap, $(mod.name))", MWCapacity, "Hourly-weighted average capacity that qualifies for the $(mod.name)")
add_results_formula!(data, :gen, rebate_price_result_name, "$(rebate_result_name)/pcap_qual_$(mod.name)",DollarsPerMWCapacity, "The per MW of qualifying capacity price of the rebate receive by EGU's from the $(mod.name) reserve requirement.")

# Add it to net_total_revenue_prelim
add_to_results_formula!(data, :gen, :net_total_revenue_prelim, "+ $rebate_result_name")

Expand Down Expand Up @@ -404,7 +434,8 @@ function modify_results!(mod::ReserveRequirement, config, data)

# Make a results formula
add_results_formula!(data, :storage, rebate_result_name, "SumHourlyWeighted(pcap, $rebate_col_name)", Dollars, "This is the total rebate recieved by storage facilities from the $(mod.name) reserve requirement.")
add_results_formula!(data, :storage, rebate_price_result_name, "$(rebate_result_name)/pcap_total",DollarsPerMWCapacity, "The per MW price of the rebate receive by EGU's from the $(mod.name) reserve requirement.")
add_results_formula!(data, :storage, "pcap_qual_$(mod.name)", "AverageHourlyWeighted(pcap, $(mod.name))", MWCapacity, "Hourly-weighted average capacity that qualifies for the $(mod.name)")
add_results_formula!(data, :storage, rebate_price_result_name, "$(rebate_result_name)/pcap_qual_$(mod.name)",DollarsPerMWCapacity, "The per MW price of the rebate receive by EGU's from the $(mod.name) reserve requirement.")

# Add it to net_total_revenue_prelim
add_to_results_formula!(data, :storage, :net_total_revenue_prelim, "+ $rebate_result_name")
Expand Down
7 changes: 7 additions & 0 deletions test/testoptimizemodel.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@
sum(compute_result(data, :gen, :emis_co2e_total, :, yr_idx) * dam_co2[yr_idx,:] for yr_idx in 1:get_num_years(data))

end

@testset "Test pgen calculations" begin
pgen_min = compute_result(data, :gen, :pgen_min)
pgen_max = compute_result(data, :gen, :pgen_max)
pgen_avg = compute_result(data, :gen, :pgen_avg)
@test pgen_min < pgen_avg < pgen_max
end

@testset "Test DC lines" begin
res_raw = get_raw_results(data)
Expand Down
4 changes: 4 additions & 0 deletions test/testpoltypes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@
@test compute_result(data, :gen, :state_reserve_rebate, :, "y2030") == 0.0
@test compute_result(data, :gen, :state_reserve_rebate) > 0.0
@test compute_result(data, :gen, :state_reserve_rebate_per_mw_price) > 0.0
@test compute_result(data, :gen, :pcap_qual_state_reserve) < compute_result(data, :gen, :pcap_total)


if compute_result(data, :storage, :edischarge_total, :nation=>"narnia") > 0
Expand All @@ -515,6 +516,9 @@
@test compute_result(data, :storage, :state_reserve_rebate_per_mw_price) > 0.0
end

@test compute_result(data, :state_reserve_requirements, :pres_req_sum_min) <= compute_result(data, :state_reserve_requirements, :pres_req_min)
@test compute_result(data, :state_reserve_requirements, :pres_req_sum_max) >= compute_result(data, :state_reserve_requirements, :pres_req_max)

@test haskey(data[:results][:raw], :pres_flow_subarea_state_reserve)

@test compute_result(data, :bus, :state_reserve_merchandising_surplus_total) > 0.0
Expand Down
Loading