Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8c1a0be
Corrects distribution cost description
mckennapep Sep 17, 2025
9645839
Sets up option for retail price
mckennapep Sep 17, 2025
4d4948a
Sets up generic ResultsTemplate method for cross-table results
mckennapep Sep 18, 2025
86a1bad
Adjusts retail rate set up for COS regions
mckennapep Sep 29, 2025
a05cf66
Adds past_invest option to config file to work with aggregated gen model
mckennapep Oct 9, 2025
a9a0e37
Adjustments so that past_invest_cost_total can be included in retail …
mckennapep Oct 9, 2025
f749cdd
Moves get_cross_table function to correct location
mckennapep Oct 10, 2025
326f07f
Moves get_cross_table function to the correct location
mckennapep Oct 10, 2025
6cf66c4
Option to add calibrator values to retail price calculation set up
mckennapep Oct 10, 2025
2b8ed3b
Sets up mod that calcultes the calibrator values for retail price
mckennapep Oct 10, 2025
1fdb8cf
Moves helpful functions in ResultsTemplate to util
mckennapep Oct 15, 2025
0e3d93d
Creates a RetailPrice mod that has option to calculate calibrator val…
mckennapep Oct 15, 2025
a6e601d
Adjusts kwargs for retail price calibrator
mckennapep Oct 15, 2025
4ae6c4f
Warns if retail price row does not have corresponding calibrator
mckennapep Oct 15, 2025
e03be1b
Specialized methods for different calibraton modes
mckennapep Oct 15, 2025
6cfb9de
Option to apply ref value to all years
mckennapep Oct 15, 2025
113db58
Second calibration step to ensure average price for larger region mat…
mckennapep Oct 15, 2025
eafd974
Removes a previous version of retail price mod
mckennapep Oct 16, 2025
48aafee
Updates doc string, clean up and comment code
mckennapep Oct 16, 2025
4b7033b
Minor fixes for past invest calculation so that results doesn't error…
mckennapep Oct 17, 2025
d1c700f
Fixes bug in how cal value calculation handles reference values witho…
mckennapep Oct 17, 2025
d9966bc
Removes incorrect calibration mod from e4st.jl
mckennapep Oct 21, 2025
fb8cac6
Adjusts how mode and input files are handled in mod
mckennapep Nov 3, 2025
63fcce4
Updates calculation of generation standard payments
mckennapep Nov 3, 2025
3f15a0c
Fix bugs that were found in tests
mckennapep Nov 11, 2025
3aed8a5
Set up tests for retail price mode
mckennapep Nov 11, 2025
31e185d
Adjusts payment calculation for when a year doesn't have a target
mckennapep Nov 12, 2025
7d7fd61
Delete one of two retail price includes in runtests
mckennapep Nov 12, 2025
ea6a0ad
Fixes so that resreq is added with any name and code cov improves
mckennapep Nov 12, 2025
73cd20a
Retail price calc updated so that merchandising surplus is not distri…
mckennapep Nov 12, 2025
8a6e5eb
Includes comments in retail price
mckennapep Nov 12, 2025
cdd3941
Add to docs of RetailPrice mod
mckennapep Nov 12, 2025
243ce17
Fix bug in test
mckennapep Nov 12, 2025
d2dd0e5
Fixes for get_cal_values mode with multi-year reference prices
mckennapep Nov 17, 2025
d76d165
Merge remote-tracking branch 'origin/main' into feat-167-rtl-prc
mckennapep Dec 22, 2025
a3f40e2
Merge remote-tracking branch 'origin/main' into feat-167-rtl-prc
mckennapep Jan 5, 2026
6ac56f9
Fixes calibrator output table when area is empty, and improves comments
mckennapep Jan 5, 2026
c5a221f
Merge branch 'main' into feat-167-rtl-prc
mckennapep Jul 27, 2026
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 src/E4ST.jl
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ include("types/modifications/GenHashID.jl")
include("types/modifications/LeftJoinCols.jl")
include("types/modifications/CapacityConstraint.jl")
include("types/modifications/PerfectForesight.jl")
# include("types/modifications/RetailPriceCalibration.jl")
include("types/modifications/RetailPrice.jl")

# Include Policies
include("types/policies/ITC.jl")
Expand Down Expand Up @@ -106,6 +108,7 @@ include("results/process.jl")
include("results/aggregate.jl")
include("results/welfare.jl")
include("results/util.jl")
include("results/retail_price.jl")

