Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/io/config.jl
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ function make_paths_absolute!(config, filename)
end
make_paths_absolute!(config) = make_paths_absolute!(config, config[:config_file])

_abspath(path, fn::AbstractString) = abspath(path, fn)
_abspath(path, fn::AbstractString) = isempty(fn) ? fn : abspath(path, fn)
_abspath(path, fns::AbstractVector{<:AbstractString}) = map(fn->(isabspath(fn) ? fn : abspath(path, fn)), fns)
"""
contains_file_or_path(s) -> Bool
Expand Down
1 change: 1 addition & 0 deletions src/results/results_formulas.csv
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ bus,distribution_cost_total,"SumHourlyWeighted(plserv, distribution_cost)",Dolla
bus,unserved_load_cost_total, "SumHourlyWeighted(plcurt, voll)",Dollars,Total cost of unserved load.
bus,gs_payment,0,Dollars,Cost of required credits for clean/renewable generation for all generation standards (RPS's and CES's) for the qualifying demand at each given bus.
bus,emission_cost,0,Dollars,Cost for paying all emissions prices for imported energy.
bus,emission_cap_cost,0,Dollars,Cost attributed to imports for all emission caps, allocated to buses.
branch,eflow_total,SumHourlyWeighted(pflow),MWhFlow,Total energy flowing in this branch
branch,pflow_hourly_min,MinHourly(pflow),MWFlow,Minimum sum of power flowing in these branches
branch,pflow_hourly_max,MaxHourly(pflow),MWFlow,Maximum sum of power flowing in these branches
Expand Down
9 changes: 9 additions & 0 deletions src/results/retail_price.jl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ function setup_retail_price!(config, data)
# payments for RPS and CES policies
add_price_term!(data, :avg_elec_rate, :bus, :gs_payment, +)

# cost from imported power under emission caps and emission prices, allocated to the importing
# bus by EmissionCap/EmissionPrice (branch/dc_line have no area columns of their own).
# these results formulas default to 0 if there is no emission cap/price or the mods do not cover imports
add_price_term!(data, :avg_elec_rate, :bus, :emission_cap_cost, +)
add_price_term!(data, :avg_elec_rate, :bus, :emission_cost, +)


# check for price terms and add

# past invest file will overwrite the past invest column of the gen table
if haskey(config, :past_invest_file)
add_price_term!(data, :avg_elec_rate, :past_invest, :cost_of_service_past_costs, +)
Expand Down
50 changes: 48 additions & 2 deletions src/types/policies/EmissionCap.jl
Original file line number Diff line number Diff line change
Expand Up @@ -318,13 +318,18 @@ function add_import_results!(data, table_name, pol::EmissionCap, cols, alw_prc)

# results formula for cost of emission cap policy contributed by imports
add_results_formula!(data, table_name, cols.import_cost, "SumHourlyWeighted($(cols.prc), pflow)*(1-pol.offset)",
Dollars, "The cost of $(pol.name) attributed to imports on $(table_name)")
Dollars, "The cost of $(pol.name) attributed to imports on $(table_name). Import costs have also been allocated to the corresponding busses in the bus table's emission_cap_cost result formula.")
# setup a results formula to track total cost of all emission cap policies for imported power
haskey(get_results_formulas(data), (table_name, :emission_cap_cost)) ||
add_results_formula!(data, table_name, :emission_cap_cost, "0", Dollars,
"Cost attributed to imports for all emission caps on $(table_name)")
"Cost attributed to imports for all emission caps on $(table_name). Import costs have aslo been allocated to the corresponding busses in the bus table's emission_cap_cost result formula")
add_to_results_formula!(data, table_name, :emission_cap_cost, cols.import_cost)

# attribute the import cost to the importing bus, so that it can be aggregated/filtered by
# any area available on the bus table (e.g. state) - branch/dc_line rows span two areas and
# have no area columns of their own.
add_import_cost_to_bus!(data, table_name, pol, cols, prc_col)

# results formula to track associated emissions from imported power
if pol.emis_col == "emis_co2"
unit = ShortTons
Expand All @@ -334,6 +339,47 @@ function add_import_results!(data, table_name, pol::EmissionCap, cols, alw_prc)
add_results_formula!(data, table_name, cols.import_emis_result, "SumHourlyWeighted($(cols.import_emis), (pflow .* $(cols.flag)))", unit, "Total emissions from imported power under $(pol.name). Note the imported emissions are calculated using the exogenous ef inputs and do not reflect the actual ef of the model run.")
end

"""
add_import_cost_to_bus!(data, table_name, pol::EmissionCap, cols, prc_col)

