diff --git a/Project.toml b/Project.toml index 09c5815..17f44d3 100644 --- a/Project.toml +++ b/Project.toml @@ -9,28 +9,30 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" ExportAll = "ad2082ca-a69e-11e9-38fa-e96309a31fe4" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" Formatting = "59287772-0a20-5a39-b81b-1366585eb4c0" -TheoryOfGames = "eb50afb4-6f20-4b37-9b66-473e668300bf" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LayeredLayouts = "f4a74d36-062a-4d48-97cd-1356bad1de4e" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" SankeyPlots = "8fd88ec8-d95c-41fc-b299-05f2225f2cc5" +StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd" +TheoryOfGames = "eb50afb4-6f20-4b37-9b66-473e668300bf" XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" [compat] -julia = "1" CSV = "0.10" DataFrames = "1" ExportAll = "0.1" FileIO = "1" Formatting = "0.4" -TheoryOfGames = "0.1" JuMP = "1" LayeredLayouts = "0.2" MathOptInterface = "1.0" Plots = "1" SankeyPlots = "0.2.2" +StatsPlots = "0.15" +TheoryOfGames = "0.1" XLSX = "0.9" -YAML = "0.4" \ No newline at end of file +YAML = "0.4" +julia = "1" diff --git a/docs/src/examples/example_aggregated_non_cooperative.jl b/docs/src/examples/example_aggregated_non_cooperative.jl index 4579a60..ffbc730 100644 --- a/docs/src/examples/example_aggregated_non_cooperative.jl +++ b/docs/src/examples/example_aggregated_non_cooperative.jl @@ -42,4 +42,10 @@ print_summary(ANC_Model) save_summary(ANC_Model, output_file_isolated) # Plot the sankey plot of resources -plot_sankey(ANC_Model) \ No newline at end of file +plot_sankey(ANC_Model) + +# DataFrame of the business plan +business_plan(ANC_Model) + +# plot business plan +business_plan_plot(ANC_Model) \ No newline at end of file diff --git a/docs/src/examples/example_cooperative.jl b/docs/src/examples/example_cooperative.jl index 623cfd8..d904e56 100644 --- a/docs/src/examples/example_cooperative.jl +++ b/docs/src/examples/example_cooperative.jl @@ -42,4 +42,10 @@ print_summary(CO_Model) save_summary(CO_Model, output_file_isolated) # Plot the sankey plot of resources -plot_sankey(CO_Model) \ No newline at end of file +plot_sankey(CO_Model) + +# DataFrame of the business plan +business_plan(CO_Model) + +# plot business plan +business_plan_plot(CO_Model) \ No newline at end of file diff --git a/docs/src/examples/example_non_cooperative.jl b/docs/src/examples/example_non_cooperative.jl index db65ac0..2bd8852 100644 --- a/docs/src/examples/example_non_cooperative.jl +++ b/docs/src/examples/example_non_cooperative.jl @@ -42,4 +42,10 @@ print_summary(NC_Model) save_summary(NC_Model, output_file_isolated) # Plot the sankey plot of resources -plot_sankey(NC_Model) \ No newline at end of file +plot_sankey(NC_Model) + +# DataFrame of the business plan +business_plan(NC_Model) + +# plot business plan +business_plan_plot(NC_Model) \ No newline at end of file diff --git a/examples/Project.toml b/examples/Project.toml new file mode 100644 index 0000000..8c79305 --- /dev/null +++ b/examples/Project.toml @@ -0,0 +1,16 @@ +[deps] +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +EnergyCommunity = "2f2d8a28-e724-42c4-aa4e-51fe4e6b7a61" +ExportAll = "ad2082ca-a69e-11e9-38fa-e96309a31fe4" +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +Formatting = "59287772-0a20-5a39-b81b-1366585eb4c0" +TheoryOfGames = "eb50afb4-6f20-4b37-9b66-473e668300bf" +HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" +ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" +JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +SankeyPlots = "8fd88ec8-d95c-41fc-b299-05f2225f2cc5" +XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" +YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" diff --git a/examples/RunSingleModel.jl b/examples/RunSingleModel.jl index baa18d7..b74440f 100644 --- a/examples/RunSingleModel.jl +++ b/examples/RunSingleModel.jl @@ -1,6 +1,6 @@ -# Run this script from EnergyCommunity.jl root!!! -using Pkg -Pkg.activate(".") # this line requires EnergyCommunity.jl to be the current directory +# # Run this script from EnergyCommunity.jl root!!! +# using Pkg +# Pkg.activate("examples") using EnergyCommunity, JuMP using HiGHS, Plots @@ -45,6 +45,12 @@ save_summary(ECModel, output_file_combined) # Plot sankey plot of CO model plot_sankey(ECModel) +# DataFrame of the business plan +business_plan(ECModel) + +# plot 20 years business plan of CO model +business_plan_plot(ECModel) + ## Model NC # create NonCooperative model @@ -66,4 +72,10 @@ print_summary(NC_Model) save_summary(NC_Model, output_file_isolated) # plot Sankey plot of NC model -plot_sankey(NC_Model) \ No newline at end of file +plot_sankey(NC_Model) + +# DataFrame of the business plan of NC model +business_plan(NC_Model) + +# plot business plan of NC model +business_plan_plot(NC_Model) diff --git a/examples/RunSingleModel_complete.jl b/examples/RunSingleModel_complete.jl index d9de5f1..93fd319 100644 --- a/examples/RunSingleModel_complete.jl +++ b/examples/RunSingleModel_complete.jl @@ -1,6 +1,6 @@ -# Run this script from EnergyCommunity.jl root!!! +# # Run this script from EnergyCommunity.jl root!!! # using Pkg -# Pkg.activate(".") # this line requires EnergyCommunity.jl to be the current directory +# Pkg.activate("examples") using EnergyCommunity, JuMP using HiGHS, Plots diff --git a/src/ECModel.jl b/src/ECModel.jl index 9448f7b..cfe245e 100644 --- a/src/ECModel.jl +++ b/src/ECModel.jl @@ -708,6 +708,7 @@ function plot_sankey(ECModel::AbstractEC; return plot_sankey(ECModel, sank_data; label_size=label_size) end + """ split_financial_terms(ECModel::AbstractEC, profit_distribution) @@ -842,6 +843,7 @@ function split_financial_terms(ECModel::AbstractEC, profit_distribution=nothing) NPV=NPV, CAPEX=CAPEX, OPEX=OPEX, + OEM=Ann_Maintenance, REP=Ann_Replacement, RV=Ann_Recovery, REWARD=Ann_reward, @@ -852,6 +854,306 @@ function split_financial_terms(ECModel::AbstractEC, profit_distribution=nothing) ) end + +""" +split_yearly_financial_terms(ECModel::AbstractEC, profit_distribution) + +Function to describe the cost term distributions by all users for all years. + +Parameters +---------- +- ECModel : AbstractEC + EnergyCommunity model +- profit_distribution + Final objective function +- user_set_financial + User set to be considered for the financial analysis + +Returns +------- + The output value is a NamedTuple with the following elements + - NPV: the NPV of each user given the final profit_distribution adjustment by game theory techniques + - CAPEX: the annualized CAPEX + - OPEX: the annualized operating costs (yearly maintenance and yearly peak and energy grid charges) + - REP: the annualized replacement costs + - RV: the annualized recovery charges + - REWARD: the annualized reward distribution by user + - PEAK: the annualized peak costs + - EN_SELL: the annualized revenues from energy sales + - EN_BUY: the annualized costs from energy consumption and buying + - EN_NET: the annualized net energy costs +""" +function split_yearly_financial_terms(ECModel::AbstractEC, profit_distribution=nothing) + gen_data = ECModel.gen_data + + project_lifetime = field(gen_data, "project_lifetime") + + get_value = (dense_axis, element) -> (element in axes(dense_axis)[1] ? dense_axis[element] : 0.0) + zero_if_negative = x->((x>=0) ? x : 0.0) + + year_set = 0:project_lifetime + + user_set_financial = [EC_CODE; get_user_set(ECModel)] + + if isnothing(profit_distribution) + user_set = get_user_set(ECModel) + + profit_distribution = JuMP.Containers.DenseAxisArray( + [ + objective_value(ECModel) - sum(ECModel.results[:NPV_us]); + ECModel.results[:NPV_us][user_set].data; + ], + user_set_financial, + ) + end + + @assert termination_status(ECModel) != MOI.OPTIMIZE_NOT_CALLED + + ann_factor = [1. ./((1 + field(gen_data, "d_rate")).^y) for y in year_set] + + # Investment costs + CAPEX = JuMP.Containers.DenseAxisArray( + [(y == 0) && (u != EC_CODE) ? sum(Float64[get_value(ECModel.results[:CAPEX_tot_us], u)]) : 0.0 + for y in year_set, u in user_set_financial] + , year_set, user_set_financial + ) + # Maintenance costs + Ann_Maintenance = JuMP.Containers.DenseAxisArray( + [(y != 0) && (u != EC_CODE) ? get_value(ECModel.results[:C_OEM_tot_us], u) : 0.0 + for y in year_set, u in user_set_financial] + , year_set, user_set_financial + ) + # Replacement costs + #The index is 1:20 so in the result should be proper changed. I'll open an issue + Ann_Replacement = JuMP.Containers.DenseAxisArray( + [(y != 0) && (u != EC_CODE) ? get_value(ECModel.results[:C_REP_tot_us][y, :], u) : 0.0 + for y in year_set, u in user_set_financial] + , year_set, user_set_financial + ) + # Recovery value + Ann_Recovery = JuMP.Containers.DenseAxisArray( + [(y != 0) && (u != EC_CODE) ? (get_value(ECModel.results[:R_RV_tot_us][y, :], u)) : 0.0 + for y in year_set, u in user_set_financial] + , year_set, user_set_financial + ) + # Peak energy charges + Ann_peak_charges = JuMP.Containers.DenseAxisArray( + [(y != 0) && (u != EC_CODE) ? get_value(ECModel.results[:C_Peak_tot_us], u) : 0.0 + for y in year_set, u in user_set_financial] + , year_set, user_set_financial + ) + # Get revenes by selling energy and costs by buying or consuming energy + Ann_energy_revenues = JuMP.Containers.DenseAxisArray( + [(y != 0) && (u in axes(ECModel.results[:R_Energy_us])[1]) && (u != EC_CODE) ? + sum(zero_if_negative.(ECModel.results[:R_Energy_us][u,:])) : 0.0 + for y in year_set, u in user_set_financial] + , year_set, user_set_financial + ) + Ann_energy_costs = JuMP.Containers.DenseAxisArray( + [(y != 0) && (u in axes(ECModel.results[:R_Energy_us])[1]) && (u != EC_CODE) ? sum(zero_if_negative.(.-(ECModel.results[:R_Energy_us][u, :]))) : 0.0 + for y in year_set, u in user_set_financial] + , year_set, user_set_financial + ) + + # Total OPEX costs + # I think that I miss the reward here + Ann_ene_net_costs = Ann_energy_costs .- Ann_energy_revenues + + OPEX = Ann_Maintenance .+ Ann_peak_charges .+ Ann_ene_net_costs + + # get NPV given the reward allocation + # Using the total NPV by user, we calculate the annual reward that enables that + NPV = JuMP.Containers.DenseAxisArray( + [get_value(profit_distribution, u) for u in user_set_financial], + user_set_financial, + ) + + # Total reward + # This is the total discounted cost of all terms but NPV. I think that this must be improved + total_discounted_cost_by_user = JuMP.Containers.DenseAxisArray( + (CAPEX .+ OPEX .+ Ann_Replacement .- Ann_Recovery).data' * ann_factor, + user_set_financial, + ) + total_discounted_reward_by_user = NPV .+ total_discounted_cost_by_user + + Ann_reward = JuMP.Containers.DenseAxisArray( + [ + (y != 0) ? total_discounted_reward_by_user[u]/(sum(ann_factor) - 1) : 0.0 + for y in year_set, u in user_set_financial + ], + year_set, user_set_financial + ) + + # Cumulative Discounted Cash Flows + total_costs = Ann_reward .- CAPEX .- OPEX .- Ann_Replacement .+ Ann_Recovery + CUM_DCF = JuMP.Containers.DenseAxisArray( + cumsum(mapslices(x->x.*ann_factor, total_costs.data, dims=1), dims=1), + year_set, user_set_financial + ) + + return ( + NPV=NPV, + CUM_DCF=CUM_DCF, + CAPEX=CAPEX, + OPEX=OPEX, + OEM=Ann_Maintenance, + REP=Ann_Replacement, + RV=Ann_Recovery, + REWARD=Ann_reward, + PEAK=Ann_peak_charges, + EN_SELL=Ann_energy_revenues, + EN_CONS=Ann_energy_costs, + EN_NET=Ann_ene_net_costs, + year_set=year_set, + ) +end + +""" + business_plan(ECModel::AbstractEC, profit_distribution) + +Function to describe the cost term distributions by all users for all years. + +Parameters +---------- +- ECModel : AbstractEC + EnergyCommunity model +- profit_distribution + Final objective function +- user_set_financial + User set to be considered for the financial analysis + +Returns +------- + The output value is a NamedTuple with the following elements + - df_business + Dataframe with the business plan information +""" +function business_plan(ECModel::AbstractEC, profit_distribution=nothing, user_set_financial=nothing) + gen_data = ECModel.gen_data + + project_lifetime = field(gen_data, "project_lifetime") + + if isnothing(user_set_financial) + user_set_financial = get_user_set(ECModel) + end + + # Create a vector of years from 2023 to (2023 + project_lifetime) + gen_data = ECModel.gen_data + project_lifetime = field(gen_data, "project_lifetime") + + business_plan = split_yearly_financial_terms(ECModel) + year_set = business_plan.year_set + + # Create an empty DataFrame + df_business = DataFrame( + Year = Int[], + CUM_DCF = Float64[], + CAPEX = Float64[], + OEM = Float64[], + EN_SELL = Float64[], + EN_CONS = Float64[], + PEAK = Float64[], + REP = Float64[], + REWARD = Float64[], + RV = Float64[], + ) + for i in year_set + Year = year_set[i+1] + CUM_DCF = sum(business_plan.CUM_DCF[i, :]) + CAPEX = sum(business_plan.CAPEX[i, :]) + OEM = sum(business_plan.OEM[i, :]) + EN_SELL = sum(business_plan.EN_SELL[i, :]) + EN_CONS = sum(business_plan.EN_CONS[i, :]) + PEAK = sum(business_plan.PEAK[i, :]) + REP = sum(business_plan.REP[i, :]) + REWARD = sum(business_plan.REWARD[i, :]) + RV = sum(business_plan.RV[i, :]) + push!(df_business, (Year, CUM_DCF, CAPEX, OEM, EN_SELL, EN_CONS, PEAK, REP, REWARD, RV)) + end + + return df_business +end + +""" + business_plan_plot(ECModel::AbstractEC, profit_distribution) + +Function to describe the cost term distributions by all users for all years. + +Parameters +---------- +- ECModel : AbstractEC + EnergyCommunity model +- df_business + Dataframe with the business plan information +- plot_struct + Plot structure of the business plan + +Returns +------- + The output value is a plot with the business plan information +""" +function business_plan_plot( + ECModel::AbstractEC; + plot_struct=nothing, + xlabel="Year", + ylabel="Amount [k€]", + title="Business Plan", + legend=:bottomright, + color=:auto, + xrotation=45, + bar_width=0.6, + grid=false, + framestyle=:box, + barmode=:stack, + scaling_factor = 0.001, + kwargs... +) + + df_business = business_plan(ECModel) + + if isnothing(plot_struct) + # Define the plot structure + plot_struct = Dict( + "CAPEX" => [(-1, :CAPEX)], + "Repl. and Recovery" => [(-1, :REP), (+1, :RV)], + "OEM" => [(-1, :OEM), (-1, :PEAK)], + "Energy expences" => [(-1, :EN_CONS), (+1, :EN_SELL)], + "Reward" => [(+1, :REWARD)], + ) + end + + # Extract the year from the DataFrame + years = df_business.Year + + bar_labels = hcat(keys(plot_struct)...) + bar_data = [ + sum( + tup[1] .* df_business[!, tup[2]] .* scaling_factor + for tup in plot_struct[l] + ) + for l in keys(plot_struct) + ] + + # Create a bar plot + p = bar( + years, + bar_data, + labels=bar_labels, + xlabel=xlabel, ylabel=ylabel, + title=title, + legend=legend, + color=color, + xrotation=xrotation, + bar_width=bar_width, + grid=grid, + framestyle=framestyle, + barmode=barmode, + kwargs... + ) + + return p +end + function EnergyCommunity.split_financial_terms(ECModel::AbstractEC, profit_distribution::Dict) return split_financial_terms( ECModel, diff --git a/src/EnergyCommunity.jl b/src/EnergyCommunity.jl index 907e11d..dafe984 100644 --- a/src/EnergyCommunity.jl +++ b/src/EnergyCommunity.jl @@ -8,6 +8,7 @@ module EnergyCommunity using MathOptInterface using Base.Iterators using TheoryOfGames + using StatsPlots # import ECharts import SankeyPlots import CSV diff --git a/src/non_cooperative.jl b/src/non_cooperative.jl index dc2e86b..8bf4a91 100644 --- a/src/non_cooperative.jl +++ b/src/non_cooperative.jl @@ -685,11 +685,92 @@ end finalize_results!(::AbstractGroupNC, ECModel::AbstractEC) Function to finalize the results of the Non Cooperative model after the execution -Nothing to do +Many of the variables are set to zero due to the absence of cooperation between users """ function finalize_results!(::AbstractGroupNC, ECModel::AbstractEC) - # Nothing to do + + # get user set + user_set = ECModel.user_set + user_set_EC = vcat(EC_CODE, user_set) + + + gen_data = ECModel.gen_data + users_data = ECModel.users_data + market_data = ECModel.market_data + + # get time set + init_step = field(gen_data, "init_step") + final_step = field(gen_data, "final_step") + n_steps = final_step - init_step + 1 + time_set = 1:n_steps + project_lifetime = field(gen_data, "project_lifetime") + + + # Set definitions + user_set = ECModel.user_set + year_set = 1:project_lifetime + year_set_0 = 0:project_lifetime + time_set = 1:n_steps + peak_categories = profile(gen_data,"peak_categories") + # Set definition when optional value is not included + user_set = ECModel.user_set + + # Power of the aggregator + ECModel.results[:P_agg] = JuMP.Containers.DenseAxisArray( + [0.0 for t in time_set], + time_set + ) + + # Shared power: the minimum between the supply and demand for each time step + ECModel.results[:P_shared_agg] = JuMP.Containers.DenseAxisArray( + [0.0 + for t in time_set], + time_set + ) + + # Total reward awarded to the community at each time step + ECModel.results[:R_Reward_agg] = JuMP.Containers.DenseAxisArray( + [0.0 + for t in time_set], + time_set + ) + + # Total reward awarded to the community in a year + ECModel.results[:R_Reward_agg_tot] = sum(ECModel.results[:R_Reward_agg]) + + + # Total reward awarded to the aggregator in NPV terms + ECModel.results[:R_Reward_agg_NPV] = 0.0 + + + # Total reward awarded to the aggregator in NPV terms + ECModel.results[:NPV_agg] = ECModel.results[:R_Reward_agg_NPV] + + + # Cash flow + ECModel.results[:Cash_flow_agg] = JuMP.Containers.DenseAxisArray( + [(y == 0) ? 0.0 : ECModel.results[:R_Reward_agg_tot] for y in year_set_0], + year_set_0 + ) + + + # Cash flow total + ECModel.results[:Cash_flow_tot] = JuMP.Containers.DenseAxisArray( + [ + ((y == 0) ? 0.0 : + sum(ECModel.results[:Cash_flow_us][y, :]) + ECModel.results[:Cash_flow_agg][y]) + for y in year_set_0 + ], + year_set_0 + ) + + # Social welfare of the users + ECModel.results[:SW_us] = sum(ECModel.results[:NPV_us]) + + # Social welfare of the entire aggregation + ECModel.results[:SW] = ECModel.results[:SW_us] + ECModel.results[:NPV_agg] + ECModel.results[:objective_value] = ECModel.results[:SW] end diff --git a/test/refs/business_plan_plot/group_Aggregating-Non-Cooperative Model.png b/test/refs/business_plan_plot/group_Aggregating-Non-Cooperative Model.png new file mode 100644 index 0000000..d43e909 Binary files /dev/null and b/test/refs/business_plan_plot/group_Aggregating-Non-Cooperative Model.png differ diff --git a/test/refs/business_plan_plot/group_Cooperative Model.png b/test/refs/business_plan_plot/group_Cooperative Model.png new file mode 100644 index 0000000..7361ab2 Binary files /dev/null and b/test/refs/business_plan_plot/group_Cooperative Model.png differ diff --git a/test/refs/business_plan_plot/group_Non-Cooperative Model.png b/test/refs/business_plan_plot/group_Non-Cooperative Model.png new file mode 100644 index 0000000..e64bb77 Binary files /dev/null and b/test/refs/business_plan_plot/group_Non-Cooperative Model.png differ diff --git a/test/tests.jl b/test/tests.jl index 512c4ef..4599753 100644 --- a/test/tests.jl +++ b/test/tests.jl @@ -38,6 +38,7 @@ function _base_test(input_file, group, optimizer) @test_reference "refs/sankeys/group_$(string(group)).png" plot_sankey(ECModel) + @test_reference "refs/business_plan_plot/group_$(string(group)).png" business_plan_plot(ECModel) end function _utility_callback_test(input_file, optimizer, group_type; atol=ATOL, rtol=RTOL, kwargs...)