diff --git a/src/results.jl b/src/results.jl index a842a065..453af2fe 100644 --- a/src/results.jl +++ b/src/results.jl @@ -47,7 +47,8 @@ Columns in summary output include: 4. `data_type`, which is forecast or actual (data) 5. `lookahead` of the simulation in minutes 6. `revenue`, which is the total annual revenue of the simulated device in AUD - 7. `mean_rel_gap`, which is the mean relative gap across all decision times + 7. `negative_revenue`, which is the total annual negative revenue (i.e. losses) of the simulated device in AUD + 8. `mean_rel_gap`, which is the mean relative gap across all decision times # Arguments @@ -64,6 +65,7 @@ function _summarise_simulations( data::Dict{String,Any}, formulation::String, energy_capacity::Float64 ) revenues = Float64[] + negative_revenues = Float64[] bess_powers = Float64[] data_types = String[] lookaheads = Int64[] @@ -74,10 +76,12 @@ function _summarise_simulations( lookahead_df = df[df.lookahead_minutes .== lookahead, :] lookahead_df = lookahead_df[lookahead_df.status .== "binding", :] revenue = sum(lookahead_df.revenue) + negative_revenue = sum(lookahead_df[lookahead_df.revenue .< 0.0, :revenue]) average_gap = mean(unique(lookahead_df, "decision_time").relative_gap) bess_power = string(match(r"([0-9\.]*)MW", split(key, "/")[2]).captures[]) data_type = split(key, "/")[1] push!(revenues, float(revenue)) + push!(negative_revenues, negative_revenue) push!(lookaheads, lookahead) push!(average_gaps, average_gap) push!(bess_powers, parse(Float64, bess_power)) @@ -91,6 +95,7 @@ function _summarise_simulations( :data_type => data_types, :lookahead => string.(lookaheads), :revenue => revenues, + :negative_revenue => negative_revenues, :mean_rel_gap => average_gaps, ) summary_data = sort(summary_data, "power_capacity") @@ -100,18 +105,18 @@ function _summarise_simulations( end @doc raw""" -Calculates values of perfect information and foresight as absolute values (in AUD) and as +Calculates values of perfect lookahead and information as absolute values (in AUD) and as a percentage of perfect foresight revenue. -**Value of perfect information**: What is the additional benefit (revenue) that a participant +**Value of perfect lookahead**: What is the additional benefit (revenue) that a participant could gain if they were to know exactly what the market prices will be in the *lookahead horizon*. - * ``VPI = \textrm{Revenue}_\textrm{Actual Data Simulation} - \textrm{Revenue}_\textrm{Forecast Data Simulation}`` + * ``VPL = \textrm{Revenue}_\textrm{Actual Data Simulation} - \textrm{Revenue}_\textrm{Forecast Data Simulation}`` -**Value of perfect foresight**: What is the additional benefit (revenue) that a participant -could gain if they were to know exactly what the market prices will be *over the entire -year* - * ``VPF = \textrm{Revenue}_\textrm{Perfect Foresight} - \textrm{Revenue}_\textrm{Forecast Data Simulation}`` +**Value of perfect information**: What is the additional benefit (revenue) that a +participant could gain if they were to know exactly what the market prices will be +*over the entire year* + * ``VPI = \textrm{Revenue}_\textrm{Perfect Foresight} - \textrm{Revenue}_\textrm{Forecast Data Simulation}`` N.B. This function assumes that the input `df` only has data that corresponds to a device of a particular `energy_capacity`. @@ -122,12 +127,12 @@ of a particular `energy_capacity`. # Returns -`DataFrame` with absolute values of perfect information and foresight, and the same values +`DataFrame` with absolute values of perfect lookahead and information, and the same values as a percentage of perfect foresight revenue. """ -function calculate_vpi_vpf(df::DataFrame) - (v_pi_abs, v_pf_abs) = (Float64[], Float64[]) - (v_pi_percentage, v_pf_percentage) = (Float64[], Float64[]) +function calculate_vpl_vpi(df::DataFrame) + (v_pl_abs, v_pi_abs) = (Float64[], Float64[]) + (v_pl_percentage, v_pi_percentage) = (Float64[], Float64[]) (power_caps, data) = (Float64[], String[]) actual_caps = unique(df[df.data_type .== "actual", :power_capacity]) forecast_caps = unique(df[df.data_type .== "forecast", :power_capacity]) @@ -139,21 +144,21 @@ function calculate_vpi_vpf(df::DataFrame) cap_mask = df.power_capacity .== cap actual_mask = df.data_type .== "actual" forecast_mask = df.data_type .== "forecast" - pf_rev = df[ + pi_rev = df[ cap_mask .& forecast_mask .& (df.lookahead .== "Perfect Foresight"), :revenue, ] - pi_rev = df[cap_mask .& actual_mask .& lk_mask, :revenue] + pl_rev = df[cap_mask .& actual_mask .& lk_mask, :revenue] + v_pl = pl_rev - df[cap_mask .& forecast_mask .& lk_mask, :revenue] v_pi = pi_rev - df[cap_mask .& forecast_mask .& lk_mask, :revenue] - v_pf = pf_rev - df[cap_mask .& forecast_mask .& lk_mask, :revenue] - v_pi_percentage_pf = @. v_pi / pf_rev * 100 - v_pf_percentage_pf = @. v_pf / pf_rev * 100 + v_pl_percentage_pi = @. v_pl / pi_rev * 100 + v_pi_percentage_pi = @. v_pi / pi_rev * 100 push!(power_caps, cap) push!(data, lookahead) + push!(v_pl_abs, v_pl[]) push!(v_pi_abs, v_pi[]) - push!(v_pf_abs, v_pf[]) - push!(v_pi_percentage, v_pi_percentage_pf[]) - push!(v_pf_percentage, v_pf_percentage_pf[]) + push!(v_pl_percentage, v_pl_percentage_pi[]) + push!(v_pi_percentage, v_pi_percentage_pi[]) end end return DataFrame( @@ -161,23 +166,23 @@ function calculate_vpi_vpf(df::DataFrame) :energy_capacity => fill(unique(df.energy_capacity)[], length(power_caps)), :power_capacity => power_caps, :lookahead => data, + :vpl_abs => v_pl_abs, :vpi_abs => v_pi_abs, - :vpf_abs => v_pf_abs, + :vpl_per => v_pl_percentage, :vpi_per => v_pi_percentage, - :vpf_per => v_pf_percentage, ) end """ Summarises results for each simulated formulation and then calculates values of perfect -information and foresight. +lookahead and information. For each state, this function cycles through each simulated formulation and: 1. Calculates summary results (i.e. annual revenue and mean relative gap) - 2. Calculates the value of perfect information and value of perfect foresight + 2. Calculates the value of perfect lookahead and value of perfect information -For a single state, the VPIs and VPFs across simulated formulations are then released +For a single state, the VPLs and VPIs across simulated formulations are then released information a JLD2 file in the `results` folder, along with summary results for each simulated formulation. @@ -186,11 +191,11 @@ simulated formulation. # Returns -`Dict` mapping each state to `DataFrame` with VPI and VPF for each formulation and +`Dict` mapping each state to `DataFrame` with VPL and VPI for each formulation and lookahead. """ -function calculate_summaries_and_vpi_vpf_across_scenarios(sim_folder::String) - function _calculate_summary_vpi_vpf_for_formulation( +function calculate_summaries_and_vpl_vpi_across_scenarios(sim_folder::String) + function _calculate_summary_vpl_vpi_for_formulation( sim_folder::String, formulation::String, file::String, @@ -201,8 +206,8 @@ function calculate_summaries_and_vpi_vpf_across_scenarios(sim_folder::String) data = load(joinpath(results_path, file)) @info "Calculating summary information for $state $formulation" summary = _summarise_simulations(data, formulation, energy) - @info "Calculating VPI and VPF for $state $formulation" - return summary, calculate_vpi_vpf(summary) + @info "Calculating VPL and VPI for $state $formulation" + return summary, calculate_vpl_vpi(summary) end save_path = joinpath("results", "data") @@ -210,10 +215,10 @@ function calculate_summaries_and_vpi_vpf_across_scenarios(sim_folder::String) mkpath(save_path) end categorisation = _categorise_simulation_results(sim_folder) - states_vpi_vpf = Dict{String,DataFrame}() + states_vpl_vpi = Dict{String,DataFrame}() for state in keys(categorisation) summary_data = Dict{String,DataFrame}() - vpi_vpf_data = DataFrame[] + vpl_vpi_data = DataFrame[] formulation_results = categorisation[state] for (formulation, results) in pairs(formulation_results) if length(results) == 1 @@ -221,11 +226,11 @@ function calculate_summaries_and_vpi_vpf_across_scenarios(sim_folder::String) energy = parse( Float64, match(r"[A-Z]{2,3}_([0-9\.]*)MWh_.*", file).captures[] ) - (summary, vpi_vpf) = _calculate_summary_vpi_vpf_for_formulation( + (summary, vpl_vpi) = _calculate_summary_vpl_vpi_for_formulation( sim_folder, formulation, file, energy, state ) - vpi_vpf[!, :param] = fill(missing, size(vpi_vpf, 1)) - push!(vpi_vpf_data, vpi_vpf) + vpl_vpi[!, :param] = fill(missing, size(vpl_vpi, 1)) + push!(vpl_vpi_data, vpl_vpi) summary_data["$(formulation)"] = summary else for file in results @@ -234,11 +239,11 @@ function calculate_summaries_and_vpi_vpf_across_scenarios(sim_folder::String) ) energy = parse(Float64, energy_param_capture.captures[1]) param = string(energy_param_capture.captures[2]) - (summary, vpi_vpf) = _calculate_summary_vpi_vpf_for_formulation( + (summary, vpl_vpi) = _calculate_summary_vpl_vpi_for_formulation( sim_folder, formulation, file, energy, state ) - vpi_vpf[!, :param] = fill(param, size(vpi_vpf, 1)) - push!(vpi_vpf_data, vpi_vpf) + vpl_vpi[!, :param] = fill(param, size(vpl_vpi, 1)) + push!(vpl_vpi_data, vpl_vpi) summary_data["$(formulation)/$(param)"] = summary end end @@ -249,12 +254,12 @@ function calculate_summaries_and_vpi_vpf_across_scenarios(sim_folder::String) f[key] = value end end - state_vpi_vpf = vcat(vpi_vpf_data...) - @info "Saving VPI and VPF data for $state" - jldopen(joinpath(save_path, "vpi_vpf.jld2"), "w"; compress=true) do f - f["$(state)"] = state_vpi_vpf + state_vpl_vpi = vcat(vpl_vpi_data...) + @info "Saving VPL and VPI data for $state" + jldopen(joinpath(save_path, "vpl_vpi.jld2"), "w"; compress=true) do f + f["$(state)"] = state_vpl_vpi end - states_vpi_vpf[state] = state_vpi_vpf + states_vpl_vpi[state] = state_vpl_vpi end - return states_vpi_vpf + return states_vpl_vpi end