From b5e2fa92d4549bd030bea01151aca46444bd6941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Tue, 16 Dec 2025 14:15:56 +0100 Subject: [PATCH 1/9] Make use of C_inv in bioCHP model and adjust tests accordingly --- Project.toml | 2 ++ src/EnergyModelsLanguageInterfaces.jl | 1 + src/datastructures.jl | 14 +++++++++++++- test/runtests.jl | 16 ++++++++-------- test/test_bioCHP.jl | 6 +++--- test/utils.jl | 3 ++- 6 files changed, 29 insertions(+), 13 deletions(-) diff --git a/Project.toml b/Project.toml index 87b6297..fe783e5 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ version = "0.1.0" 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" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" @@ -18,6 +19,7 @@ YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" 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/src/EnergyModelsLanguageInterfaces.jl b/src/EnergyModelsLanguageInterfaces.jl index e5a39a7..dd3e490 100644 --- a/src/EnergyModelsLanguageInterfaces.jl +++ b/src/EnergyModelsLanguageInterfaces.jl @@ -7,6 +7,7 @@ module EnergyModelsLanguageInterfaces using JuMP using TimeStruct using EnergyModelsBase +using EnergyModelsInvestments using EnergyModelsRenewableProducers using EnergyModelsHeat using Dates diff --git a/src/datastructures.jl b/src/datastructures.jl index 3b07b01..c97d07d 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -860,10 +860,22 @@ function BioCHP( if !(EmissionsEnergy โˆˆ typeof.(data)) push!(data, EmissionsEnergy()) end + if !any(isa(d, Investment) for d โˆˆ data) + push!(data, + SingleInvData( + FixedProfile(Float64(C_inv[])), # Capex in EUR/MW + cap_updated, # Max installed capacity [MW] + ContinuousInvestment(FixedProfile(0), cap_updated), + # Line above: Investment mode with the following arguments: + # 1. argument: min added capactity per sp [MW] + # 2. argument: max added capactity per sp [MW] + ), + ) + end return BioCHP( id, - cap_updated, + FixedProfile(0), electricity_resource, opex_var, opex_fixed, diff --git a/test/runtests.jl b/test/runtests.jl index e941591..a5f9d4a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,20 +21,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..e421d40 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]) โ‰ˆ 18111.756783644374 @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[1], t]) > 0.0 for t โˆˆ ๐’ฏ) == 407 @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[3], t]) > 0.0 for t โˆˆ ๐’ฏ) == 469 # 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 From 96ad5544aaff8be158a12ba9be8cef643643a7af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Tue, 16 Dec 2025 19:48:41 +0100 Subject: [PATCH 2/9] Fix error from incorrect local setup --- test/test_bioCHP.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_bioCHP.jl b/test/test_bioCHP.jl index e421d40..877160b 100644 --- a/test/test_bioCHP.jl +++ b/test/test_bioCHP.jl @@ -24,7 +24,7 @@ using EnergyModelsHeat @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 โˆˆ ๐’ฏ) == 407 + @test sum(value.(m[:sink_deficit][sinks[1], t]) > 0.0 for t โˆˆ ๐’ฏ) == 408 @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 โˆˆ ๐’ฏ) == 469 From 66bb19f4883849b666e024476a4e4c5e82c00aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Tue, 16 Dec 2025 19:51:39 +0100 Subject: [PATCH 3/9] Enable CSPandPV-node usage for power_thermal=0 or power_pv=0 from Tecnalia_Solar-Energy-Model --- src/datastructures.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/datastructures.jl b/src/datastructures.jl index c97d07d..65d852c 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -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 From 356f0d686efa06fa222a9f0b2c2664c4fa00ee7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Wed, 17 Dec 2025 08:38:12 +0100 Subject: [PATCH 4/9] Fix test to avoid machine epsilon precision inaccuracies, e.g., 3.552713678800501e-15 > 0 triggers the bioCHP sink test --- test/test_bioCHP.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_bioCHP.jl b/test/test_bioCHP.jl index 877160b..446a637 100644 --- a/test/test_bioCHP.jl +++ b/test/test_bioCHP.jl @@ -24,9 +24,9 @@ using EnergyModelsHeat @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 โˆˆ ๐’ฏ) == 408 - @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 โˆˆ ๐’ฏ) == 469 + @test sum(value.(m[:sink_deficit][sinks[1], t]) for t โˆˆ ๐’ฏ) โ‰ˆ 5044.854273631571 + @test sum(value.(m[:sink_deficit][sinks[2], t]) for t โˆˆ ๐’ฏ) โ‰ˆ 252.0 + @test sum(value.(m[:sink_deficit][sinks[3], t]) for t โˆˆ ๐’ฏ) โ‰ˆ 29416.180912105232 # Check that the values of the surplus are correct. @test all(value.(m[:sink_surplus][sinks[1], t]) โ‰ˆ 0.0 for t โˆˆ ๐’ฏ) From 93ab8f815f247d14937bb6988e60f384acb3545d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Wed, 17 Dec 2025 08:44:07 +0100 Subject: [PATCH 5/9] Add suggestion from review --- src/datastructures.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/datastructures.jl b/src/datastructures.jl index 65d852c..a1c19ef 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -868,8 +868,8 @@ function BioCHP( cap_updated, # Max installed capacity [MW] ContinuousInvestment(FixedProfile(0), cap_updated), # Line above: Investment mode with the following arguments: - # 1. argument: min added capactity per sp [MW] - # 2. argument: max added capactity per sp [MW] + # 1. argument: min added capacity per sp [MW] + # 2. argument: max added capacity per sp [MW] ), ) end From 3c68452d8e39dc1c6972473ae114a644d32ff125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Thu, 18 Dec 2025 09:55:13 +0100 Subject: [PATCH 6/9] Make investments related work in an extension. --- Project.toml | 7 +- README.md | 3 + ext/EMGUIExt/descriptive_names.yml | 26 +-- ext/EMIExt/EMIExt.jl | 229 ++++++++++++++++++++++++++ src/EnergyModelsLanguageInterfaces.jl | 1 - src/datastructures.jl | 220 +------------------------ test/Project.toml | 1 + test/runtests.jl | 1 + test/test_bioCHP.jl | 6 +- 9 files changed, 253 insertions(+), 241 deletions(-) create mode 100644 ext/EMIExt/EMIExt.jl diff --git a/Project.toml b/Project.toml index fe783e5..7273dff 100644 --- a/Project.toml +++ b/Project.toml @@ -7,7 +7,6 @@ version = "0.1.0" 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" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" @@ -15,6 +14,12 @@ 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" diff --git a/README.md b/README.md index 4c4ba78..12aa6b0 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ The usage of the package is best illustrated through the commented examples. 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 MultipleBuildings node is currently under development and may contain incorrect implementation, and we suggest to not use this node currently. + > [!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..2b58140 --- /dev/null +++ b/ext/EMIExt/EMIExt.jl @@ -0,0 +1,229 @@ +module EMIExt + +using TimeStruct +using EnergyModelsBase +using EnergyModelsInvestments +using EnergyModelsHeat +using EnergyModelsLanguageInterfaces +using Libdl + +const TS = TimeStruct +const EMB = EnergyModelsBase +const EMI = EnergyModelsInvestments +const EMH = EnergyModelsHeat +const EMLI = EnergyModelsLanguageInterfaces + +""" + EMLI.BioCHP( + id::Any, + cap::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. +- **`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. +- **`cap_init`** is the initial capacity of the `BioCHP`-node. + +!!! note ""EmissionsEnergy" + If `EmissionsEnergy` is not included in the `data` field, it is automatically added. +""" +function EMLI.BioCHP( + id::Any, + cap::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_init::TimeProfile = FixedProfile(0), +) + + # Get the capacity + el_capacity = cap.val + + # fuel_def: name of each biomass feedstock + bio_resources::Vector{ResourceBio} = collect(keys(mass_fractions)) + fuel_def_strings = [EMLI.bio_type(res) for res โˆˆ bio_resources] + + # Create pointers + fuel_def_buffers = [Vector{UInt8}(string(s, '\0')) for s โˆˆ fuel_def_strings] + fuel_def_ptrs = [pointer(buf) for buf โˆˆ fuel_def_buffers] + fuel_def_ptr_array = pointer(fuel_def_ptrs) + + # Create mass fractions from input that sum up to 1 + normalization = sum(mass_fractions[res] for res โˆˆ bio_resources) + normalized_mass_fractions = Dict{ResourceBio,Float64}( + res => mass_fractions[res] / normalization for res โˆˆ bio_resources + ) + + # Yj: mass fraction of each biomass feedstock + # W_el: electric power output (MW_el) + # Qk: heat demand (MW) + # Tk_in: Return temperature for each heat demand (district heating) + # Tk_in: Supply temperature for each heat demand (district heating) + heat_resources::Vector{ResourceHeat} = collect(keys(heat_output_ratios)) + Qk_dict::Dict{Resource,Real} = Dict{Resource,Real}( + res => heat_output_ratios[res] * el_capacity for res โˆˆ heat_resources + ) + Yj::Vector{Cdouble} = [normalized_mass_fractions[res] for res โˆˆ bio_resources] + YH2Oj::Vector{Cdouble} = EMLI.moisture.(bio_resources) + W_el::Cdouble = el_capacity + Qk::Vector{Cdouble} = [] + Tk_in::Vector{Cdouble} = [] + Tk_out::Vector{Cdouble} = [] + for res โˆˆ heat_resources + # Get the heat demand + push!(Qk, Qk_dict[res]) + + # Get the supply temperature + supply_heat_profile = EMH.t_supply(res) + if !isa(supply_heat_profile, FixedProfile) + @error "Current implementation require the supply heat profile to be fixed." + else + push!(Tk_in, supply_heat_profile.val) + end + + # Get the return temperature + return_heat_profile = EMH.t_return(res) + if !isa(return_heat_profile, FixedProfile) + @error "Current implementation require the return heat profile to be fixed." + else + push!(Tk_out, return_heat_profile.val) + end + end + + # Preallocate output variables + # Mj: Required mass flow of each biomass feedstock + # C_inv: Investment cost + # C_op: Variable operating cost + Mj::Vector{Cdouble} = zeros(length(Yj)) # Output vector + Q_prod::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double + W_el_prod::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double + C_inv::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double + C_op::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double + C_op_var::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double + + # Get lengths of the vectors + len_Yj::Cint = length(Yj) + len_YH2Oj::Cint = length(YH2Oj) + len_Qk::Cint = length(Qk) + len_Tk_in::Cint = length(Tk_in) + len_Tk_out::Cint = length(Tk_out) + len_Mj::Cint = length(Mj) + len_fuel_def_ptrs::Cint = length(fuel_def_ptrs) + + # Load the library if it's not already cached + if !haskey(EMLI.LIB_CACHE, libpath) + @info "Loading the C module $libpath" + EMLI.LIB_CACHE[libpath] = Libdl.dlopen(libpath) + end + lib = EMLI.LIB_CACHE[libpath] + lib = Libdl.dlopen(libpath) + + # Call the shared C function from the loaded library + err_ref = Ref{Cstring}() + ret = @ccall $(EMLI.@dlsym(lib, :bioCHP_plant_c))( + fuel_def_ptr_array::Ptr{Cstring}, len_fuel_def_ptrs::Cint, + Yj::Ptr{Cdouble}, len_Yj::Cint, + YH2Oj::Ptr{Cdouble}, len_YH2Oj::Cint, + W_el::Cdouble, + Qk::Ptr{Cdouble}, len_Qk::Cint, + Tk_in::Ptr{Cdouble}, len_Tk_in::Cint, + Tk_out::Ptr{Cdouble}, len_Tk_out::Cint, + Mj::Ptr{Cdouble}, len_Mj::Cint, + Q_prod::Ref{Cdouble}, + W_el_prod::Ref{Cdouble}, + C_inv::Ref{Cdouble}, + C_op::Ref{Cdouble}, + C_op_var::Ref{Cdouble}, + err_ref::Ref{Cstring}, + )::Cint + if ret != 0 + msg = err_ref[] == C_NULL ? "unknown error" : unsafe_string(err_ref[]) + if err_ref[] != C_NULL + ccall(:free, Cvoid, (Ptr{Cvoid},), Ptr{Cvoid}(err_ref[])) + end + error("CHP_modelling library failed: $msg") + end + + input_updated = Dict{ResourceBio,Real}( + res => Mj[i] / W_el_prod[] for (i, res) โˆˆ enumerate(bio_resources) + ) + cap_updated = FixedProfile(Float64(W_el_prod[])) + + 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 + + if !(EmissionsEnergy โˆˆ typeof.(data)) + push!(data, EmissionsEnergy()) + end + if !any(isa(d, Investment) for d โˆˆ data) + push!(data, + SingleInvData( + FixedProfile(Float64(C_inv[]/W_el_prod[])), # Capex in EUR/MW + cap_updated, # Max installed capacity [MW] + 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_init, + electricity_resource, + opex_var, + opex_fixed, + input_updated, + output, + data, + ) +end + +end diff --git a/src/EnergyModelsLanguageInterfaces.jl b/src/EnergyModelsLanguageInterfaces.jl index dd3e490..e5a39a7 100644 --- a/src/EnergyModelsLanguageInterfaces.jl +++ b/src/EnergyModelsLanguageInterfaces.jl @@ -7,7 +7,6 @@ module EnergyModelsLanguageInterfaces using JuMP using TimeStruct using EnergyModelsBase -using EnergyModelsInvestments using EnergyModelsRenewableProducers using EnergyModelsHeat using Dates diff --git a/src/datastructures.jl b/src/datastructures.jl index a1c19ef..7472962 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -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, ) @@ -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, ) @@ -644,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 @@ -678,214 +678,6 @@ function BioCHP( ) end -""" - BioCHP( - id::Any, - cap::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. -- **`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 BioCHP( - id::Any, - cap::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", - ), -) - - # Get the capacity - el_capacity = cap.val - - # fuel_def: name of each biomass feedstock - bio_resources::Vector{ResourceBio} = collect(keys(mass_fractions)) - fuel_def_strings = [bio_type(res) for res โˆˆ bio_resources] - - # Create pointers - fuel_def_buffers = [Vector{UInt8}(string(s, '\0')) for s โˆˆ fuel_def_strings] - fuel_def_ptrs = [pointer(buf) for buf โˆˆ fuel_def_buffers] - fuel_def_ptr_array = pointer(fuel_def_ptrs) - - # Create mass fractions from input that sum up to 1 - normalization = sum(mass_fractions[res] for res โˆˆ bio_resources) - normalized_mass_fractions = Dict{ResourceBio,Float64}( - res => mass_fractions[res] / normalization for res โˆˆ bio_resources - ) - - # Yj: mass fraction of each biomass feedstock - # W_el: electric power output (MW_el) - # Qk: heat demand (MW) - # Tk_in: Return temperature for each heat demand (district heating) - # Tk_in: Supply temperature for each heat demand (district heating) - heat_resources::Vector{ResourceHeat} = collect(keys(heat_output_ratios)) - Qk_dict::Dict{Resource,Real} = Dict{Resource,Real}( - res => heat_output_ratios[res] * el_capacity for res โˆˆ heat_resources - ) - Yj::Vector{Cdouble} = [normalized_mass_fractions[res] for res โˆˆ bio_resources] - YH2Oj::Vector{Cdouble} = moisture.(bio_resources) - W_el::Cdouble = el_capacity - Qk::Vector{Cdouble} = [] - Tk_in::Vector{Cdouble} = [] - Tk_out::Vector{Cdouble} = [] - for res โˆˆ heat_resources - # Get the heat demand - push!(Qk, Qk_dict[res]) - - # Get the supply temperature - supply_heat_profile = EMH.t_supply(res) - if !isa(supply_heat_profile, FixedProfile) - @error "Current implementation require the supply heat profile to be fixed." - else - push!(Tk_in, supply_heat_profile.val) - end - - # Get the return temperature - return_heat_profile = EMH.t_return(res) - if !isa(return_heat_profile, FixedProfile) - @error "Current implementation require the return heat profile to be fixed." - else - push!(Tk_out, return_heat_profile.val) - end - end - - # Preallocate output variables - # Mj: Required mass flow of each biomass feedstock - # C_inv: Investment cost - # C_op: Variable operating cost - Mj::Vector{Cdouble} = zeros(length(Yj)) # Output vector - Q_prod::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double - W_el_prod::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double - C_inv::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double - C_op::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double - C_op_var::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double - - # Get lengths of the vectors - len_Yj::Cint = length(Yj) - len_YH2Oj::Cint = length(YH2Oj) - len_Qk::Cint = length(Qk) - len_Tk_in::Cint = length(Tk_in) - len_Tk_out::Cint = length(Tk_out) - len_Mj::Cint = length(Mj) - len_fuel_def_ptrs::Cint = length(fuel_def_ptrs) - - # Load the library if it's not already cached - if !haskey(LIB_CACHE, libpath) - @info "Loading the C module $libpath" - LIB_CACHE[libpath] = Libdl.dlopen(libpath) - end - lib = LIB_CACHE[libpath] - lib = Libdl.dlopen(libpath) - - # Call the shared C function from the loaded library - err_ref = Ref{Cstring}() - ret = @ccall $(@dlsym(lib, :bioCHP_plant_c))( - fuel_def_ptr_array::Ptr{Cstring}, len_fuel_def_ptrs::Cint, - Yj::Ptr{Cdouble}, len_Yj::Cint, - YH2Oj::Ptr{Cdouble}, len_YH2Oj::Cint, - W_el::Cdouble, - Qk::Ptr{Cdouble}, len_Qk::Cint, - Tk_in::Ptr{Cdouble}, len_Tk_in::Cint, - Tk_out::Ptr{Cdouble}, len_Tk_out::Cint, - Mj::Ptr{Cdouble}, len_Mj::Cint, - Q_prod::Ref{Cdouble}, - W_el_prod::Ref{Cdouble}, - C_inv::Ref{Cdouble}, - C_op::Ref{Cdouble}, - C_op_var::Ref{Cdouble}, - err_ref::Ref{Cstring}, - )::Cint - if ret != 0 - msg = err_ref[] == C_NULL ? "unknown error" : unsafe_string(err_ref[]) - if err_ref[] != C_NULL - ccall(:free, Cvoid, (Ptr{Cvoid},), Ptr{Cvoid}(err_ref[])) - end - error("CHP_modelling library failed: $msg") - end - - input_updated = Dict{ResourceBio,Real}( - res => Mj[i] / W_el_prod[] for (i, res) โˆˆ enumerate(bio_resources) - ) - cap_updated = FixedProfile(Float64(W_el_prod[])) - - opex_fixed = FixedProfile(Float64(C_op[])) - opex_var = FixedProfile(Float64(C_op_var[] / 8760)) - - output = Dict{Resource,Real}( - resource => val / W_el_prod[] for (resource, val) โˆˆ Qk_dict - ) - output[electricity_resource] = 1.0 - - if !(EmissionsEnergy โˆˆ typeof.(data)) - push!(data, EmissionsEnergy()) - end - if !any(isa(d, Investment) for d โˆˆ data) - push!(data, - SingleInvData( - FixedProfile(Float64(C_inv[])), # Capex in EUR/MW - cap_updated, # Max installed capacity [MW] - 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 BioCHP( - id, - FixedProfile(0), - electricity_resource, - opex_var, - opex_fixed, - input_updated, - output, - data, - ) -end - """ electricity_resource(n::BioCHP) 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 a5f9d4a..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 diff --git a/test/test_bioCHP.jl b/test/test_bioCHP.jl index 446a637..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]) โ‰ˆ 18111.756783644374 + @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]) for t โˆˆ ๐’ฏ) โ‰ˆ 5044.854273631571 + @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 โˆˆ ๐’ฏ) โ‰ˆ 29416.180912105232 + @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 โˆˆ ๐’ฏ) From 9e6c530388f332e85d87fc0cac1f6f911e80fe50 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 18 Dec 2025 11:57:06 +0100 Subject: [PATCH 7/9] Minor update to README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 12aa6b0..e2caab2 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,14 @@ 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 MultipleBuildings node is currently under development and may contain incorrect implementation, and we suggest to not use this node currently. +> 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. From 7da4216bcea23f1a724962f22033ed6d4f70a168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Vegard=20Ven=C3=A5s?= Date: Thu, 18 Dec 2025 12:42:03 +0100 Subject: [PATCH 8/9] Add suggestions from review --- ext/EMIExt/EMIExt.jl | 149 ++++------------------------ src/datastructures.jl | 219 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+), 132 deletions(-) diff --git a/ext/EMIExt/EMIExt.jl b/ext/EMIExt/EMIExt.jl index 2b58140..2ea202b 100644 --- a/ext/EMIExt/EMIExt.jl +++ b/ext/EMIExt/EMIExt.jl @@ -5,7 +5,6 @@ using EnergyModelsBase using EnergyModelsInvestments using EnergyModelsHeat using EnergyModelsLanguageInterfaces -using Libdl const TS = TimeStruct const EMB = EnergyModelsBase @@ -17,6 +16,8 @@ const EMLI = EnergyModelsLanguageInterfaces EMLI.BioCHP( id::Any, cap::TimeProfile, + cap_init::TimeProfile, + max_installed::TimeProfile, mass_fractions::Dict{<:ResourceBio,<:Real}, heat_output_ratios::Dict{<:ResourceHeat,<:Real}, electricity_resource::Resource; @@ -41,6 +42,8 @@ library file located at `libpath`. The BioCHP has electricity production of the # Arguments - **`id`** is the name or identifier of the node. - **`cap`** is the installed electric capacity. +- **`cap_init`** is the initial capacity. +- **`max_installed`** is the maximal 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. @@ -50,7 +53,6 @@ library file located at `libpath`. The BioCHP has electricity production of the - **`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. -- **`cap_init`** is the initial capacity of the `BioCHP`-node. !!! note ""EmissionsEnergy" If `EmissionsEnergy` is not included in the `data` field, it is automatically added. @@ -58,6 +60,8 @@ library file located at `libpath`. The BioCHP has electricity production of the function EMLI.BioCHP( id::Any, cap::TimeProfile, + cap_init::TimeProfile, + max_installed::TimeProfile, mass_fractions::Dict{<:ResourceBio,<:Real}, heat_output_ratios::Dict{<:ResourceHeat,<:Real}, electricity_resource::Resource; @@ -71,141 +75,22 @@ function EMLI.BioCHP( "lib", "libbioCHP_wrapper.so", ), - cap_init::TimeProfile = FixedProfile(0), ) - - # Get the capacity - el_capacity = cap.val - - # fuel_def: name of each biomass feedstock - bio_resources::Vector{ResourceBio} = collect(keys(mass_fractions)) - fuel_def_strings = [EMLI.bio_type(res) for res โˆˆ bio_resources] - - # Create pointers - fuel_def_buffers = [Vector{UInt8}(string(s, '\0')) for s โˆˆ fuel_def_strings] - fuel_def_ptrs = [pointer(buf) for buf โˆˆ fuel_def_buffers] - fuel_def_ptr_array = pointer(fuel_def_ptrs) - - # Create mass fractions from input that sum up to 1 - normalization = sum(mass_fractions[res] for res โˆˆ bio_resources) - normalized_mass_fractions = Dict{ResourceBio,Float64}( - res => mass_fractions[res] / normalization for res โˆˆ bio_resources - ) - - # Yj: mass fraction of each biomass feedstock - # W_el: electric power output (MW_el) - # Qk: heat demand (MW) - # Tk_in: Return temperature for each heat demand (district heating) - # Tk_in: Supply temperature for each heat demand (district heating) - heat_resources::Vector{ResourceHeat} = collect(keys(heat_output_ratios)) - Qk_dict::Dict{Resource,Real} = Dict{Resource,Real}( - res => heat_output_ratios[res] * el_capacity for res โˆˆ heat_resources - ) - Yj::Vector{Cdouble} = [normalized_mass_fractions[res] for res โˆˆ bio_resources] - YH2Oj::Vector{Cdouble} = EMLI.moisture.(bio_resources) - W_el::Cdouble = el_capacity - Qk::Vector{Cdouble} = [] - Tk_in::Vector{Cdouble} = [] - Tk_out::Vector{Cdouble} = [] - for res โˆˆ heat_resources - # Get the heat demand - push!(Qk, Qk_dict[res]) - - # Get the supply temperature - supply_heat_profile = EMH.t_supply(res) - if !isa(supply_heat_profile, FixedProfile) - @error "Current implementation require the supply heat profile to be fixed." - else - push!(Tk_in, supply_heat_profile.val) - end - - # Get the return temperature - return_heat_profile = EMH.t_return(res) - if !isa(return_heat_profile, FixedProfile) - @error "Current implementation require the return heat profile to be fixed." - else - push!(Tk_out, return_heat_profile.val) - end - end - - # Preallocate output variables - # Mj: Required mass flow of each biomass feedstock - # C_inv: Investment cost - # C_op: Variable operating cost - Mj::Vector{Cdouble} = zeros(length(Yj)) # Output vector - Q_prod::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double - W_el_prod::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double - C_inv::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double - C_op::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double - C_op_var::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double - - # Get lengths of the vectors - len_Yj::Cint = length(Yj) - len_YH2Oj::Cint = length(YH2Oj) - len_Qk::Cint = length(Qk) - len_Tk_in::Cint = length(Tk_in) - len_Tk_out::Cint = length(Tk_out) - len_Mj::Cint = length(Mj) - len_fuel_def_ptrs::Cint = length(fuel_def_ptrs) - - # Load the library if it's not already cached - if !haskey(EMLI.LIB_CACHE, libpath) - @info "Loading the C module $libpath" - EMLI.LIB_CACHE[libpath] = Libdl.dlopen(libpath) - end - lib = EMLI.LIB_CACHE[libpath] - lib = Libdl.dlopen(libpath) - - # Call the shared C function from the loaded library - err_ref = Ref{Cstring}() - ret = @ccall $(EMLI.@dlsym(lib, :bioCHP_plant_c))( - fuel_def_ptr_array::Ptr{Cstring}, len_fuel_def_ptrs::Cint, - Yj::Ptr{Cdouble}, len_Yj::Cint, - YH2Oj::Ptr{Cdouble}, len_YH2Oj::Cint, - W_el::Cdouble, - Qk::Ptr{Cdouble}, len_Qk::Cint, - Tk_in::Ptr{Cdouble}, len_Tk_in::Cint, - Tk_out::Ptr{Cdouble}, len_Tk_out::Cint, - Mj::Ptr{Cdouble}, len_Mj::Cint, - Q_prod::Ref{Cdouble}, - W_el_prod::Ref{Cdouble}, - C_inv::Ref{Cdouble}, - C_op::Ref{Cdouble}, - C_op_var::Ref{Cdouble}, - err_ref::Ref{Cstring}, - )::Cint - if ret != 0 - msg = err_ref[] == C_NULL ? "unknown error" : unsafe_string(err_ref[]) - if err_ref[] != C_NULL - ccall(:free, Cvoid, (Ptr{Cvoid},), Ptr{Cvoid}(err_ref[])) - end - error("CHP_modelling library failed: $msg") - end - - input_updated = Dict{ResourceBio,Real}( - res => Mj[i] / W_el_prod[] for (i, res) โˆˆ enumerate(bio_resources) - ) - cap_updated = FixedProfile(Float64(W_el_prod[])) - - 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 + cap_updated, opex_var, opex_fixed, input_updated, output, data, capex = EMLI.BioCHP( + cap, + mass_fractions, + heat_output_ratios, + electricity_resource, + data, + libpath, ) - output[electricity_resource] = 1.0 - if !(EmissionsEnergy โˆˆ typeof.(data)) - push!(data, EmissionsEnergy()) - end if !any(isa(d, Investment) for d โˆˆ data) push!(data, SingleInvData( - FixedProfile(Float64(C_inv[]/W_el_prod[])), # Capex in EUR/MW - cap_updated, # Max installed capacity [MW] + FixedProfile(capex), # Capex in EUR/MW + 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] @@ -216,7 +101,7 @@ function EMLI.BioCHP( return EMLI.BioCHP( id, - cap_init, + cap_updated, electricity_resource, opex_var, opex_fixed, diff --git a/src/datastructures.jl b/src/datastructures.jl index 7472962..35ab240 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -678,6 +678,225 @@ function BioCHP( ) end +""" + BioCHP( + id::Any, + cap::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. +- **`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. +- **`cap_init`** is the initial capacity of the `BioCHP`-node. + +!!! note ""EmissionsEnergy" + If `EmissionsEnergy` is not included in the `data` field, it is automatically added. +""" +function BioCHP( + id::Any, + cap::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, _ = 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 + + # fuel_def: name of each biomass feedstock + bio_resources::Vector{ResourceBio} = collect(keys(mass_fractions)) + fuel_def_strings = [bio_type(res) for res โˆˆ bio_resources] + + # Create pointers + fuel_def_buffers = [Vector{UInt8}(string(s, '\0')) for s โˆˆ fuel_def_strings] + fuel_def_ptrs = [pointer(buf) for buf โˆˆ fuel_def_buffers] + fuel_def_ptr_array = pointer(fuel_def_ptrs) + + # Create mass fractions from input that sum up to 1 + normalization = sum(mass_fractions[res] for res โˆˆ bio_resources) + normalized_mass_fractions = Dict{ResourceBio,Float64}( + res => mass_fractions[res] / normalization for res โˆˆ bio_resources + ) + + # Yj: mass fraction of each biomass feedstock + # W_el: electric power output (MW_el) + # Qk: heat demand (MW) + # Tk_in: Return temperature for each heat demand (district heating) + # Tk_in: Supply temperature for each heat demand (district heating) + heat_resources::Vector{ResourceHeat} = collect(keys(heat_output_ratios)) + Qk_dict::Dict{Resource,Real} = Dict{Resource,Real}( + res => heat_output_ratios[res] * el_capacity for res โˆˆ heat_resources + ) + Yj::Vector{Cdouble} = [normalized_mass_fractions[res] for res โˆˆ bio_resources] + YH2Oj::Vector{Cdouble} = moisture.(bio_resources) + W_el::Cdouble = el_capacity + Qk::Vector{Cdouble} = [] + Tk_in::Vector{Cdouble} = [] + Tk_out::Vector{Cdouble} = [] + for res โˆˆ heat_resources + # Get the heat demand + push!(Qk, Qk_dict[res]) + + # Get the supply temperature + supply_heat_profile = EMH.t_supply(res) + if !isa(supply_heat_profile, FixedProfile) + @error "Current implementation require the supply heat profile to be fixed." + else + push!(Tk_in, supply_heat_profile.val) + end + + # Get the return temperature + return_heat_profile = EMH.t_return(res) + if !isa(return_heat_profile, FixedProfile) + @error "Current implementation require the return heat profile to be fixed." + else + push!(Tk_out, return_heat_profile.val) + end + end + + # Preallocate output variables + # Mj: Required mass flow of each biomass feedstock + # C_inv: Investment cost + # C_op: Variable operating cost + Mj::Vector{Cdouble} = zeros(length(Yj)) # Output vector + Q_prod::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double + W_el_prod::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double + C_inv::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double + C_op::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double + C_op_var::Ref{Cdouble} = Ref{Cdouble}(0.0) # Output double + + # Get lengths of the vectors + len_Yj::Cint = length(Yj) + len_YH2Oj::Cint = length(YH2Oj) + len_Qk::Cint = length(Qk) + len_Tk_in::Cint = length(Tk_in) + len_Tk_out::Cint = length(Tk_out) + len_Mj::Cint = length(Mj) + len_fuel_def_ptrs::Cint = length(fuel_def_ptrs) + + # Load the library if it's not already cached + if !haskey(LIB_CACHE, libpath) + @info "Loading the C module $libpath" + LIB_CACHE[libpath] = Libdl.dlopen(libpath) + end + lib = LIB_CACHE[libpath] + lib = Libdl.dlopen(libpath) + + # Call the shared C function from the loaded library + err_ref = Ref{Cstring}() + ret = @ccall $(@dlsym(lib, :bioCHP_plant_c))( + fuel_def_ptr_array::Ptr{Cstring}, len_fuel_def_ptrs::Cint, + Yj::Ptr{Cdouble}, len_Yj::Cint, + YH2Oj::Ptr{Cdouble}, len_YH2Oj::Cint, + W_el::Cdouble, + Qk::Ptr{Cdouble}, len_Qk::Cint, + Tk_in::Ptr{Cdouble}, len_Tk_in::Cint, + Tk_out::Ptr{Cdouble}, len_Tk_out::Cint, + Mj::Ptr{Cdouble}, len_Mj::Cint, + Q_prod::Ref{Cdouble}, + W_el_prod::Ref{Cdouble}, + C_inv::Ref{Cdouble}, + C_op::Ref{Cdouble}, + C_op_var::Ref{Cdouble}, + err_ref::Ref{Cstring}, + )::Cint + if ret != 0 + msg = err_ref[] == C_NULL ? "unknown error" : unsafe_string(err_ref[]) + if err_ref[] != C_NULL + ccall(:free, Cvoid, (Ptr{Cvoid},), Ptr{Cvoid}(err_ref[])) + end + error("CHP_modelling library failed: $msg") + end + + input_updated = Dict{ResourceBio,Real}( + res => Mj[i] / W_el_prod[] for (i, res) โˆˆ enumerate(bio_resources) + ) + cap_updated = FixedProfile(Float64(W_el_prod[])) + + 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 cap_updated, opex_var, opex_fixed, input_updated, output, data, capex +end + """ electricity_resource(n::BioCHP) From ec046572f3833d172ac86d3d7123111dad2e42cd Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 18 Dec 2025 12:55:35 +0100 Subject: [PATCH 9/9] Minor update to the docstrings and `Vector{<:Data}` --- ext/EMIExt/EMIExt.jl | 12 ++++++------ src/datastructures.jl | 22 ++++++++++------------ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/ext/EMIExt/EMIExt.jl b/ext/EMIExt/EMIExt.jl index 2ea202b..4a5d8d5 100644 --- a/ext/EMIExt/EMIExt.jl +++ b/ext/EMIExt/EMIExt.jl @@ -17,7 +17,7 @@ const EMLI = EnergyModelsLanguageInterfaces id::Any, cap::TimeProfile, cap_init::TimeProfile, - max_installed::TimeProfile, + cap_max_installed::TimeProfile, mass_fractions::Dict{<:ResourceBio,<:Real}, heat_output_ratios::Dict{<:ResourceHeat,<:Real}, electricity_resource::Resource; @@ -41,9 +41,9 @@ library file located at `libpath`. The BioCHP has electricity production of the # Arguments - **`id`** is the name or identifier of the node. -- **`cap`** is the installed electric capacity. -- **`cap_init`** is the initial capacity. -- **`max_installed`** is the maximal installed capacity. +- **`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. @@ -61,7 +61,7 @@ function EMLI.BioCHP( id::Any, cap::TimeProfile, cap_init::TimeProfile, - max_installed::TimeProfile, + cap_max_installed::TimeProfile, mass_fractions::Dict{<:ResourceBio,<:Real}, heat_output_ratios::Dict{<:ResourceHeat,<:Real}, electricity_resource::Resource; @@ -89,7 +89,7 @@ function EMLI.BioCHP( push!(data, SingleInvData( FixedProfile(capex), # Capex in EUR/MW - max_installed, # Max installed capacity [MW] + cap_max_installed, # Max installed capacity [MW] cap_init, ContinuousInvestment(FixedProfile(0), cap_updated), # Line above: Investment mode with the following arguments: diff --git a/src/datastructures.jl b/src/datastructures.jl index 35ab240..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 = "" ) @@ -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 @@ -349,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. @@ -456,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 @@ -685,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__, "..", @@ -712,10 +712,8 @@ 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. -- **`cap_init`** is the initial capacity of the `BioCHP`-node. !!! note ""EmissionsEnergy" If `EmissionsEnergy` is not included in the `data` field, it is automatically added. @@ -726,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__, "..", @@ -762,7 +760,7 @@ function BioCHP( mass_fractions::Dict{<:ResourceBio,<:Real}, heat_output_ratios::Dict{<:ResourceHeat,<:Real}, electricity_resource::Resource, - data::Vector{Data}, + data::Vector{<:Data}, libpath::String, ) # Get the capacity