Allocates the per-row import cost computed in [`add_import_results!`](@ref) (using the same
per-row price container `prc_col`) onto the importing bus,which is the endpoint inside the capped
region (`t_bus_idx` when `dir > 0`, `f_bus_idx` when `dir < 0`). This mirrors how branch-level
merchandising surplus is allocated to buses in `parse_lmp_results!`. This is necessary to compute
results like retail price at the state level, since the branch and dc line table have no area
columns.
"""
function add_import_cost_to_bus!(data, table_name, pol::EmissionCap, cols, prc_col)
table = get_table(data, table_name)
bus = get_table(data, :bus)
nyr = get_num_years(data)
nhr = get_num_hours(data)
hour_weights = get_hour_weights(data)

bus_cost_col = Symbol("$(pol.name)_import_cost")
bus_cost_total = Symbol("$(pol.name)_import_cost_total")
if !hasproperty(bus, bus_cost_col)
add_table_col!(data, :bus, bus_cost_col, Container[ByYearAndHour(zeros(nyr, nhr)) for _ in 1:nrow(bus)], Dollars,
"Cost of $(pol.name) attributed to imports, allocated to the importing bus.")
add_results_formula!(data, :bus, bus_cost_total, "SumHourly($(bus_cost_col))", Dollars,
"Total cost of $(pol.name) attributed to imports, allocated to buses.")
haskey(get_results_formulas(data), (:bus, :emission_cap_cost)) ||
add_results_formula!(data, :bus, :emission_cap_cost, "0", Dollars,
"Cost attributed to imports for all emission caps, allocated to buses.")
add_to_results_formula!(data, :bus, :emission_cap_cost, bus_cost_total)
end

for (row_idx, row) in enumerate(eachrow(table))
dir = row[cols.dir]
dir == 0 && continue
bus_idx = dir > 0 ? row[:t_bus_idx] : row[:f_bus_idx]
prc = prc_col[row_idx]
for y in 1:nyr, h in 1:nhr
bus[bus_idx, bus_cost_col][y,h] += hour_weights[h] * prc[y,h] * row[:pflow][y,h] * (1-pol.offset)
end
end
end

"""
tag_import_flows!(table, col::Symbol) ->
Tag rows in branch or dc line tables where power is imported into the EmissionCap region..
Expand Down
47 changes: 47 additions & 0 deletions src/types/policies/EmissionPrice.jl
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ function add_import_results!(data, table_name, pol::EmissionPrice, col::Symbol)
"The total cost of imported emissions for $(table_name).")
add_to_results_formula!(data, table_name, :emission_cost, cols.import_cost)

# attribute the import cost to the importing bus, so that it can be aggregated/filtered by
# any area available on the bus table (e.g. state) - branch/dc_line rows span two areas and
# have no area columns of their own.
add_import_cost_to_bus!(data, table_name, pol, col)

if pol.emis_col == "emis_co2"
unit = ShortTons
else
Expand All @@ -234,6 +239,48 @@ function add_import_results!(data, table_name, pol::EmissionPrice, col::Symbol)
add_results_formula!(data, table_name, cols.import_emis_result, "SumHourlyWeighted($(cols.import_emis), (pflow .* $(cols.flag)))", unit, "Total emissions from imported power under $(pol.name). Note the imported emissions are calculated using the exogenous ef inputs and do not reflect the actual ef of the model run.")
end

"""
add_import_cost_to_bus!(data, table_name, pol::EmissionPrice, col::Symbol)
Allocates the per-row import cost computed in [`add_import_results!`](@ref) (using the same
per-row price container `prc_col`) onto the importing bus,which is the endpoint inside the capped
region (`t_bus_idx` when `dir > 0`, `f_bus_idx` when `dir < 0`). This mirrors how branch-level
merchandising surplus is allocated to buses in `parse_lmp_results!`. This is necessary to compute
results like retail price at the state level, since the branch and dc line table have no area
columns.
"""
function add_import_cost_to_bus!(data, table_name, pol::EmissionPrice, col::Symbol)
table = get_table(data, table_name)
bus = get_table(data, :bus)
cols = _emisprc_colnames(pol, table_name)
nyr = get_num_years(data)
nhr = get_num_hours(data)
hour_weights = get_hour_weights(data)
bus_set = Set(get_row_idxs(bus, parse_comparisons(pol.bus_filters)))