# Include postprocessing
include("post/post.jl")
Expand Down
1 change: 1 addition & 0 deletions src/io/config.jl
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ function summarize_config()
(:require_optimal, false, true, "Whether or not to require whether or not the model is solved to optimality. If set to true and the optimizer terminates with a suboptimal termination status, [`run_e4st`](@ref) returns after optimizing, without parsing results, etc."),
(:model_string_names, false, false, "Whether or not to allow the model to have string names. Defaults to `false` for memory savings. Can be helpful to turn on for debugging, especially if you are encountering an infeasible model"),
(:yearly_objective_scalars, false, 1, "The amount to scale the objective by for each year, defaults to 1 for each year."),
(:past_invest_file, false, nothing, "File used to calculate past investment costs for existing generators. Only necessary when an aggregated gen table is used which would distort the past investment cost calculation.")

)

Expand Down
157 changes: 146 additions & 11 deletions src/io/data.jl
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function read_data!(config, data)
promote_cols!(data)
setup_results_formulas!(config, data)
setup_welfare!(config, data)
setup_retail_price!(config, data)

# Save the data to file as specified.
if get(config, :save_data, true)
Expand Down Expand Up @@ -84,6 +85,7 @@ function read_data_files!(config, data)
read_table!(config, data, :load_add_file=>:load_add, optional=true)
read_table!(config, data, :build_gen_file => :build_gen, optional=true)
read_table!(config, data, :gentype_genfuel_file => :genfuel, optional=true)
read_table!(config, data, :past_invest_file => :past_invest, optional=true)
end
export read_data_files!

Expand Down Expand Up @@ -179,6 +181,7 @@ function setup_data!(config, data)
setup_table!(config, data, :nominal_load)
setup_table!(config, data, :gen) # needs to come after build_gen setup for newgens
setup_table!(config, data, :af_table)
setup_table!(config, data, :past_invest)
end
export setup_data!

Expand Down Expand Up @@ -518,17 +521,19 @@ function setup_table!(config, data, ::Val{:gen})
z = Container(0.0)
to_container!(gen, :past_invest_cost)
to_container!(gen, :past_invest_subsidy)
for (idx_g, g) in enumerate(eachrow(gen))
if g.build_status == "unbuilt"
if any(!=(0), g.past_invest_cost) || any(!=(0), g.past_invest_subsidy)
@warn "Generator $idx_g is unbuilt yet has past capex cost/subsidy, setting to zero"
g.past_invest_cost = z
g.past_invest_subsidy = z
if !haskey(config,:past_invest_file)
for (idx_g, g) in enumerate(eachrow(gen))
if g.build_status == "unbuilt"
if any(!=(0), g.past_invest_cost) || any(!=(0), g.past_invest_subsidy)
@warn "Generator $idx_g is unbuilt yet has past capex cost/subsidy, setting to zero"
g.past_invest_cost = z
g.past_invest_subsidy = z
end
else
past_invest_percentages = get_past_invest_percentages(g, years)
g.past_invest_cost = g.past_invest_cost .* past_invest_percentages
g.past_invest_subsidy = g.past_invest_subsidy .* past_invest_percentages
end
else
past_invest_percentages = get_past_invest_percentages(g, years)
g.past_invest_cost = g.past_invest_cost .* past_invest_percentages
g.past_invest_subsidy = g.past_invest_subsidy .* past_invest_percentages
end
end

Expand Down Expand Up @@ -573,6 +578,76 @@ function setup_table!(config, data, ::Val{:gen})
return gen
end
export setup_table!
"""
setup_table!(config, data, ::Val{:past_invest})

Sets up the invest cost gen table. This is necessary when gens are aggregated so that capex for built gens can be found.
Creates age column which is a ByYear column. Unbuilt generators have a negative age before year_on.
"""
function setup_table!(config, data, ::Val{:past_invest})

if !haskey(config, :past_invest_file)
return
end

bus = get_table(data, :bus)
past_invest = get_table(data, :past_invest)
years = get_years(data)

# Set up year_unbuilt before setting up new gens. Plus we will want to save the column
hasproperty(past_invest, :year_unbuilt) || (past_invest.year_unbuilt = map(y->add_to_year(y, -1), past_invest.year_on))

