diff --git a/Project.toml b/Project.toml index 87b6297..7273dff 100644 --- a/Project.toml +++ b/Project.toml @@ -14,10 +14,17 @@ PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" TimeStruct = "f9ed5ce0-9f41-4eaa-96da-f38ab8df101c" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" +[weakdeps] +EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" + +[extensions] +EMIExt = "EnergyModelsInvestments" + [compat] Dates = "1.10" EnergyModelsBase = "0.9.0" EnergyModelsHeat = "0.1.1" +EnergyModelsInvestments = "0.8.1" EnergyModelsRenewableProducers = "0.6.5" JuMP = "1.23" Libdl = "1.10" diff --git a/README.md b/README.md index 4c4ba78..e2caab2 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,15 @@ This is exemplified with multiple new nodal descriptions from different models. ## Usage -The usage of the package is best illustrated through the commented examples. +The usage of the package is best illustrated through the commented examples in the documentation. The examples are minimum working examples highlighting how to use the receding horizon framework. In addition, they provide a user with an overview regarding potential adjustments to their elements. +> [!CAUTION] +> The node `MultipleBuildingTypes` is currently under development. +> It may contain incorrect implementations with respect to the calculation of the variable OPEX. +> We hence recommendto not use this node in its current stage. + > [!WARNING] > The package is not yet registered. > It is hence necessary to first clone the package and manually add the package to the example environment through: diff --git a/ext/EMGUIExt/descriptive_names.yml b/ext/EMGUIExt/descriptive_names.yml index 85c45df..c1ecf09 100644 --- a/ext/EMGUIExt/descriptive_names.yml +++ b/ext/EMGUIExt/descriptive_names.yml @@ -1,25 +1,7 @@ # This file contains description of EMX-structures and variables # with fields of type TimeStruct.TimeProfile structures: - WindPower: - cap: "Installed capacity" - profile: "Power production in each operational period as a ratio of the installed capacity at that time" - opex_var: "Relative variable operating expense per energy unit produced" - opex_fixed: "Relative fixed operating expense per installed capacity" - - MultipleBuildingTypes: - cap: "Demand" - penalty_surplus: "Penalty for surplus" - penalty_deficit: "Penalty for deficits" - - CSPandPV: - cap: "Installed capacity" - profile: "Power production profile as a ratio of installed capacity" - opex_var: "Relative variable operating expense per energy unit produced" - opex_fixed: "Relative fixed operating expense per installed capacity" - -variables: - buildings_surplus: "Surplus delivered to a sink, i.e., oversatisfied demand" - buildings_deficit: "Deficit in a sink, i.e., not satisfied demand" - solar_curtailment: "Curtailment of a solar energy source" - solar_cap_use: "Absolute capacity utilization" \ No newline at end of file + EnergyModelsLanguageInterfaces: + MultipleBuildingTypes: + penalty_surplus: "Penalty for surplus" + penalty_deficit: "Penalty for deficits" \ No newline at end of file diff --git a/ext/EMIExt/EMIExt.jl b/ext/EMIExt/EMIExt.jl new file mode 100644 index 0000000..4a5d8d5 --- /dev/null +++ b/ext/EMIExt/EMIExt.jl @@ -0,0 +1,114 @@ +module EMIExt + +using TimeStruct +using EnergyModelsBase +using EnergyModelsInvestments +using EnergyModelsHeat +using EnergyModelsLanguageInterfaces + +const TS = TimeStruct +const EMB = EnergyModelsBase +const EMI = EnergyModelsInvestments +const EMH = EnergyModelsHeat +const EMLI = EnergyModelsLanguageInterfaces + +""" + EMLI.BioCHP( + id::Any, + cap::TimeProfile, + cap_init::TimeProfile, + cap_max_installed::TimeProfile, + mass_fractions::Dict{<:ResourceBio,<:Real}, + heat_output_ratios::Dict{<:ResourceHeat,<:Real}, + electricity_resource::Resource; + data::Vector{Data} = Data[], + libpath::String = joinpath( + @__DIR__, + "..", + "..", + "CHP_modelling", + "build", + "lib", + "libbioCHP_wrapper.so", + ), + ) + +Constructs a [`BioCHP`](@ref) instance where the power and heat production profiles are +sampled from the `bioCHP_plant_c` function in the C++ library `CHP_modelling` with shared +library file located at `libpath`. The BioCHP has electricity production of the resource +`electricity_resource` and heat production of the resources in `heat_output_ratios` +(which can be different `ResourceHeat`s at different temperature levels). + +# Arguments +- **`id`** is the name or identifier of the node. +- **`cap`** is the installed electric capacity used in the CHP submodule for the calculations. +- **`cap_init`** is the initial capacity for the node. +- **`cap_max_installed`** is the maximum installed capacity. +- **`mass_fractions`** is the mass fractions of each input `ResourceBio`. +- **`heat_output_ratios`** is the output heat `ResourceHeat`s with the ratio of installed + capacity of heat to that of the electricity. +- **`electricity_resource`** is the `Resource` for the electricity. + +# Keyword arguments +- **`data::Vector{Data}`** is the additional data (*e.g.*, for investments). The field `data` + is conditional through usage of a constructor. +- **`libpath`** is the absolute path of the `CHP_modelling` library file. + +!!! note ""EmissionsEnergy" + If `EmissionsEnergy` is not included in the `data` field, it is automatically added. +""" +function EMLI.BioCHP( + id::Any, + cap::TimeProfile, + cap_init::TimeProfile, + cap_max_installed::TimeProfile, + mass_fractions::Dict{<:ResourceBio,<:Real}, + heat_output_ratios::Dict{<:ResourceHeat,<:Real}, + electricity_resource::Resource; + data::Vector{Data} = Data[], + libpath::String = joinpath( + @__DIR__, + "..", + "..", + "CHP_modelling", + "build", + "lib", + "libbioCHP_wrapper.so", + ), +) + cap_updated, opex_var, opex_fixed, input_updated, output, data, capex = EMLI.BioCHP( + cap, + mass_fractions, + heat_output_ratios, + electricity_resource, + data, + libpath, + ) + + if !any(isa(d, Investment) for d ∈ data) + push!(data, + SingleInvData( + FixedProfile(capex), # Capex in EUR/MW + cap_max_installed, # Max installed capacity [MW] + cap_init, + ContinuousInvestment(FixedProfile(0), cap_updated), + # Line above: Investment mode with the following arguments: + # 1. argument: min added capacity per sp [MW] + # 2. argument: max added capacity per sp [MW] + ), + ) + end + + return EMLI.BioCHP( + id, + cap_updated, + electricity_resource, + opex_var, + opex_fixed, + input_updated, + output, + data, + ) +end + +end diff --git a/src/datastructures.jl b/src/datastructures.jl index 3b07b01..3229d32 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -12,7 +12,7 @@ sampling the profile from a Python code through a constructor. - **`opex_var::TimeProfile`** is the variable operating expense per energy unit produced. - **`opex_fixed::TimeProfile`** is the fixed operating expense. - **`output::Dict{Resource, Real}`** are the generated `Resource`s, normally Power. -- **`data::Vector{Data}`** is the additional data (e.g. for investments). The field `data` +- **`data::Vector{<:Data}`** is the additional data (e.g. for investments). The field `data` is conditional through usage of a constructor. """ struct WindPower <: AbstractNonDisRES @@ -22,7 +22,7 @@ struct WindPower <: AbstractNonDisRES opex_var::TimeProfile opex_fixed::TimeProfile output::Dict{<:Resource,<:Real} - data::Vector{Data} + data::Vector{<:Data} end function WindPower( id::Any, @@ -45,7 +45,7 @@ end opex_var::TimeProfile, opex_fixed::TimeProfile, output::Dict{<:Resource,<:Real}; - data::Vector{Data} = Data[], + data::Vector{<:Data} = Data[], method::String = "Ninja", data_path::String = "" ) @@ -96,7 +96,7 @@ function WindPower( opex_var::TimeProfile, opex_fixed::TimeProfile, output::Dict{<:Resource,<:Real}; - data::Vector{Data} = Data[], + data::Vector{<:Data} = Data[], method::String = "Ninja", data_path::String = "", source::String = "NORA3", @@ -133,7 +133,7 @@ the strategic level. - **`opex_fixed::Dict{<:Resource,<:TimeProfile}`** is the fixed operating expense (for all resources in a Dict). - **`output::Dict{Resource, Real}`** are the generated `Resource`s, normally Power. -- **`data::Vector{Data}`** is the additional data (e.g. for investments). The field `data` +- **`data::Vector{<:Data}`** is the additional data (e.g. for investments). The field `data` is conditional through usage of a constructor. !!! danger @@ -146,7 +146,7 @@ struct CSPandPV <: AbstractNonDisRES opex_var::Dict{<:Resource,<:TimeProfile} opex_fixed::Dict{<:Resource,<:TimeProfile} output::Dict{<:Resource,<:Real} - data::Vector{Data} + data::Vector{<:Data} end function CSPandPV( id::Any, @@ -166,7 +166,7 @@ end time_start::DateTime, time_end::DateTime, resources_map::Dict{String,<:Resource}; - data::Vector{Data} = Data[], + data::Vector{<:Data} = Data[], data_location::String = joinpath(tempdir(), "CSPandPV"), overwrite_saved_data::Bool = false, ) @@ -202,7 +202,7 @@ the `executeSolarEnergyModelProcess` function in the `solar_power_plants` python `false`. !!! note - The argument `process_pay_load` is a dictionary that contains the process information + The argument `process_pay_load` is a dictionary that contains the process information for the Python function. The defaults can be achieved through ```julia @@ -216,7 +216,7 @@ function CSPandPV( time_start::DateTime, time_end::DateTime, resources_map::Dict{String,<:Resource}; - data::Vector{Data} = Data[], + data::Vector{<:Data} = Data[], data_location::String = joinpath(tempdir(), "CSPandPV"), overwrite_saved_data::Bool = false, ) @@ -259,9 +259,10 @@ function CSPandPV( # Construct normalized power profiles profile = Dict{Resource,TimeProfile}( - resources_map[key] => OperationalProfile(power_outputs[key] / max_power[key]) - for - key ∈ keys(power_outputs) + resources_map[key] => OperationalProfile( + max_power[key] == 0 ? power_outputs[key] : power_outputs[key] / max_power[key], + ) + for key ∈ keys(power_outputs) ) # Set the fixed OPEX to the values in the process_pay_load @@ -348,7 +349,7 @@ and deficit. - **`cap::Dict{<:Resource,<:TimeProfile}`** is the demand. - **`penalty_surplus::Dict{<:Resource,<:TimeProfile}`** are the penalties for surplus. - **`penalty_deficit::Dict{<:Resource,<:TimeProfile}`** are the penalties for deficit. -- **`input::Dict{<:Resource,<:Real}`** are the input +- **`input::Dict{<:Resource,<:Real}`** are the input [`Resource`](@extref EnergyModelsBase.Resource)s with conversion value `Real`. - **`data::Vector{<:Data}`** is the additional data (*e.g.*, for investments). The field `data` is conditional through usage of a constructor. @@ -455,7 +456,7 @@ Constructs a `MultipleBuildingTypes` instance where the demand profiles are samp of the energy carriers. !!! note - The argument `process_pay_load` is a dictionary that contains the process information + The argument `process_pay_load` is a dictionary that contains the process information for the Python function. The defaults can be achieved through ```julia @@ -643,7 +644,7 @@ The capacity is hereby normalized to a conversion value of 1 in the fields `inpu value `Real`. - **`output::Dict{<:Resource,<:Real}`** are the generated [`Resource`](@extref EnergyModelsBase.Resource)s with conversion value `Real`. -- **`data::Vector{Data}`** is the additional data (*e.g.*, for investments). The field `data` +- **`data::Vector{<:Data}`** is the additional data (*e.g.*, for investments). The field `data` is conditional through usage of a constructor. """ struct BioCHP <: NetworkNode @@ -684,7 +685,7 @@ end mass_fractions::Dict{<:ResourceBio,<:Real}, heat_output_ratios::Dict{<:ResourceHeat,<:Real}, electricity_resource::Resource; - data::Vector{Data} = Data[], + data::Vector{<:Data} = Data[], libpath::String = joinpath( @__DIR__, "..", @@ -711,8 +712,7 @@ library file located at `libpath`. The BioCHP has electricity production of the - **`electricity_resource`** is the `Resource` for the electricity. # Keyword arguments -- **`data::Vector{Data}`** is the additional data (*e.g.*, for investments). The field `data` - is conditional through usage of a constructor. +- **`data::Vector{<:Data}`** is the additional data (*e.g.*, for investments). - **`libpath`** is the absolute path of the `CHP_modelling` library file. !!! note ""EmissionsEnergy" @@ -724,7 +724,7 @@ function BioCHP( mass_fractions::Dict{<:ResourceBio,<:Real}, heat_output_ratios::Dict{<:ResourceHeat,<:Real}, electricity_resource::Resource; - data::Vector{Data} = Data[], + data::Vector{<:Data} = Data[], libpath::String = joinpath( @__DIR__, "..", @@ -735,7 +735,34 @@ function BioCHP( "libbioCHP_wrapper.so", ), ) + cap_updated, opex_var, opex_fixed, input_updated, output, data, _ = BioCHP( + cap, + mass_fractions, + heat_output_ratios, + electricity_resource, + data, + libpath, + ) + return BioCHP( + id, + cap_updated, + electricity_resource, + opex_var, + opex_fixed, + input_updated, + output, + data, + ) +end +function BioCHP( + cap::TimeProfile, + mass_fractions::Dict{<:ResourceBio,<:Real}, + heat_output_ratios::Dict{<:ResourceHeat,<:Real}, + electricity_resource::Resource, + data::Vector{<:Data}, + libpath::String, +) # Get the capacity el_capacity = cap.val @@ -849,28 +876,23 @@ function BioCHP( ) cap_updated = FixedProfile(Float64(W_el_prod[])) - opex_fixed = FixedProfile(Float64(C_op[])) - opex_var = FixedProfile(Float64(C_op_var[] / 8760)) + tot_opex = C_op[] + fixed_opex = tot_opex - C_op_var[] + var_opex = C_op_var[] / 8760 + opex_fixed = FixedProfile(Float64(fixed_opex)) + opex_var = FixedProfile(Float64(var_opex)) output = Dict{Resource,Real}( resource => val / W_el_prod[] for (resource, val) ∈ Qk_dict ) output[electricity_resource] = 1.0 + capex = Float64(C_inv[]/W_el_prod[]) if !(EmissionsEnergy ∈ typeof.(data)) push!(data, EmissionsEnergy()) end - return BioCHP( - id, - cap_updated, - electricity_resource, - opex_var, - opex_fixed, - input_updated, - output, - data, - ) + return cap_updated, opex_var, opex_fixed, input_updated, output, data, capex end """ diff --git a/test/Project.toml b/test/Project.toml index e29fa7f..6d6926a 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -3,6 +3,7 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" EnergyModelsHeat = "ad1b8b27-e232-4da9-b498-bea9c19a30d7" +EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" EnergyModelsRenewableProducers = "b007c34f-ba52-4995-ba37-fffe79fbde35" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" diff --git a/test/runtests.jl b/test/runtests.jl index e941591..747cefe 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,6 +3,7 @@ using Test using TimeStruct using EnergyModelsBase using EnergyModelsRenewableProducers +using EnergyModelsInvestments using EnergyModelsHeat using Dates using JSON @@ -21,20 +22,20 @@ include(joinpath(testdir, "utils.jl")) @testset "EnergyModelsLanguageInterfaces" begin ## Run all Aqua tests - include("Aqua.jl") + include(joinpath(testdir, "Aqua.jl")) ## Check if there is need for formatting - include("JuliaFormatter.jl") + include(joinpath(testdir, "JuliaFormatter.jl")) ## Test sampling routines - include("test_sampling_routines.jl") + include(joinpath(testdir, "test_sampling_routines.jl")) ## Test checks - include("test_checks.jl") + include(joinpath(testdir, "test_checks.jl")) ## Test nodes - include("test_windpower.jl") - include("test_buildings.jl") - include("test_CSPandPV.jl") - include("test_bioCHP.jl") + include(joinpath(testdir, "test_windpower.jl")) + include(joinpath(testdir, "test_buildings.jl")) + include(joinpath(testdir, "test_CSPandPV.jl")) + include(joinpath(testdir, "test_bioCHP.jl")) end diff --git a/test/test_bioCHP.jl b/test/test_bioCHP.jl index 69b1f38..fb349f3 100644 --- a/test/test_bioCHP.jl +++ b/test/test_bioCHP.jl @@ -19,14 +19,14 @@ using EnergyModelsHeat sp2 = 𝒯ᴵⁿᵛ[2] sp3 = 𝒯ᴵⁿᵛ[3] - @test value(m[:emissions_strategic][sp1, CO2]) ≈ 18341.597565299344 + @test value(m[:emissions_strategic][sp1, CO2]) ≈ 18341.59756529935 @test value(m[:emissions_strategic][sp2, CO2]) ≈ 1471.5853754583773 @test value(m[:emissions_strategic][sp3, CO2]) ≈ 0.0 # Check that the values of the deficits are correct. - @test sum(value.(m[:sink_deficit][sinks[1], t]) > 0.0 for t ∈ 𝒯) == 385 - @test sum(value.(m[:sink_deficit][sinks[2], t]) > 0.0 for t ∈ 𝒯) == 168 - @test sum(value.(m[:sink_deficit][sinks[3], t]) > 0.0 for t ∈ 𝒯) == 455 + @test sum(value.(m[:sink_deficit][sinks[1], t]) for t ∈ 𝒯) ≈ 5015.335151970511 + @test sum(value.(m[:sink_deficit][sinks[2], t]) for t ∈ 𝒯) ≈ 252.0 + @test sum(value.(m[:sink_deficit][sinks[3], t]) for t ∈ 𝒯) ≈ 29317.783839901702 # Check that the values of the surplus are correct. @test all(value.(m[:sink_surplus][sinks[1], t]) ≈ 0.0 for t ∈ 𝒯) diff --git a/test/utils.jl b/test/utils.jl index 9a30c9d..b7c60b1 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -383,7 +383,8 @@ function simple_graph_biochp(; output = nothing) em_limits = Dict(CO2 => FixedProfile(1e5)) # Emission cap for CO₂ in t/year em_cost = Dict(CO2 => StrategicProfile([71.0, 100, 500])) # Emission price for CO₂ in €/t - modeltype = OperationalModel(em_limits, em_cost, CO2) + discount_rate = 0.07 # Discount rate for investments + modeltype = InvestmentModel(em_limits, em_cost, CO2, discount_rate) return case, modeltype, create_model(case, modeltype) end