bus_cost_col = Symbol("$(pol.name)_import_cost")
bus_cost_total = Symbol("$(pol.name)_import_cost_total")
if !hasproperty(bus, bus_cost_col)
add_table_col!(data, :bus, bus_cost_col, Container[ByYearAndHour(zeros(nyr, nhr)) for _ in 1:nrow(bus)], Dollars,
"Cost of $(pol.name) attributed to imports, allocated to the importing bus.")
add_results_formula!(data, :bus, bus_cost_total, "SumHourly($(bus_cost_col))", Dollars,
"Total cost of $(pol.name) attributed to imports, allocated to buses.")
haskey(get_results_formulas(data), (:bus, :emission_cost)) ||
add_results_formula!(data, :bus, :emission_cost, "0", Dollars,
"The total cost of imported emissions, allocated to buses.")
add_to_results_formula!(data, :bus, :emission_cost, bus_cost_total)
end

for row in eachrow(table)
t_in = row[:t_bus_idx] in bus_set
f_in = row[:f_bus_idx] in bus_set
t_in == f_in && continue # not a branch/dc_line crossing the priced region boundary
bus_idx = t_in ? row[:t_bus_idx] : row[:f_bus_idx]
for y in 1:nyr, h in 1:nhr
bus[bus_idx, bus_cost_col][y,h] += hour_weights[h] * row[col][y,h] * row[:pflow][y,h] * row[cols.flag][y,h]
end
end
end


"""
tag_import_flows!(table, col::Symbol) ->
Expand Down
6 changes: 6 additions & 0 deletions test/testpoltypes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,9 @@
target_2040 = config[:mods][:example_emiscap_arch][:targets][:y2040]
@test total_emis_2040 <= target_2040 || total_emis_2040 ≈ target_2040

# check that the results formulas on the branch and bus tables are aligned
@test compute_result(data, :branch, :emission_cap_cost) + compute_result(data, :dc_line, :emission_cap_cost) ≈ compute_result(data, :bus, :emission_cap_cost)

end
end

Expand Down Expand Up @@ -762,6 +765,9 @@
@test compute_result(data, :branch, :example_emisprc_arch_import_emis, (:), "y2040") > 0
@test compute_result(data, :dc_line, :example_emisprc_arch_import_emis, (:), "y2040") > 0

# check that the results formulas on the branch and bus tables are aligned
@test compute_result(data, :branch, :emission_cost) + compute_result(data, :dc_line, :emission_cost) ≈ compute_result(data, :bus, :emission_cost)

end
end

Expand Down
2 changes: 1 addition & 1 deletion test/testretailprice.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
#check retail price terms
@test all(k -> haskey(retail_price, k), [:bus, :gen, :storage, :past_invest])
bus_terms = retail_price[:bus]
@test all(k -> haskey(bus_terms, k), [:electricity_cost, :distribution_cost_total, :merchandising_surplus_comp_total, :gs_payment, :state_reserve_cost])
@test all(k -> haskey(bus_terms, k), [:electricity_cost, :distribution_cost_total, :merchandising_surplus_comp_total, :gs_payment, :state_reserve_cost, :emission_cap_cost, :emission_cost])
gen_terms = retail_price[:gen]
@test haskey(gen_terms, :cost_of_service_rebate)
storage_terms = retail_price[:storage]
Expand Down
14 changes: 14 additions & 0 deletions test/testsaveconfig.jl
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,19 @@

newconfig = nothing

@testset "YAML printing of NamedTuple config values" begin
# Modification/Policy fields aren't required to be OrderedDicts - a field could be a
# NamedTuple, which YAML.jl doesn't know how to serialize on its own. E4ST teaches it how
# via a YAML._print(io, ::NamedTuple, ...) override (see config.jl), converting the
# NamedTuple to an OrderedDict before printing. Test that override directly here.
nt = (a = 1, b = "two", c = [1, 2, 3])
io = IOBuffer()
YAML.write(io, nt)
yaml_str = String(take!(io))

reloaded = YAML.load(yaml_str, dicttype = OrderedDict{Symbol, Any})
@test reloaded == OrderedDict(pairs(nt))
end

end

Loading