# Set up past capex cost and subsidy to be for built generators only
# Make columns as needed
hasproperty(past_invest, :past_invest_cost) || (past_invest.past_invest_cost = zeros(nrow(past_invest)))
hasproperty(past_invest, :past_invest_subsidy) || (past_invest.past_invest_subsidy = zeros(nrow(past_invest)))
z = Container(0.0)
to_container!(past_invest, :past_invest_cost)
to_container!(past_invest, :past_invest_subsidy)
for (idx_g, g) in enumerate(eachrow(past_invest))
if g.build_status == "unbuilt"
if any(!=(0), g.past_invest_cost) || any(!=(0), g.past_invest_subsidy)
@warn "Generator $idx_g is unbuilt yet has past capex cost/subsidy, setting to zero"
g.past_invest_cost = z
g.past_invest_subsidy = z
end
else
past_invest_percentages = get_past_invest_percentages(g, years)
g.past_invest_cost = g.past_invest_cost .* past_invest_percentages
g.past_invest_subsidy = g.past_invest_subsidy .* past_invest_percentages
end
end


original_cols = propertynames(past_invest)
data[:past_invest_table_original_cols] = original_cols

#removes capex_obj if read in from previous sim
:capex_obj in propertynames(data[:past_invest]) && select!(data[:past_invest], Not(:capex_obj))

#set build_status to 'built' for all gens marked 'new'. This marks gens built in a previous sim as 'built'.
b = "built" # pre-allocate
transform!(past_invest, :build_status => ByRow(s->isnew(s) ? b : s) => :build_status) # transform in-place

# Set the pcap_max to be equal to pcap0 for built generators
past_invest.pcap_max = map(row->isbuilt(row) ? row.pcap0 : row.pcap_max, eachrow(past_invest))
past_invest.pcap0 = map(row->isbuilt(row) ? row.pcap0 : 0.0, eachrow(past_invest))


### Add age column as by ByYear based on year_on
years = year2float.(get_years(data))
gen_age = Container[ByNothing(0.0) for i in 1:nrow(past_invest)]
for idx_g in 1:nrow(past_invest)
year_on = year2float(past_invest[idx_g, :year_on])
g_age = [year - year_on for year in years]
gen_age[idx_g] = ByYear(g_age)
end

add_table_col!(data, :past_invest, :age, gen_age, NumYears, "The age of the generator in each simulation year, given as a byYear container. Negative age is given for gens before their year_on.")

return past_invest
end


"""
join_bus_columns!(data, table_name)
Expand Down Expand Up @@ -977,6 +1052,51 @@ function summarize_table(::Val{:gen})
return df
end

@doc """
summarize_table(::Val{:past_invest})

$(table2markdown(summarize_table(Val(:past_invest))))
"""
function summarize_table(::Val{:past_invest})
df = TableSummary()
push!(df,
(:bus_idx, Int64, NA, true, "The index of the `bus` table that the generator corresponds to"),
(:status, Bool, NA, false, "Whether or not the generator is in service"),
(:build_status, String15, NA, true, "Whether the generator is `built`, `new`, `unbuilt`, or `unretrofitted`. All generators marked `new` when the gen file is read in will be changed to `built`. Can also be changed to `retired_exog` or `retired_endog` after the simulation is run. See [`update_build_status!`](@ref). Note that `unretrofitted` means it is a [`Retrofit`](@ref) option based on a `built` generator."),
(:build_type, AbstractString, NA, true, "Whether the generator is 'real', 'exog' (exogenously built), or 'endog' (endogenously built)"),
(:build_id, AbstractString, NA, true, "Identifier of the build row. For pre-existing generators not specified in the build file, this is usually left empty"),
(:year_on, YearString, Year, true, "The first year of operation for the generator. (For new gens this is also the year it was built)"),
(:year_unbuilt,YearString, Year, false, "The latest year the generator was known not to be built. Defaults to year_on - 1. Used for past capex accounting."),
(:econ_life, Float64, NumYears, true, "The number of years in the economic lifetime of the generator."),
(:year_off, YearString, Year, true, "The first year that the generator is no longer operating in the simulation, computed from the simulation. Leave as y9999 if an existing generator that has not been retired in the simulation yet."),
(:year_shutdown, YearString, Year, true, "The forced (exogenous) shutdown year for the generator. Often equal to the year_on plus the econ_life"),
(:genfuel, AbstractString, NA, true, "The fuel type that the generator uses"),
(:gentype, String, NA, true, "The generation technology type that the generator uses"),
(:pcap_inv, Float64, MWCapacity, true, "Original invested nameplate power generation capacity for the generator. This is the original invested capacity of exogenously built generators (even if there have been retirements ), and the original invested capacity in year_on for endogenously built generators."),
(:pcap0, Float64, MWCapacity, true, "Nameplate power generation capacity for the generator at the start of the simulation"),
(:pcap_min, Float64, MWCapacity, true, "Minimum nameplate power generation capacity of the generator (normally set to zero to allow for retirement)"),
(:pcap_max, Float64, MWCapacity, true, "Maximum nameplate power generation capacity of the generator"),
(:vom, Float64, DollarsPerMWhGenerated, true, "Variable operation and maintenance cost per MWh of generation"),
(:fuel_price, Float64, DollarsPerMMBtu, false, "Fuel cost per MMBtu of fuel used. `heat_rate` column also necessary when supplying `fuel_price`"),
(:heat_rate, Float64, MMBtuPerMWhGenerated, false, "Heat rate, or MMBtu of fuel consumed per MWh electricity generated (0 for generators that don't use combustion)"),
(:fom, Float64, DollarsPerMWCapacityPerHour, true, "Hourly fixed operation and maintenance cost for a MW of generation capacity"),
(:capex, Float64, DollarsPerMWBuiltCapacityPerHour, true, "Hourly capital expenditures for a MW of generation capacity. For already-built generators, this is not accounted for in the optimization or accounting. For accounting for investment costs and subsidies in built generators, use `past_invest_cost` and `past_invest_subsidy`"),
(:transmission_capex, Float64, DollarsPerMWBuiltCapacityPerHour, true, "Hourly capital expenditures for the transmission supporting a MW of generation capacity"),
(:routine_capex, Float64, DollarsPerMWCapacityPerHour, true, "Routine capital expenditures for a MW of discharge capacity"),
(:past_invest_cost, Float64, DollarsPerMWCapacityPerHour, false, "Investment costs per MW of initial capacity per hour, for past investments"),
(:past_invest_subsidy, Float64, DollarsPerMWCapacityPerHour, false, "Investment subsidies from govt. per MW of initial capacity per hour, for past investments"),
(:cf_min, Float64, MWhGeneratedPerMWhCapacity, false, "The minimum capacity factor, or operable ratio of power generation to capacity for the generator to operate. Take care to ensure this is not above the hourly availability factor in any of the hours, or else the model may be infeasible. Set to zero by default."),
(:cf_max, Float64, MWhGeneratedPerMWhCapacity, false, "The maximum capacity factor, or operable ratio of power generation to capacity for the generator to operate"),
(:cf_hist, Float64, MWhGeneratedPerMWhCapacity, false, "The historical capacity factor for the generator, or the gentype if no previous data is available. Primarily used to calculate estimate policy value (PTC and EmissionPrice capex_adj)"),
(:af, Float64, MWhGeneratedPerMWhCapacity, false, "The availability factor, or maximum available ratio of pewer generation to nameplate capacity for the generator."),
(:emis_co2, Float64, ShortTonsPerMWhGenerated, false, "The emission rate per MWh of CO2"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the other emission level columns (so2, nox, pm25) also be included here?

(:capt_co2_percent, Float64, NA, false, "The percentage of co2 emissions captured, to be sequestered."),
(:chp_co2_multi,Float64,NA,false,"The percentage of CO2 emissions from CHP attributed to the power generation. Used to calculate CO2e"),
(:reg_factor, Float64, NA, true, "The percentage of generation that dispatches to a cost-of-service regulated market"),
)
return df
end


@doc """
summarize_table(::Val{:bus})
Expand Down Expand Up @@ -1128,6 +1248,11 @@ end
export get_table
export get_table

function get_cross_table(data, table_name)
return data[table_name]::OrderedDict{Symbol, OrderedDict{Symbol,OrderedDict{Symbol,Function}}}
end
export get_cross_table

"""
get_subtable(table::DataFrame, conditions...)

Expand Down Expand Up @@ -1535,7 +1660,17 @@ Returns the number of years in this simulation
function get_num_years(data)
return length(get_years(data))
end
export get_num_years, get_years

"""
get_first_sim_year(data) -> year

Returns the first year as a string (i.e. "y2022") of the years being represented in the sim.
"""
function get_first_sim_year(data)
return data[:years][1]::String
end

export get_num_years, get_years, get_first_sim_year

"""
get_bus_gens(data, bus_idx)
Expand Down
22 changes: 20 additions & 2 deletions src/results/formulas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ function setup_results_formulas!(config, data)

results_formulas_table = read_table(data, results_formulas_file, :results_formulas)

for row in eachrow(results_formulas_table)
add_results_formula!(data, row.table_name, row.result_name, row.formula, row.unit, row.description)
foreach(eachrow(results_formulas_table)) do row
haskey(data, row.table_name) ?
add_results_formula!(data, row.table_name, row.result_name, row.formula, row.unit, row.description) :
@warn "There is no table $(row.table_name) in data. $(row.result_name) will not be added to formulas."
end
end
export setup_results_formulas!
Expand Down Expand Up @@ -801,6 +803,22 @@ function (f::CostOfServiceRebate)(data, table, idxs, yr_idxs, hr_idxs)
end
export CostOfServiceRebate

struct CostOfServicePastCost <: Function
table_name::Symbol
end
function (f::CostOfServicePastCost)(data, table, idxs, yr_idxs, hr_idxs)
reg_factor = table.reg_factor::Vector{Float64}
res = 0.0
for i in idxs
rf = reg_factor[i]
past_invest = compute_result(data, f.table_name, :past_invest_cost_total, i, yr_idxs, hr_idxs)
prod = rf * past_invest
res += prod
end
return res
# return sum0(reg_factor[i] * compute_result(data, f.table_name, :net_total_revenue_prelim, i, yr_idxs, hr_idxs) for i in idxs)
end
export CostOfServicePastCost

function _sum(v1, idxs)
res = 0.0
Expand Down
14 changes: 10 additions & 4 deletions src/results/parse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ function parse_lmp_results!(config, data)
res_raw = get_raw_results(data)

branch = get_table(data, :branch)
bus = get_table(data, :bus)
f_bus_idxs = branch.f_bus_idx::Vector{Int64}
t_bus_idxs = branch.t_bus_idx::Vector{Int64}

Expand Down Expand Up @@ -420,6 +421,7 @@ function parse_lmp_results!(config, data)
hour_weights_mat = [hour_weights[hr_idx] for yr_idx in 1:nyr, hr_idx in 1:nhr]

# Loop through each branch and add the hourly merchandising surplus, in dollars, to the appropriate bus
ms_all = zeros(size(lmp_elserv)) # nbus x nyr x nhr
ms = zeros(size(lmp_elserv)) # nbus x nyr x nhr
ms_branch = zeros(size(pflow_branch))

Expand All @@ -430,13 +432,17 @@ function parse_lmp_results!(config, data)
t_bus_lmp = view(lmp_elserv, t_bus_idx, :, :) # nyr x nhr
pflow = view(pflow_branch, branch_idx, :, :) # nyr x nhr
ms_per_bus = ((t_bus_lmp .- f_bus_lmp) .* pflow) .* hour_weights_mat .* 0.5
ms[f_bus_idx, :, :] .+= ms_per_bus
ms[t_bus_idx, :, :] .+= ms_per_bus
ms_all[f_bus_idx, :, :] .+= ms_per_bus
ms_all[t_bus_idx, :, :] .+= ms_per_bus
f_bus_reg_factor = first(filter(row -> row.bus_idx == f_bus_idx, bus))[:reg_factor]
t_bus_reg_factor = first(filter(row -> row.bus_idx == t_bus_idx, bus))[:reg_factor]
ms[f_bus_idx, :, :] .+= ms_per_bus .* (1 - f_bus_reg_factor)
ms[t_bus_idx, :, :] .+= ms_per_bus .* (1 - t_bus_reg_factor)
ms_branch[branch_idx, :, :] = ms_per_bus .* 2
end

add_table_col!(data, :bus, :merchandising_surplus, ms, Dollars, "Merchandising surplus, in dollars, from selling electricity for a higher price at one end of a line than another.")

add_table_col!(data, :bus, :merchandising_surplus, ms_all, Dollars, "Merchandising surplus, in dollars, from selling electricity for a higher price at one end of a line than another, calculated for every node.")
add_table_col!(data, :bus, :merchandising_surplus_comp, ms, Dollars, "Merchandising surplus, in dollars, from selling electricity for a higher price at one end of a line than another, calculated for nodes in competitive regions only.")
add_table_col!(data, :branch, :merchandising_surplus, ms_branch, Dollars, "Merchandising surplus, in dollars, from selling electricity for a higher price at one end of a line than another.")

# # Add the LMP's to the results and to the branch table
Expand Down
Loading
Loading