From 6e0e78993a964abe98afddcc63788b1edff2e34f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Espen=20Flo=20B=C3=B8dal?= Date: Wed, 9 Jul 2025 10:30:01 +0200 Subject: [PATCH 01/15] Added functions for enabling complex resource flow --- src/constraint_functions.jl | 2 ++ src/model.jl | 47 +++++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/constraint_functions.jl b/src/constraint_functions.jl index 9ee64da..c16d053 100644 --- a/src/constraint_functions.jl +++ b/src/constraint_functions.jl @@ -149,6 +149,8 @@ This function serves as fallback option if no other function is specified for a """ function constraints_trans_balance(m, tm::TransmissionMode, 𝒯::TimeStructure, modeltype::EnergyModel) + @info "Creating transmission balance constraints for TransmissionMode: $(tm.id) of type $(typeof(tm))" + @constraint(m, [t ∈ 𝒯], m[:trans_out][tm, t] == m[:trans_in][tm, t] - m[:trans_loss][tm, t]) diff --git a/src/model.jl b/src/model.jl index 40d7319..bf668e7 100644 --- a/src/model.jl +++ b/src/model.jl @@ -75,19 +75,41 @@ hence, provides the user with two individual methods: period `t`. The exchange resources are extracted using the function [`exchange_resources`](@ref) """ -function EMB.variables_flow(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, π’³α΅›α΅‰αΆœ, 𝒯, modeltype::EnergyModel) +function EMB.variables_flow(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, π’³α΅›α΅‰αΆœ, 𝒫, 𝒯, modeltype::EnergyModel) # Extract the individual transmission modes β„³ = modes(ℒᡗʳᡃⁿ˒) # Create the transmission mode flow variables @variable(m, trans_in[β„³, 𝒯]) @variable(m, trans_out[β„³, 𝒯]) + + # Create new flow variables for specific resource types + for p_sub in EMB.res_types_seg(𝒫) + EMB.variables_flow_resource(m, β„³, p_sub, 𝒯, modeltype) + end end -function EMB.variables_flow(m, π’œ::Vector{<:Area}, π’³α΅›α΅‰αΆœ, 𝒯, modeltype::EnergyModel) +function EMB.variables_flow(m, π’œ::Vector{<:Area}, π’³α΅›α΅‰αΆœ, 𝒫, 𝒯, modeltype::EnergyModel) ℒᡗʳᡃⁿ˒ = get_transmissions(π’³α΅›α΅‰αΆœ) @variable(m, area_exchange[a ∈ π’œ, 𝒯, p ∈ exchange_resources(ℒᡗʳᡃⁿ˒, a)]) + + # Create new flow variables for specific resource types + for p_sub in EMB.res_types_seg(𝒫) + EMB.variables_flow_resource(m, π’œ, p_sub, 𝒯, modeltype) + end end + +""" + variables_flow_resource(m, π’œ::Vector{<:TransmissionMode}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end + variables_flow_resource(m, β„’::Vector{<:Area}, 𝒫::Vector{Resource}, 𝒯, modeltype::EnergyModel) + +Declaration of flow variables for the differrent resource types. + +The default method is empty but it is required for multiple dispatch in energy flow models. +""" +function EMB.variables_flow_resource(m, π’œ::Vector{<:TransmissionMode}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end +function EMB.variables_flow_resource(m, π’œ::Vector{<:Area}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end + """ EMB.variables_opex(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, π’³α΅›α΅‰αΆœ, 𝒯, modeltype::EnergyModel) EMB.variables_opex(m, π’œ::Vector{<:Area}, π’³α΅›α΅‰αΆœ, 𝒯, modeltype::EnergyModel) @@ -313,11 +335,23 @@ function EMB.constraints_couple(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vecto sum(compute_trans_out(m, t, p, tm) for tm ∈ modes(β„’α΅—α΅’)) ) end + + # Create new constraints for specific resource types + for p_sub in EMB.res_types_seg(𝒫) + constraints_couple_resource(m, π’œ, ℒᡗʳᡃⁿ˒, p_sub, 𝒯, modeltype) + end end function EMB.constraints_couple(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, π’œ::Vector{<:Area}, 𝒫, 𝒯, modeltype::EnergyModel) return EMB.constraints_couple(m, π’œ, ℒᡗʳᡃⁿ˒, 𝒫, 𝒯, modeltype) end +""" + constraints_couple_resource(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{Transmission}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) + +Create constraints for output flowrate and input links. +""" +function constraints_couple_resource(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{<:Transmission}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) println("Hei her er jeg!!!") end + """ EMB.emissions_operational(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, 𝒫ᡉᡐ, 𝒯, modeltype::EnergyModel) @@ -389,9 +423,14 @@ end Set all constraints for a [`GeoAvailability`](@ref). The energy balance is handled in the function [`constraints_couple`](@ref EnergyModelsBase.constraints_couple). -Hence, no constraints are added in this function. """ -function EMB.create_node(m, n::GeoAvailability, 𝒯, 𝒫, modeltype::EnergyModel) end +function EMB.create_node(m, n::GeoAvailability, 𝒯, 𝒫, modeltype::EnergyModel) + + # Constraints based on the resource types + for p_sub in EMB.res_types_seg(inputs(n)) + EMB.constraints_flow_resource(m, n, 𝒯, p_sub, modeltype) + end +end """ create_area(m, a::Area, 𝒯, ℒᡗʳᡃⁿ˒, modeltype) From d4df739cfbbef46d855939b095c1b42c524f320e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Espen=20Flo=20B=C3=B8dal?= Date: Wed, 9 Jul 2025 10:40:36 +0200 Subject: [PATCH 02/15] Remove temp debug line --- src/model.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model.jl b/src/model.jl index bf668e7..39bbbe6 100644 --- a/src/model.jl +++ b/src/model.jl @@ -350,7 +350,7 @@ end Create constraints for output flowrate and input links. """ -function constraints_couple_resource(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{<:Transmission}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) println("Hei her er jeg!!!") end +function constraints_couple_resource(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{<:Transmission}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end """ EMB.emissions_operational(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, 𝒫ᡉᡐ, 𝒯, modeltype::EnergyModel) From 40d961c67b3c25b7c847388002dde7deba98bb84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Espen=20Flo=20B=C3=B8dal?= Date: Wed, 1 Oct 2025 10:58:24 +0200 Subject: [PATCH 03/15] Changed name of resource constraint function --- src/model.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model.jl b/src/model.jl index 39bbbe6..7f4e271 100644 --- a/src/model.jl +++ b/src/model.jl @@ -428,7 +428,7 @@ function EMB.create_node(m, n::GeoAvailability, 𝒯, 𝒫, modeltype::EnergyMod # Constraints based on the resource types for p_sub in EMB.res_types_seg(inputs(n)) - EMB.constraints_flow_resource(m, n, 𝒯, p_sub, modeltype) + EMB.constraints_resource(m, n, 𝒯, p_sub, modeltype) end end From 43fb5282ded7f1fa49abad507925b9be1d6a6e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Espen=20Flo=20B=C3=B8dal?= Date: Thu, 19 Feb 2026 10:23:58 +0100 Subject: [PATCH 04/15] Removed duplication of constraint_resource * removed constraint_resource in create_node GeoAvailability --- src/model.jl | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/model.jl b/src/model.jl index 7f4e271..2e7a04b 100644 --- a/src/model.jl +++ b/src/model.jl @@ -424,13 +424,7 @@ Set all constraints for a [`GeoAvailability`](@ref). The energy balance is handl function [`constraints_couple`](@ref EnergyModelsBase.constraints_couple). """ -function EMB.create_node(m, n::GeoAvailability, 𝒯, 𝒫, modeltype::EnergyModel) - - # Constraints based on the resource types - for p_sub in EMB.res_types_seg(inputs(n)) - EMB.constraints_resource(m, n, 𝒯, p_sub, modeltype) - end -end +function EMB.create_node(m, n::GeoAvailability, 𝒯, 𝒫, modeltype::EnergyModel) end """ create_area(m, a::Area, 𝒯, ℒᡗʳᡃⁿ˒, modeltype) From 2773bf5b76482bab45ba4b8bbe00169251409c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Espen=20Flo=20B=C3=B8dal?= Date: Mon, 13 Apr 2026 16:59:30 +0200 Subject: [PATCH 05/15] feat: document resource-specific flow hooks and add PotentialPower transmission test - document new resource-segment extension points for flow variables and coupling constraints - expand how-to guidance with user-facing migration notes for resource-specific dispatch - improve model docstrings to clarify fallback hook behavior for extension packages - add a dedicated resource-flow test module with: - custom PotentialPower resource - custom PotentialLossMode transmission mode - two-area transmission corridor case validating potential loss behavior - include the new resource-flow test in the test runner - align project and test environments with EnergyModelsBase 0.10 and EnergyModelsInvestments 0.9 --- Project.toml | 4 +- docs/src/how-to/update-models.md | 26 +++ docs/src/library/internals/functions.md | 2 + docs/src/manual/constraint-functions.md | 5 + src/model.jl | 21 ++- test/Project.toml | 1 + test/runtests.jl | 4 + test/test_area.jl | 1 - test/test_resource_flow.jl | 236 ++++++++++++++++++++++++ 9 files changed, 291 insertions(+), 9 deletions(-) create mode 100644 test/test_resource_flow.jl diff --git a/Project.toml b/Project.toml index 0825870..3e6f070 100644 --- a/Project.toml +++ b/Project.toml @@ -16,8 +16,8 @@ EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" EMIExt = "EnergyModelsInvestments" [compat] -EnergyModelsBase = "0.9.0" -EnergyModelsInvestments = "0.8" +EnergyModelsBase = "0.10.0" +EnergyModelsInvestments = "0.9" SparseVariables = "0.7.3" JuMP = "1.5" TimeStruct = "0.9" diff --git a/docs/src/how-to/update-models.md b/docs/src/how-to/update-models.md index ffe9ca1..b763a65 100644 --- a/docs/src/how-to/update-models.md +++ b/docs/src/how-to/update-models.md @@ -35,6 +35,32 @@ case = Case( Furthermore, we reworked the design for inclusion of emission and OPEX variables. +### [Resource-specific variables and coupling constraints](@id how_to-update-10-resource_dispatch) + +Starting from this update, `EnergyModelsGeography` follows the same extension pattern as +`EnergyModelsBase` for resource-specific dispatch in model construction. + +The methods `EMB.variables_flow` and `EMB.constraints_couple` now segment the full resource +vector `𝒫` by type and call dedicated hook functions for each segment. + +- [`variables_flow_resource`](@ref) can be implemented to create additional flow variables for a + specific resource subtype. +- [`constraints_couple_resource`](@ref) can be implemented to add coupling constraints for a + specific resource subtype. + +The default fallback methods are intentionally empty and only provide dispatch points. +This keeps current behavior unchanged unless extension packages add their own methods. + +!!! tip "How to extend" + In extension packages, define methods with concrete resource subtypes, for example + `Vector{<:YourResourceType}` as the resource argument, and keep the fallback methods in + `EnergyModelsGeography` untouched. + +For an overview of standard coupling logic, see +[`Constraint functions`](@ref man-con). +For all internal extension hooks, see +[`Internal functions`](@ref lib-int-fun). + ### [Modes with emissions](@id how_to-update-10-emissions) It is now necessary to provide a new method to the function [`EnergyModelsBase.has_emissions`](@ref) if you plan to include [`TransmissionMode`](@ref)s with emissions instead of a separate function declared within `EnergyModelsGeography`. diff --git a/docs/src/library/internals/functions.md b/docs/src/library/internals/functions.md index 5940bc0..ddcfb3a 100644 --- a/docs/src/library/internals/functions.md +++ b/docs/src/library/internals/functions.md @@ -23,6 +23,7 @@ create_transmission_mode ```@docs constraints_capacity constraints_capacity_installed(m, tm::TransmissionMode, 𝒯::TimeStructure, modeltype::EnergyModel) +constraints_couple_resource constraints_emission constraints_opex_fixed constraints_opex_var @@ -40,6 +41,7 @@ compute_trans_out ## [Variable creation functions](@id lib-int-fun-var) ```@docs +variables_flow_resource variables_trans_mode ``` diff --git a/docs/src/manual/constraint-functions.md b/docs/src/manual/constraint-functions.md index 97353ef..1d1bfc2 100644 --- a/docs/src/manual/constraint-functions.md +++ b/docs/src/manual/constraint-functions.md @@ -5,6 +5,11 @@ The general approach is similar to `EnergyModelsBase`. Bidirectional transport requires at the time being the introduciton of an *if*-loop. In later implementation, it is planned to also use dispatch for this analysis as well. +For resource-specific extensions of area-transmission coupling, see +[`constraints_couple_resource`](@ref). +This hook is called from the default implementation of +`EMB.constraints_couple` for each resource-type segment. + ## [Capacity constraints](@id man-con-cap) ```julia diff --git a/src/model.jl b/src/model.jl index 2e7a04b..2506bac 100644 --- a/src/model.jl +++ b/src/model.jl @@ -84,7 +84,7 @@ function EMB.variables_flow(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, 𝒳ᡛ @variable(m, trans_out[β„³, 𝒯]) # Create new flow variables for specific resource types - for p_sub in EMB.res_types_seg(𝒫) + for p_sub in EMB.res_types_vec(𝒫) EMB.variables_flow_resource(m, β„³, p_sub, 𝒯, modeltype) end end @@ -93,7 +93,7 @@ function EMB.variables_flow(m, π’œ::Vector{<:Area}, π’³α΅›α΅‰αΆœ, 𝒫, 𝒯, @variable(m, area_exchange[a ∈ π’œ, 𝒯, p ∈ exchange_resources(ℒᡗʳᡃⁿ˒, a)]) # Create new flow variables for specific resource types - for p_sub in EMB.res_types_seg(𝒫) + for p_sub in EMB.res_types_vec(𝒫) EMB.variables_flow_resource(m, π’œ, p_sub, 𝒯, modeltype) end end @@ -103,9 +103,13 @@ end variables_flow_resource(m, π’œ::Vector{<:TransmissionMode}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end variables_flow_resource(m, β„’::Vector{<:Area}, 𝒫::Vector{Resource}, 𝒯, modeltype::EnergyModel) -Declaration of flow variables for the differrent resource types. +Declaration of flow variables for the different resource-type segments. -The default method is empty but it is required for multiple dispatch in energy flow models. +The methods are called from [`EMB.variables_flow`](@ref) after segmenting `𝒫` through +`EMB.res_types_vec(𝒫)`. + +The default methods are empty and serve as dispatch hooks for extension packages that add +resource-specific variables. """ function EMB.variables_flow_resource(m, π’œ::Vector{<:TransmissionMode}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end function EMB.variables_flow_resource(m, π’œ::Vector{<:Area}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end @@ -337,7 +341,7 @@ function EMB.constraints_couple(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vecto end # Create new constraints for specific resource types - for p_sub in EMB.res_types_seg(𝒫) + for p_sub in EMB.res_types_vec(𝒫) constraints_couple_resource(m, π’œ, ℒᡗʳᡃⁿ˒, p_sub, 𝒯, modeltype) end end @@ -348,7 +352,12 @@ end """ constraints_couple_resource(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{Transmission}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) -Create constraints for output flowrate and input links. +Create resource-specific coupling constraints. + +The method is called from [`EMB.constraints_couple`](@ref) for each resource-type segment +generated by `EMB.res_types_vec(𝒫)`. + +The default method is empty and serves as a dispatch hook for extension packages. """ function constraints_couple_resource(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{<:Transmission}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end diff --git a/test/Project.toml b/test/Project.toml index 378a016..90f3cd0 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,5 +1,6 @@ [deps] EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" +EnergyModelsGeography = "3f775d88-a4da-46c4-a2cc-aa9f16db6708" EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" diff --git a/test/runtests.jl b/test/runtests.jl index bed8fa0..9df4fca 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -46,6 +46,10 @@ include("utils.jl") include("test_area.jl") end + @testset "Geography | Resource flow" begin + include("test_resource_flow.jl") + end + @testset "Geography | Utilities" begin include("test_utils.jl") end diff --git a/test/test_area.jl b/test/test_area.jl index 2d55747..bd659dc 100644 --- a/test/test_area.jl +++ b/test/test_area.jl @@ -90,7 +90,6 @@ function simple_geo_area(mode_fun::Function) return case, modeltype end - # Testset for the individual extraction methods incorporated in the model @testset "Mode utilities" begin mode_fun(name::String) = PipeSimple( diff --git a/test/test_resource_flow.jl b/test/test_resource_flow.jl new file mode 100644 index 0000000..251fb4a --- /dev/null +++ b/test/test_resource_flow.jl @@ -0,0 +1,236 @@ +struct PotentialPower <: Resource + id::String + co2_int::Float64 + potential_lower::Float64 + potential_upper::Float64 +end + +EMB.is_resource_emit(::PotentialPower) = false +lower_limit(p::PotentialPower) = p.potential_lower +upper_limit(p::PotentialPower) = p.potential_upper + +struct PotentialLossMode{T <: PotentialPower} <: TransmissionMode + id::String + resource::T + trans_cap::TimeProfile + trans_loss::TimeProfile + opex_var::TimeProfile + opex_fixed::TimeProfile + directions::Int + data::Vector{Data} + loss_factor::Float64 +end + +function PotentialLossMode( + id::String, + resource::T, + trans_cap::TimeProfile, + trans_loss::TimeProfile, + opex_var::TimeProfile, + opex_fixed::TimeProfile, + loss_factor::Float64, +) where {T <: PotentialPower} + return PotentialLossMode( + id, + resource, + trans_cap, + trans_loss, + opex_var, + opex_fixed, + 1, + Data[], + loss_factor, + ) +end + +""" + resource_flow_case_with_loss(loss_factor::Float64) + +Create a two-area case with one transmission corridor carrying `PotentialPower`. +The transport itself is lossless, while `PotentialLossMode` reduces the transmitted +potential through resource-specific functions. +""" +function resource_flow_case_with_loss(loss_factor::Float64) + pp = PotentialPower("PotentialPower", 0.0, 0.9, 1.1) + co2 = ResourceEmit("CO2_RF", 1.0) + products = Resource[pp, co2] + + source = RefSource( + "pp_source", + FixedProfile(4), + FixedProfile(10), + FixedProfile(0), + Dict(pp => 1), + ) + sink = RefSink( + "pp_sink", + FixedProfile(3), + Dict(:surplus => FixedProfile(4), :deficit => FixedProfile(100)), + Dict(pp => 1), + ) + + nodes = [GeoAvailability(1, products), GeoAvailability(2, products), source, sink] + links = [ + Direct("src-area", source, nodes[1], Linear()), + Direct("area-snk", nodes[2], sink, Linear()), + ] + + areas = [ + RefArea(1, "AreaA", 10.751, 59.921, nodes[1]), + RefArea(2, "AreaB", 10.398, 63.4366, nodes[2]), + ] + + mode = PotentialLossMode( + "potential_loss", + pp, + FixedProfile(4), + FixedProfile(0), + FixedProfile(0), + FixedProfile(0), + loss_factor, + ) + transmissions = [Transmission(areas[1], areas[2], [mode])] + + T = TwoLevel(2, 2, SimpleTimes(5, 2); op_per_strat = 10) + modeltype = OperationalModel( + Dict(co2 => FixedProfile(100)), + Dict(co2 => FixedProfile(0)), + co2, + ) + + case = Case( + T, + products, + [nodes, links, areas, transmissions], + [[get_nodes, get_links], [get_areas, get_transmissions]], + ) + return case, modeltype +end + +function EMB.variables_flow_resource( + m, + β„³::Vector{<:TransmissionMode}, + 𝒫::Vector{<:PotentialPower}, + 𝒯, + modeltype::EnergyModel, +) + β„³α΅– = filter(tm -> any(p -> p ∈ 𝒫, inputs(tm)) || any(p -> p ∈ 𝒫, outputs(tm)), β„³) + + @variable( + m, + lower_limit(p) <= + energy_potential_trans_in[tm ∈ β„³α΅–, 𝒯, p ∈ intersect(inputs(tm), 𝒫)] <= + upper_limit(p) + ) + @variable( + m, + lower_limit(p) <= + energy_potential_trans_out[tm ∈ β„³α΅–, 𝒯, p ∈ intersect(outputs(tm), 𝒫)] <= + upper_limit(p) + ) +end + +function EMB.variables_flow_resource( + m, + π’œ::Vector{<:Area}, + 𝒫::Vector{<:PotentialPower}, + 𝒯, + modeltype::EnergyModel, +) + @variable(m, lower_limit(p) <= energy_potential_area_in[a ∈ π’œ, 𝒯, p ∈ 𝒫] <= upper_limit(p)) + @variable(m, lower_limit(p) <= energy_potential_area_out[a ∈ π’œ, 𝒯, p ∈ 𝒫] <= upper_limit(p)) +end + +function EMG.constraints_trans_balance( + m, + tm::PotentialLossMode, + 𝒯::TimeStructure, + modeltype::EnergyModel, +) + @constraint(m, [t ∈ 𝒯], + m[:trans_out][tm, t] == m[:trans_in][tm, t] - m[:trans_loss][tm, t] + ) + @constraint(m, [t ∈ 𝒯, p ∈ outputs(tm)], + m[:energy_potential_trans_out][tm, t, p] == + tm.loss_factor * m[:energy_potential_trans_in][tm, t, p] + ) +end + +function EMG.constraints_couple_resource( + m, + π’œ::Vector{<:Area}, + ℒᡗʳᡃⁿ˒::Vector{<:Transmission}, + 𝒫::Vector{<:PotentialPower}, + 𝒯, + modeltype::EnergyModel, +) + for a ∈ π’œ, p ∈ 𝒫 + ℒᢠʳᡒᡐ, β„’α΅—α΅’ = EMG.trans_sub(ℒᡗʳᡃⁿ˒, a) + ℳᢠʳᡒᡐ = EMG.modes_sub(ℒᢠʳᡒᡐ, p) + β„³α΅—α΅’ = EMG.modes_sub(β„’α΅—α΅’, p) + + if !isempty(ℳᢠʳᡒᡐ) + @constraint(m, [t ∈ 𝒯], + m[:energy_potential_area_out][a, t, p] == + sum(m[:energy_potential_trans_in][tm, t, p] for tm ∈ ℳᢠʳᡒᡐ) + ) + end + + if !isempty(β„³α΅—α΅’) + @constraint(m, [t ∈ 𝒯], + m[:energy_potential_area_in][a, t, p] == + sum(m[:energy_potential_trans_out][tm, t, p] for tm ∈ β„³α΅—α΅’) + ) + end + end +end + +@testset "Resource flow | PotentialPower" begin + case, modeltype = resource_flow_case_with_loss(0.9) + pp, co2 = get_products(case) + 𝒯 = get_time_struct(case) + n_t = length(𝒯) + π’œ = get_areas(case) + ℒᡗʳᡃⁿ˒ = get_transmissions(case) + tm = modes(ℒᡗʳᡃⁿ˒)[1] + area_from, area_to = π’œ + + m = optimize(case, modeltype) + general_tests(m) + + @test haskey(m, :energy_potential_trans_in) + @test haskey(m, :energy_potential_trans_out) + @test haskey(m, :energy_potential_area_in) + @test haskey(m, :energy_potential_area_out) + + @test length(m[:energy_potential_trans_in]) == n_t + @test length(m[:energy_potential_trans_out]) == n_t + @test length(m[:energy_potential_area_in]) == 2 * n_t + @test length(m[:energy_potential_area_out]) == 2 * n_t + + @test all(value(m[:energy_potential_trans_in][tm, t, pp]) β‰₯ lower_limit(pp) for t ∈ 𝒯) + @test all(value(m[:energy_potential_trans_in][tm, t, pp]) ≀ upper_limit(pp) for t ∈ 𝒯) + @test all(value(m[:energy_potential_trans_out][tm, t, pp]) β‰₯ lower_limit(pp) for t ∈ 𝒯) + @test all(value(m[:energy_potential_trans_out][tm, t, pp]) ≀ upper_limit(pp) for t ∈ 𝒯) + + @test all( + value(m[:energy_potential_area_out][area_from, t, pp]) β‰ˆ + value(m[:energy_potential_trans_in][tm, t, pp]) + for t ∈ 𝒯) + @test all( + value(m[:energy_potential_trans_out][tm, t, pp]) β‰ˆ + 0.9 * value(m[:energy_potential_trans_in][tm, t, pp]) + for t ∈ 𝒯) + @test all( + value(m[:energy_potential_area_in][area_to, t, pp]) β‰ˆ + value(m[:energy_potential_trans_out][tm, t, pp]) + for t ∈ 𝒯) + + @test all( + value(m[:energy_potential_area_in][area_to, t, pp]) < + value(m[:energy_potential_area_out][area_from, t, pp]) + for t ∈ 𝒯) + @test all(value(m[:trans_in][tm, t]) β‰ˆ value(m[:trans_out][tm, t]) for t ∈ 𝒯) + @test all(value(m[:energy_potential_trans_in][tm, t, pp]) < value(m[:trans_in][tm, t]) for t ∈ 𝒯) + @test all(value(m[:energy_potential_trans_out][tm, t, pp]) < value(m[:trans_out][tm, t]) for t ∈ 𝒯) +end From 08d2532eced476fe03d2dc1136d8a6890b868230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Espen=20Flo=20B=C3=B8dal?= Date: Tue, 14 Apr 2026 13:16:19 +0200 Subject: [PATCH 06/15] feat: document resource-specific flow extensions and update potential-flow example * add an unversioned release note for EMB v0.10 resource dispatch changes * add a how-to guide for extending geography models with resource-specific flow variables * align docs terminology from hooks to extension functions and fix cross-references * update public docs pages with module metadata so Documenter resolves bindings correctly * relax EMB compat and adjust EMI investment constraints to use the full time structure --- NEWS.md | 15 ++ Project.toml | 3 +- docs/make.jl | 3 +- .../how-to/extend-resource-functionality.md | 161 ++++++++++++++++++ docs/src/how-to/update-models.md | 10 +- docs/src/index.md | 3 +- docs/src/library/internals/functions.md | 4 +- docs/src/library/public/area.md | 4 + docs/src/library/public/case_element.md | 4 + docs/src/library/public/mode.md | 4 + docs/src/manual/constraint-functions.md | 4 +- ext/EMIExt/model.jl | 2 +- src/model.jl | 4 +- test/test_resource_flow.jl | 27 +-- 14 files changed, 217 insertions(+), 31 deletions(-) create mode 100644 docs/src/how-to/extend-resource-functionality.md diff --git a/NEWS.md b/NEWS.md index ad43f16..7ec27e3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,20 @@ # Release notes +## Unversioned + +### Adjustments to `EnergyModelsBase` v0.10 + +* Replaced `res_types_seg` with `res_types_vec` at all internal call sites to align with the renamed function in `EnergyModelsBase` v0.10. + +### New resource-specific extension functions + +Both `EMB.variables_flow` (for `TransmissionMode`s and `Area`s) and `EMB.constraints_couple` now iterate over type-segmented resource vectors and call dedicated extension functions per segment: + +* [`variables_flow_resource`](@ref EnergyModelsBase.variables_flow_resource): can be implemented in extension packages for a `Vector` of a specific `Resource` subtype together with either a `Vector{<:TransmissionMode}` or a `Vector{<:Area}` to create additional JuMP variables for that resource. +* [`constraints_couple_resource`](@ref EnergyModelsGeography.constraints_couple_resource): can be implemented in extension packages for a `Vector` of a specific `Resource` subtype to add coupling constraints between areas and transmission modes for that resource. + +Default fallback methods are empty so existing models are unaffected. + ## Version 0.11.5 (2026-01-06) ### Adjustments diff --git a/Project.toml b/Project.toml index b5dddb8..48c482c 100644 --- a/Project.toml +++ b/Project.toml @@ -16,8 +16,7 @@ EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" EMIExt = "EnergyModelsInvestments" [compat] -EnergyModelsBase = "0.10.0" -EnergyModelsInvestments = "0.9" +EnergyModelsBase = "^0.10" SparseVariables = "0.7.3" JuMP = "1.5" TimeStruct = "0.9" diff --git a/docs/make.jl b/docs/make.jl index cf472c3..e46a5c9 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -55,6 +55,7 @@ makedocs( ], "How to" => Any[ "Update models" => "how-to/update-models.md", + "Extend resource functionality" => "how-to/extend-resource-functionality.md", "Contribute to EnergyModelsGeography" => "how-to/contribute.md", ], "Library" => Any[ @@ -67,8 +68,6 @@ makedocs( ], "Internals"=>Any[ "Functions"=>"library/internals/functions.md", - "Methods `EnergyModelsBase`"=>"library/internals/methods_EMB.md", - "Methods `EnergyModelsInvestments`"=>"library/internals/methods_EMIExt.md", ], ], ], diff --git a/docs/src/how-to/extend-resource-functionality.md b/docs/src/how-to/extend-resource-functionality.md new file mode 100644 index 0000000..27fce70 --- /dev/null +++ b/docs/src/how-to/extend-resource-functionality.md @@ -0,0 +1,161 @@ +# [Extend Resource Functionality](@id how_to-extend-resources) + +This guide is the `EnergyModelsGeography` counterpart to the extension functionality in +`EnergyModelsBase` described in `extend-resource-functionality.md`. +It shows how that same pattern is used for geography-specific coupling through a concrete +example from `test_resource_flow.jl`: a `PotentialPower` resource with dedicated flow +variables and coupling constraints. + +## Practical example: `PotentialPower` + +The goal is to track a resource-specific "potential" flow in parallel with standard +transmission flow and enforce a mode-specific loss factor. + +### 1. Define the resource and mode + +```julia +struct PotentialPower <: Resource + id::String + co2_int::Float64 + potential_lower::Float64 + potential_upper::Float64 +end + +EMB.is_resource_emit(::PotentialPower) = false +lower_limit(p::PotentialPower) = p.potential_lower +upper_limit(p::PotentialPower) = p.potential_upper + +struct PotentialLossMode{T <: PotentialPower} <: TransmissionMode + id::String + resource::T + trans_cap::TimeProfile + trans_loss::TimeProfile + opex_var::TimeProfile + opex_fixed::TimeProfile + directions::Int + data::Vector{Data} + loss_factor::Float64 +end +``` + +### 2. Add resource-specific variables + +Implement `EMB.variables_flow_resource` for both mode-level and area-level variables. + +```julia +function EMB.variables_flow_resource( + m, + β„³::Vector{<:TransmissionMode}, + 𝒫::Vector{<:PotentialPower}, + 𝒯, + modeltype::EnergyModel, +) + β„³α΅– = filter(tm -> any(p -> p ∈ 𝒫, inputs(tm)) || any(p -> p ∈ 𝒫, outputs(tm)), β„³) + + @variable( + m, + lower_limit(p) <= + energy_potential_trans_in[tm ∈ β„³α΅–, 𝒯, p ∈ intersect(inputs(tm), 𝒫)] <= + upper_limit(p) + ) + @variable( + m, + lower_limit(p) <= + energy_potential_trans_out[tm ∈ β„³α΅–, 𝒯, p ∈ intersect(outputs(tm), 𝒫)] <= + upper_limit(p) + ) +end + +# It would be cleaner to dispatch on GeoAvailability instead of Area, but Area is used to test the functionality extension implemented in this package +function EMB.variables_flow_resource( + m, + π’œ::Vector{<:Area}, + 𝒫::Vector{<:PotentialPower}, + 𝒯, + modeltype::EnergyModel, +) + 𝒩ᡃᡛ = [availability_node(a) for a ∈ π’œ] + @variable(m, lower_limit(p) <= energy_potential_node_in[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] <= upper_limit(p)) + @variable(m, lower_limit(p) <= energy_potential_node_out[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] <= upper_limit(p)) +end +``` + +### 3. Use the new variables in a custom transmission mode + +Apply the resource-specific variable in the mode balance implementation. + +```julia +function EMG.constraints_trans_balance( + m, + tm::PotentialLossMode, + 𝒯::TimeStructure, + modeltype::EnergyModel, +) + @constraint(m, [t ∈ 𝒯], + m[:trans_out][tm, t] == m[:trans_in][tm, t] - m[:trans_loss][tm, t] + ) + @constraint(m, [t ∈ 𝒯, p ∈ outputs(tm)], + m[:energy_potential_trans_out][tm, t, p] == + tm.loss_factor * m[:energy_potential_trans_in][tm, t, p] + ) +end + +``` + +### 4. Couple variables between area and transmission mode + +Map area-level variables to transmission-level variables with +`EMG.constraints_couple_resource`. + +```julia +function EMG.constraints_couple_resource( + m, + π’œ::Vector{<:Area}, + ℒᡗʳᡃⁿ˒::Vector{<:Transmission}, + 𝒫::Vector{<:PotentialPower}, + 𝒯, + modeltype::EnergyModel, +) + for a ∈ π’œ, p ∈ 𝒫 + ℒᢠʳᡒᡐ, β„’α΅—α΅’ = EMG.trans_sub(ℒᡗʳᡃⁿ˒, a) + ℳᢠʳᡒᡐ = EMG.modes_sub(ℒᢠʳᡒᡐ, p) + β„³α΅—α΅’ = EMG.modes_sub(β„’α΅—α΅’, p) + + if !isempty(ℳᢠʳᡒᡐ) + @constraint(m, [t ∈ 𝒯], + m[:energy_potential_node_out][availability_node(a), t, p] == + sum(m[:energy_potential_trans_in][tm, t, p] for tm ∈ ℳᢠʳᡒᡐ) + ) + end + + if !isempty(β„³α΅—α΅’) + @constraint(m, [t ∈ 𝒯], + m[:energy_potential_node_in][availability_node(a), t, p] == + sum(m[:energy_potential_trans_out][tm, t, p] for tm ∈ β„³α΅—α΅’) + ) + end + end +end +``` + +### 5. What this gives you + +- Bounded resource-specific transmission variables. +- Explicit coupling between area and transmission representation. +- Mode-specific transformations (here: potential loss factor) without changing core code. + +## Other useful applications + +The same extension pattern is useful whenever transport quality matters, not only quantity. + +- District heating networks: track temperature state (supply/return quality) and enforce + temperature-dependent delivery constraints. +- Natural gas networks: track pressure-related transport limits and represent gas mixtures + (e.g., hydrogen blending constraints across corridors). +- Any carrier with quality degradation: track concentration, purity, or state-of-charge style + attributes with resource-specific balance equations. + +## See also + +- [`update-models`](@ref how_to-update) +- [`Constraint functions`](@ref man-con) \ No newline at end of file diff --git a/docs/src/how-to/update-models.md b/docs/src/how-to/update-models.md index b763a65..008d9e0 100644 --- a/docs/src/how-to/update-models.md +++ b/docs/src/how-to/update-models.md @@ -41,11 +41,11 @@ Starting from this update, `EnergyModelsGeography` follows the same extension pa `EnergyModelsBase` for resource-specific dispatch in model construction. The methods `EMB.variables_flow` and `EMB.constraints_couple` now segment the full resource -vector `𝒫` by type and call dedicated hook functions for each segment. +vector `𝒫` by type and call dedicated extension functions for each segment. -- [`variables_flow_resource`](@ref) can be implemented to create additional flow variables for a +- [`variables_flow_resource`](@ref EnergyModelsBase.variables_flow_resource) can be implemented to create additional flow variables for a specific resource subtype. -- [`constraints_couple_resource`](@ref) can be implemented to add coupling constraints for a +- [`constraints_couple_resource`](@ref EnergyModelsGeography.constraints_couple_resource) can be implemented to add coupling constraints for a specific resource subtype. The default fallback methods are intentionally empty and only provide dispatch points. @@ -58,13 +58,13 @@ This keeps current behavior unchanged unless extension packages add their own me For an overview of standard coupling logic, see [`Constraint functions`](@ref man-con). -For all internal extension hooks, see +For all internal extension functions, see [`Internal functions`](@ref lib-int-fun). ### [Modes with emissions](@id how_to-update-10-emissions) It is now necessary to provide a new method to the function [`EnergyModelsBase.has_emissions`](@ref) if you plan to include [`TransmissionMode`](@ref)s with emissions instead of a separate function declared within `EnergyModelsGeography`. -In addition, the function `emission` was renamed to [`EnergyModelsGeography.emissions`](@ref) and, if not called with a `TimePeriod` as input argument, returns a `TimeProfile` instead of a Real. +In addition, the function `emission` was renamed to [`emissions`](@ref EnergyModelsGeography.emissions) and, if not called with a `TimePeriod` as input argument, returns a `TimeProfile` instead of a Real. ## [Adjustments from 0.9.x](@id how_to-update-09) diff --git a/docs/src/index.md b/docs/src/index.md index 7354351..6d235aa 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -38,6 +38,7 @@ Depth = 1 ```@contents Pages = [ "how-to/update-models.md", + "how-to/extend-resource-functionality.md", "how-to/contribute.md", ] Depth = 1 @@ -63,8 +64,6 @@ Depth = 1 ```@contents Pages = [ "library/internals/functions.md", - "library/internals/methods_EMB.md", - "library/internals/methods_EMIExt.md", ] Depth = 1 ``` diff --git a/docs/src/library/internals/functions.md b/docs/src/library/internals/functions.md index ddcfb3a..6d02a1e 100644 --- a/docs/src/library/internals/functions.md +++ b/docs/src/library/internals/functions.md @@ -16,6 +16,8 @@ CurrentModule = EnergyModelsGeography create_area create_model create_transmission_mode +constraints_couple_resource +EMB.variables_flow_resource ``` ## [Constraint functions](@id lib-int-fun-con) @@ -23,7 +25,6 @@ create_transmission_mode ```@docs constraints_capacity constraints_capacity_installed(m, tm::TransmissionMode, 𝒯::TimeStructure, modeltype::EnergyModel) -constraints_couple_resource constraints_emission constraints_opex_fixed constraints_opex_var @@ -41,7 +42,6 @@ compute_trans_out ## [Variable creation functions](@id lib-int-fun-var) ```@docs -variables_flow_resource variables_trans_mode ``` diff --git a/docs/src/library/public/area.md b/docs/src/library/public/area.md index 03f85f1..bcbd16f 100644 --- a/docs/src/library/public/area.md +++ b/docs/src/library/public/area.md @@ -1,5 +1,9 @@ # [`Area`](@id lib-pub-area) +```@meta +CurrentModule = EnergyModelsGeography +``` + A geographical `Area` consist of a location and a connection to a local energy system **via** a specialized `Availability` node called `GeoAvailability`. The specialized `Availability` node is required to modify the energy/mass balance to allow for imports and exports. Constraints related to the area keep track of a resource's export and import to the local system and exchange with other areas. diff --git a/docs/src/library/public/case_element.md b/docs/src/library/public/case_element.md index 1e312b5..cfda598 100644 --- a/docs/src/library/public/case_element.md +++ b/docs/src/library/public/case_element.md @@ -1,5 +1,9 @@ # [Case description](@id lib-pub-case) +```@meta +CurrentModule = EnergyModelsGeography +``` + ## Index ```@index diff --git a/docs/src/library/public/mode.md b/docs/src/library/public/mode.md index 47dd08e..0da3625 100644 --- a/docs/src/library/public/mode.md +++ b/docs/src/library/public/mode.md @@ -1,5 +1,9 @@ # [`TransmissionMode`](@id lib-pub-mode) +```@meta +CurrentModule = EnergyModelsGeography +``` + `TransmissionMode` describes how resources are transported, for example by dynamic transmission modes on ship, truck or railway (represented generically by `RefDynamic`, although not implemented in the current version) or by static transmission modes on overhead power lines or gas pipelines (respresented generically by `RefStatic`). `TransmissionMode`s includes capacity limits (`trans_cap`), losses (`trans_loss`) and directions (`directions`) for the generic transmission modes `RefDynamic` and `RefStatic`. More specialized `TransmissionModes` such as subtypes of the abstract type `PipeMode` can convert one `inlet` resource to another `outlet` resource. diff --git a/docs/src/manual/constraint-functions.md b/docs/src/manual/constraint-functions.md index 1d1bfc2..ebb60a1 100644 --- a/docs/src/manual/constraint-functions.md +++ b/docs/src/manual/constraint-functions.md @@ -6,8 +6,8 @@ Bidirectional transport requires at the time being the introduciton of an *if*-l In later implementation, it is planned to also use dispatch for this analysis as well. For resource-specific extensions of area-transmission coupling, see -[`constraints_couple_resource`](@ref). -This hook is called from the default implementation of +[`constraints_couple_resource`](@ref EnergyModelsGeography.constraints_couple_resource). +This extension function is called from the default implementation of `EMB.constraints_couple` for each resource-type segment. ## [Capacity constraints](@id man-con-cap) diff --git a/ext/EMIExt/model.jl b/ext/EMIExt/model.jl index 756d0fa..9c30691 100644 --- a/ext/EMIExt/model.jl +++ b/ext/EMIExt/model.jl @@ -91,7 +91,7 @@ function EMG.constraints_capacity_installed( 𝒯ᴡⁿᡛ = strategic_periods(𝒯) # Add the investment constraints - EMI.add_investment_constraints(m, tm, inv_data, :cap, :trans_cap, 𝒯ᴡⁿᡛ, disc_rate) + EMI.add_investment_constraints(m, tm, inv_data, :cap, :trans_cap, 𝒯, disc_rate) else for t ∈ 𝒯 fix(m[:trans_cap][tm, t], EMB.capacity(tm, t); force=true) diff --git a/src/model.jl b/src/model.jl index 9d5ac24..24812f6 100644 --- a/src/model.jl +++ b/src/model.jl @@ -108,7 +108,7 @@ Declaration of flow variables for the different resource-type segments. The methods are called from [`EMB.variables_flow`](@ref) after segmenting `𝒫` through `EMB.res_types_vec(𝒫)`. -The default methods are empty and serve as dispatch hooks for extension packages that add +The default methods are empty and intended to be implemented in extension packages that add resource-specific variables. """ function EMB.variables_flow_resource(m, π’œ::Vector{<:TransmissionMode}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end @@ -426,7 +426,7 @@ Create resource-specific coupling constraints. The method is called from [`EMB.constraints_couple`](@ref) for each resource-type segment generated by `EMB.res_types_vec(𝒫)`. -The default method is empty and serves as a dispatch hook for extension packages. +The default method is empty and intended to be implemented in extension packages. """ function constraints_couple_resource(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{<:Transmission}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end diff --git a/test/test_resource_flow.jl b/test/test_resource_flow.jl index 251fb4a..7750448 100644 --- a/test/test_resource_flow.jl +++ b/test/test_resource_flow.jl @@ -132,13 +132,14 @@ end function EMB.variables_flow_resource( m, - π’œ::Vector{<:Area}, + οΏ½::Vector{<:Area}, 𝒫::Vector{<:PotentialPower}, 𝒯, modeltype::EnergyModel, ) - @variable(m, lower_limit(p) <= energy_potential_area_in[a ∈ π’œ, 𝒯, p ∈ 𝒫] <= upper_limit(p)) - @variable(m, lower_limit(p) <= energy_potential_area_out[a ∈ π’œ, 𝒯, p ∈ 𝒫] <= upper_limit(p)) + 𝒩ᡃᡛ = [availability_node(a) for a ∈ π’œ] + @variable(m, lower_limit(p) <= energy_potential_node_in[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] <= upper_limit(p)) + @variable(m, lower_limit(p) <= energy_potential_node_out[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] <= upper_limit(p)) end function EMG.constraints_trans_balance( @@ -171,14 +172,14 @@ function EMG.constraints_couple_resource( if !isempty(ℳᢠʳᡒᡐ) @constraint(m, [t ∈ 𝒯], - m[:energy_potential_area_out][a, t, p] == + m[:energy_potential_node_out][availability_node(a), t, p] == sum(m[:energy_potential_trans_in][tm, t, p] for tm ∈ ℳᢠʳᡒᡐ) ) end if !isempty(β„³α΅—α΅’) @constraint(m, [t ∈ 𝒯], - m[:energy_potential_area_in][a, t, p] == + m[:energy_potential_node_in][availability_node(a), t, p] == sum(m[:energy_potential_trans_out][tm, t, p] for tm ∈ β„³α΅—α΅’) ) end @@ -200,13 +201,13 @@ end @test haskey(m, :energy_potential_trans_in) @test haskey(m, :energy_potential_trans_out) - @test haskey(m, :energy_potential_area_in) - @test haskey(m, :energy_potential_area_out) + @test haskey(m, :energy_potential_node_in) + @test haskey(m, :energy_potential_node_out) @test length(m[:energy_potential_trans_in]) == n_t @test length(m[:energy_potential_trans_out]) == n_t - @test length(m[:energy_potential_area_in]) == 2 * n_t - @test length(m[:energy_potential_area_out]) == 2 * n_t + @test length(m[:energy_potential_node_in]) == 2 * n_t + @test length(m[:energy_potential_node_out]) == 2 * n_t @test all(value(m[:energy_potential_trans_in][tm, t, pp]) β‰₯ lower_limit(pp) for t ∈ 𝒯) @test all(value(m[:energy_potential_trans_in][tm, t, pp]) ≀ upper_limit(pp) for t ∈ 𝒯) @@ -214,7 +215,7 @@ end @test all(value(m[:energy_potential_trans_out][tm, t, pp]) ≀ upper_limit(pp) for t ∈ 𝒯) @test all( - value(m[:energy_potential_area_out][area_from, t, pp]) β‰ˆ + value(m[:energy_potential_node_out][availability_node(area_from), t, pp]) β‰ˆ value(m[:energy_potential_trans_in][tm, t, pp]) for t ∈ 𝒯) @test all( @@ -222,13 +223,13 @@ end 0.9 * value(m[:energy_potential_trans_in][tm, t, pp]) for t ∈ 𝒯) @test all( - value(m[:energy_potential_area_in][area_to, t, pp]) β‰ˆ + value(m[:energy_potential_node_in][availability_node(area_to), t, pp]) β‰ˆ value(m[:energy_potential_trans_out][tm, t, pp]) for t ∈ 𝒯) @test all( - value(m[:energy_potential_area_in][area_to, t, pp]) < - value(m[:energy_potential_area_out][area_from, t, pp]) + value(m[:energy_potential_node_in][availability_node(area_to), t, pp]) < + value(m[:energy_potential_node_out][availability_node(area_from), t, pp]) for t ∈ 𝒯) @test all(value(m[:trans_in][tm, t]) β‰ˆ value(m[:trans_out][tm, t]) for t ∈ 𝒯) @test all(value(m[:energy_potential_trans_in][tm, t, pp]) < value(m[:trans_in][tm, t]) for t ∈ 𝒯) From 098813a26b6c4f03cfa701f7a401a36beb87a0c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Espen=20Flo=20B=C3=B8dal?= Date: Tue, 14 Apr 2026 14:13:53 +0200 Subject: [PATCH 07/15] Bugfix: Update test files for complex resource flow --- test/Project.toml | 1 - test/test_resource_flow.jl | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/Project.toml b/test/Project.toml index 90f3cd0..cabfad3 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,5 +1,4 @@ [deps] -EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" EnergyModelsGeography = "3f775d88-a4da-46c4-a2cc-aa9f16db6708" EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" diff --git a/test/test_resource_flow.jl b/test/test_resource_flow.jl index 7750448..38fb859 100644 --- a/test/test_resource_flow.jl +++ b/test/test_resource_flow.jl @@ -132,7 +132,7 @@ end function EMB.variables_flow_resource( m, - οΏ½::Vector{<:Area}, + π’œ::Vector{<:Area}, 𝒫::Vector{<:PotentialPower}, 𝒯, modeltype::EnergyModel, From 2892bc145f18150c58743345b3add520188e5518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Espen=20Flo=20B=C3=B8dal?= Date: Tue, 14 Apr 2026 14:17:33 +0200 Subject: [PATCH 08/15] Removed wrong package from test Project.toml --- test/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Project.toml b/test/Project.toml index cabfad3..378a016 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,5 +1,5 @@ [deps] -EnergyModelsGeography = "3f775d88-a4da-46c4-a2cc-aa9f16db6708" +EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" From a6218c245bf37fc28cc2847a4fb57fabe3e8db96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Espen=20Flo=20B=C3=B8dal?= Date: Tue, 14 Apr 2026 14:36:24 +0200 Subject: [PATCH 09/15] Updates versioning --- NEWS.md | 6 +++--- Project.toml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/NEWS.md b/NEWS.md index 7ec27e3..ff3183b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,10 +1,10 @@ # Release notes -## Unversioned +## Version 0.11.6 (2026-04-14) -### Adjustments to `EnergyModelsBase` v0.10 +### Adjustments to `EnergyModelsBase` v0.9.5 -* Replaced `res_types_seg` with `res_types_vec` at all internal call sites to align with the renamed function in `EnergyModelsBase` v0.10. +* Replaced `res_types_seg` with `res_types_vec` at all internal call sites to align with the renamed function in `EnergyModelsBase` v0.9.5. ### New resource-specific extension functions diff --git a/Project.toml b/Project.toml index 48c482c..5abe2af 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "EnergyModelsGeography" uuid = "3f775d88-a4da-46c4-a2cc-aa9f16db6708" authors = ["Espen Flo BΓΈdal "] -version = "0.11.5" +version = "0.11.6" [deps] EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" @@ -16,7 +16,7 @@ EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" EMIExt = "EnergyModelsInvestments" [compat] -EnergyModelsBase = "^0.10" +EnergyModelsBase = "0.9.5" SparseVariables = "0.7.3" JuMP = "1.5" TimeStruct = "0.9" From 023fc7f49d6ab39f7e4023b365300e04fff71bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Espen=20Flo=20B=C3=B8dal?= Date: Tue, 14 Apr 2026 14:50:25 +0200 Subject: [PATCH 10/15] Revert update in EMIExt --- ext/EMIExt/model.jl | 2 +- test/Project.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/EMIExt/model.jl b/ext/EMIExt/model.jl index 9c30691..756d0fa 100644 --- a/ext/EMIExt/model.jl +++ b/ext/EMIExt/model.jl @@ -91,7 +91,7 @@ function EMG.constraints_capacity_installed( 𝒯ᴡⁿᡛ = strategic_periods(𝒯) # Add the investment constraints - EMI.add_investment_constraints(m, tm, inv_data, :cap, :trans_cap, 𝒯, disc_rate) + EMI.add_investment_constraints(m, tm, inv_data, :cap, :trans_cap, 𝒯ᴡⁿᡛ, disc_rate) else for t ∈ 𝒯 fix(m[:trans_cap][tm, t], EMB.capacity(tm, t); force=true) diff --git a/test/Project.toml b/test/Project.toml index 378a016..90f3cd0 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,5 +1,6 @@ [deps] EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" +EnergyModelsGeography = "3f775d88-a4da-46c4-a2cc-aa9f16db6708" EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" From 48d7e335784263c27bae9b2d0dd95ab297c1899a Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Tue, 14 Apr 2026 15:27:48 +0200 Subject: [PATCH 11/15] Adjusted documentation and NEWS.md --- NEWS.md | 19 +++++--------- docs/make.jl | 2 ++ .../how-to/extend-resource-functionality.md | 21 ++++++++------- docs/src/how-to/update-models.md | 26 ------------------- docs/src/index.md | 2 ++ docs/src/library/internals/functions.md | 1 - docs/src/library/internals/methods_EMB.md | 1 + docs/src/manual/constraint-functions.md | 6 ++--- 8 files changed, 25 insertions(+), 53 deletions(-) diff --git a/NEWS.md b/NEWS.md index ff3183b..99d167f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,19 +1,14 @@ # Release notes -## Version 0.11.6 (2026-04-14) +## Version 0.11.6 (2026-04-15) -### Adjustments to `EnergyModelsBase` v0.9.5 +### Add support for resource-specific constraint functions -* Replaced `res_types_seg` with `res_types_vec` at all internal call sites to align with the renamed function in `EnergyModelsBase` v0.9.5. - -### New resource-specific extension functions - -Both `EMB.variables_flow` (for `TransmissionMode`s and `Area`s) and `EMB.constraints_couple` now iterate over type-segmented resource vectors and call dedicated extension functions per segment: - -* [`variables_flow_resource`](@ref EnergyModelsBase.variables_flow_resource): can be implemented in extension packages for a `Vector` of a specific `Resource` subtype together with either a `Vector{<:TransmissionMode}` or a `Vector{<:Area}` to create additional JuMP variables for that resource. -* [`constraints_couple_resource`](@ref EnergyModelsGeography.constraints_couple_resource): can be implemented in extension packages for a `Vector` of a specific `Resource` subtype to add coupling constraints between areas and transmission modes for that resource. - -Default fallback methods are empty so existing models are unaffected. +* Introduced support for resource specific constraint functions as introduced in [`EnergyModelsBase` v0.9.5](https://github.com/EnergyModelsX/EnergyModelsBase.jl/releases/tag/v0.9.5) +* Both `EMB.variables_flow` (for `TransmissionMode`s and `Area`s) and `EMB.constraints_couple` now iterate over type-segmented resource vectors and call dedicated extension functions per segment: + * [`variables_flow_resource`](https://energymodelsx.github.io/EnergyModelsBase.jl/stable/library/internals/functions/#EnergyModelsBase.variables_flow_resource): can be implemented in extension packages for a `Vector` of a specific `Resource` subtype together with either a `Vector{<:TransmissionMode}` or a `Vector{<:Area}` to create additional JuMP variables for that resource. + * [`constraints_couple_resource`](https://energymodelsx.github.io/EnergyModelsBase.jl/stable/library/internals/functions/#EnergyModelsBase.constraints_couple_resource): can be implemented in extension packages for a `Vector` of a specific `Resource` subtype to add coupling constraints between areas and transmission modes for that resource. +* Default fallback methods are added to allow for resources without additional variables. ## Version 0.11.5 (2026-01-06) diff --git a/docs/make.jl b/docs/make.jl index e46a5c9..17c0ad3 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -68,6 +68,8 @@ makedocs( ], "Internals"=>Any[ "Functions"=>"library/internals/functions.md", + "Methods `EnergyModelsBase`"=>"library/internals/methods_EMB.md", + "Methods `EnergyModelsInvestments`"=>"library/internals/methods_EMIExt.md", ], ], ], diff --git a/docs/src/how-to/extend-resource-functionality.md b/docs/src/how-to/extend-resource-functionality.md index 27fce70..0e88b01 100644 --- a/docs/src/how-to/extend-resource-functionality.md +++ b/docs/src/how-to/extend-resource-functionality.md @@ -1,18 +1,19 @@ -# [Extend Resource Functionality](@id how_to-extend-resources) +# [Extend Resource functionality](@id how_to-res_funct) -This guide is the `EnergyModelsGeography` counterpart to the extension functionality in -`EnergyModelsBase` described in `extend-resource-functionality.md`. -It shows how that same pattern is used for geography-specific coupling through a concrete -example from `test_resource_flow.jl`: a `PotentialPower` resource with dedicated flow +This guide is the `EnergyModelsGeography` counterpart to the resource functionality *[introduced in `EnergyModelsBase`](@ref EnergyModelsBase how_to-res_funct)*. +It shows how that same pattern is used for geography-specific coupling through a concrete example from `test_resource_flow.jl`: a `PotentialPower` resource with dedicated flow variables and coupling constraints. -## Practical example: `PotentialPower` +## [Practical example: `PotentialPower`](@id how_to-res_funct-example) -The goal is to track a resource-specific "potential" flow in parallel with standard -transmission flow and enforce a mode-specific loss factor. +The goal is to track a resource-specific "potential" flow in parallel with standard transmission flow and enforce a mode-specific loss factor. ### 1. Define the resource and mode +!!! tip + You can use the same resource type as declared in `EnergyModelsBase` or any other package. + This corresponds to *[step 1 in the example of `EnergyModelsBase`](@ref EnergyModelsBase how_to-res_funct-example)*. + ```julia struct PotentialPower <: Resource id::String @@ -40,7 +41,7 @@ end ### 2. Add resource-specific variables -Implement `EMB.variables_flow_resource` for both mode-level and area-level variables. +Implement `EMB.variables_flow_resource` for both mode-level and area-level variables to introduce new variables. ```julia function EMB.variables_flow_resource( @@ -158,4 +159,4 @@ The same extension pattern is useful whenever transport quality matters, not onl ## See also - [`update-models`](@ref how_to-update) -- [`Constraint functions`](@ref man-con) \ No newline at end of file +- [`Constraint functions`](@ref man-con) diff --git a/docs/src/how-to/update-models.md b/docs/src/how-to/update-models.md index 008d9e0..714b4d8 100644 --- a/docs/src/how-to/update-models.md +++ b/docs/src/how-to/update-models.md @@ -35,32 +35,6 @@ case = Case( Furthermore, we reworked the design for inclusion of emission and OPEX variables. -### [Resource-specific variables and coupling constraints](@id how_to-update-10-resource_dispatch) - -Starting from this update, `EnergyModelsGeography` follows the same extension pattern as -`EnergyModelsBase` for resource-specific dispatch in model construction. - -The methods `EMB.variables_flow` and `EMB.constraints_couple` now segment the full resource -vector `𝒫` by type and call dedicated extension functions for each segment. - -- [`variables_flow_resource`](@ref EnergyModelsBase.variables_flow_resource) can be implemented to create additional flow variables for a - specific resource subtype. -- [`constraints_couple_resource`](@ref EnergyModelsGeography.constraints_couple_resource) can be implemented to add coupling constraints for a - specific resource subtype. - -The default fallback methods are intentionally empty and only provide dispatch points. -This keeps current behavior unchanged unless extension packages add their own methods. - -!!! tip "How to extend" - In extension packages, define methods with concrete resource subtypes, for example - `Vector{<:YourResourceType}` as the resource argument, and keep the fallback methods in - `EnergyModelsGeography` untouched. - -For an overview of standard coupling logic, see -[`Constraint functions`](@ref man-con). -For all internal extension functions, see -[`Internal functions`](@ref lib-int-fun). - ### [Modes with emissions](@id how_to-update-10-emissions) It is now necessary to provide a new method to the function [`EnergyModelsBase.has_emissions`](@ref) if you plan to include [`TransmissionMode`](@ref)s with emissions instead of a separate function declared within `EnergyModelsGeography`. diff --git a/docs/src/index.md b/docs/src/index.md index 6d235aa..efef321 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -64,6 +64,8 @@ Depth = 1 ```@contents Pages = [ "library/internals/functions.md", + "library/internals/methods_EMB.md", + "library/internals/methods_EMIExt.md", ] Depth = 1 ``` diff --git a/docs/src/library/internals/functions.md b/docs/src/library/internals/functions.md index 6d02a1e..2ace78c 100644 --- a/docs/src/library/internals/functions.md +++ b/docs/src/library/internals/functions.md @@ -17,7 +17,6 @@ create_area create_model create_transmission_mode constraints_couple_resource -EMB.variables_flow_resource ``` ## [Constraint functions](@id lib-int-fun-con) diff --git a/docs/src/library/internals/methods_EMB.md b/docs/src/library/internals/methods_EMB.md index 27db5ed..3020668 100644 --- a/docs/src/library/internals/methods_EMB.md +++ b/docs/src/library/internals/methods_EMB.md @@ -21,6 +21,7 @@ EMB.constraints_couple ```@docs EMB.variables_capacity EMB.variables_flow +EMB.variables_flow_resource EMB.variables_opex EMB.variables_capex(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, 𝒳, 𝒯, modeltype::EnergyModel) EMB.variables_elements diff --git a/docs/src/manual/constraint-functions.md b/docs/src/manual/constraint-functions.md index ebb60a1..f096908 100644 --- a/docs/src/manual/constraint-functions.md +++ b/docs/src/manual/constraint-functions.md @@ -5,10 +5,8 @@ The general approach is similar to `EnergyModelsBase`. Bidirectional transport requires at the time being the introduciton of an *if*-loop. In later implementation, it is planned to also use dispatch for this analysis as well. -For resource-specific extensions of area-transmission coupling, see -[`constraints_couple_resource`](@ref EnergyModelsGeography.constraints_couple_resource). -This extension function is called from the default implementation of -`EMB.constraints_couple` for each resource-type segment. +For resource-specific extensions of area-transmission coupling, see the Section on *[Extend Resource functionality](@ref how_to-res_funct)*. +This extension is called from the default implementation of `EMB.constraints_couple` for each resource-type segment. ## [Capacity constraints](@id man-con-cap) From 4d4623f79be47cea771786025f50d588d8ea5af9 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Tue, 14 Apr 2026 16:06:45 +0200 Subject: [PATCH 12/15] Minor fixes to the code base --- docs/src/library/internals/functions.md | 1 - docs/src/library/internals/methods_EMB.md | 1 + src/constraint_functions.jl | 2 -- src/model.jl | 16 ++++++++-------- test/test_resource_flow.jl | 14 +++++++------- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/docs/src/library/internals/functions.md b/docs/src/library/internals/functions.md index 2ace78c..5940bc0 100644 --- a/docs/src/library/internals/functions.md +++ b/docs/src/library/internals/functions.md @@ -16,7 +16,6 @@ CurrentModule = EnergyModelsGeography create_area create_model create_transmission_mode -constraints_couple_resource ``` ## [Constraint functions](@id lib-int-fun-con) diff --git a/docs/src/library/internals/methods_EMB.md b/docs/src/library/internals/methods_EMB.md index 3020668..77faab2 100644 --- a/docs/src/library/internals/methods_EMB.md +++ b/docs/src/library/internals/methods_EMB.md @@ -14,6 +14,7 @@ EMB.objective_operational EMB.emissions_operational EMB.constraints_elements EMB.constraints_couple +EMB.constraints_couple_resource ``` ## [Variable methods](@id lib-int-met_emb-var) diff --git a/src/constraint_functions.jl b/src/constraint_functions.jl index c16d053..9ee64da 100644 --- a/src/constraint_functions.jl +++ b/src/constraint_functions.jl @@ -149,8 +149,6 @@ This function serves as fallback option if no other function is specified for a """ function constraints_trans_balance(m, tm::TransmissionMode, 𝒯::TimeStructure, modeltype::EnergyModel) - @info "Creating transmission balance constraints for TransmissionMode: $(tm.id) of type $(typeof(tm))" - @constraint(m, [t ∈ 𝒯], m[:trans_out][tm, t] == m[:trans_in][tm, t] - m[:trans_loss][tm, t]) diff --git a/src/model.jl b/src/model.jl index 24812f6..2480d33 100644 --- a/src/model.jl +++ b/src/model.jl @@ -84,7 +84,7 @@ function EMB.variables_flow(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, 𝒳ᡛ @variable(m, trans_out[β„³, 𝒯]) # Create new flow variables for specific resource types - for p_sub in EMB.res_types_vec(𝒫) + for p_sub ∈ EMB.res_types_vec(𝒫) EMB.variables_flow_resource(m, β„³, p_sub, 𝒯, modeltype) end end @@ -93,15 +93,15 @@ function EMB.variables_flow(m, π’œ::Vector{<:Area}, π’³α΅›α΅‰αΆœ, 𝒫, 𝒯, @variable(m, area_exchange[a ∈ π’œ, 𝒯, p ∈ exchange_resources(ℒᡗʳᡃⁿ˒, a)]) # Create new flow variables for specific resource types - for p_sub in EMB.res_types_vec(𝒫) + for p_sub ∈ EMB.res_types_vec(𝒫) EMB.variables_flow_resource(m, π’œ, p_sub, 𝒯, modeltype) end end """ - variables_flow_resource(m, π’œ::Vector{<:TransmissionMode}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end - variables_flow_resource(m, β„’::Vector{<:Area}, 𝒫::Vector{Resource}, 𝒯, modeltype::EnergyModel) + EMB.variables_flow_resource(m, π’œ::Vector{<:TransmissionMode}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) + EMB.variables_flow_resource(m, β„’::Vector{<:Area}, 𝒫::Vector{Resource}, 𝒯, modeltype::EnergyModel) Declaration of flow variables for the different resource-type segments. @@ -410,8 +410,8 @@ function EMB.constraints_couple(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vecto end # Create new constraints for specific resource types - for p_sub in EMB.res_types_vec(𝒫) - constraints_couple_resource(m, π’œ, ℒᡗʳᡃⁿ˒, p_sub, 𝒯, modeltype) + for p_sub ∈ EMB.res_types_vec(𝒫) + EMB.constraints_couple_resource(m, π’œ, ℒᡗʳᡃⁿ˒, p_sub, 𝒯, modeltype) end end function EMB.constraints_couple(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, π’œ::Vector{<:Area}, 𝒫, 𝒯, modeltype::EnergyModel) @@ -419,7 +419,7 @@ function EMB.constraints_couple(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, π’œ: end """ - constraints_couple_resource(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{Transmission}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) + EMB.constraints_couple_resource(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{Transmission}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) Create resource-specific coupling constraints. @@ -428,7 +428,7 @@ generated by `EMB.res_types_vec(𝒫)`. The default method is empty and intended to be implemented in extension packages. """ -function constraints_couple_resource(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{<:Transmission}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end +function EMB.constraints_couple_resource(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{<:Transmission}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end """ EMB.emissions_operational(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, 𝒫ᡉᡐ, 𝒯, modeltype::EnergyModel) diff --git a/test/test_resource_flow.jl b/test/test_resource_flow.jl index 38fb859..e47d51b 100644 --- a/test/test_resource_flow.jl +++ b/test/test_resource_flow.jl @@ -118,14 +118,14 @@ function EMB.variables_flow_resource( @variable( m, - lower_limit(p) <= - energy_potential_trans_in[tm ∈ β„³α΅–, 𝒯, p ∈ intersect(inputs(tm), 𝒫)] <= + lower_limit(p) ≀ + energy_potential_trans_in[tm ∈ β„³α΅–, 𝒯, p ∈ intersect(inputs(tm), 𝒫)] ≀ upper_limit(p) ) @variable( m, - lower_limit(p) <= - energy_potential_trans_out[tm ∈ β„³α΅–, 𝒯, p ∈ intersect(outputs(tm), 𝒫)] <= + lower_limit(p) ≀ + energy_potential_trans_out[tm ∈ β„³α΅–, 𝒯, p ∈ intersect(outputs(tm), 𝒫)] ≀ upper_limit(p) ) end @@ -138,8 +138,8 @@ function EMB.variables_flow_resource( modeltype::EnergyModel, ) 𝒩ᡃᡛ = [availability_node(a) for a ∈ π’œ] - @variable(m, lower_limit(p) <= energy_potential_node_in[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] <= upper_limit(p)) - @variable(m, lower_limit(p) <= energy_potential_node_out[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] <= upper_limit(p)) + @variable(m, lower_limit(p) ≀ energy_potential_node_in[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] ≀ upper_limit(p)) + @variable(m, lower_limit(p) ≀ energy_potential_node_out[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] ≀ upper_limit(p)) end function EMG.constraints_trans_balance( @@ -157,7 +157,7 @@ function EMG.constraints_trans_balance( ) end -function EMG.constraints_couple_resource( +function EMB.constraints_couple_resource( m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{<:Transmission}, From 593257e29a7787346c104651718f9ba028e8a303 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Tue, 14 Apr 2026 16:54:39 +0200 Subject: [PATCH 13/15] Minor rework of the functionality --- .../how-to/extend-resource-functionality.md | 28 ++++--- docs/src/library/internals/methods_EMB.md | 1 + src/model.jl | 48 ++++++++++- test/test_resource_flow.jl | 83 ++++++++++++++----- 4 files changed, 121 insertions(+), 39 deletions(-) diff --git a/docs/src/how-to/extend-resource-functionality.md b/docs/src/how-to/extend-resource-functionality.md index 0e88b01..20ce0f9 100644 --- a/docs/src/how-to/extend-resource-functionality.md +++ b/docs/src/how-to/extend-resource-functionality.md @@ -4,6 +4,12 @@ This guide is the `EnergyModelsGeography` counterpart to the resource functional It shows how that same pattern is used for geography-specific coupling through a concrete example from `test_resource_flow.jl`: a `PotentialPower` resource with dedicated flow variables and coupling constraints. +!!! warning + While we allow resource variable introduction for [`Area`](@ref)s, we strongly advise against introducing new variables for an `Area`. + It is instead easier to access in the function [`EMB.constraints_couple_resource`](@ref) the relevant `Availability` node as outlined below. + + This approach allows you to couple the local energy system with the transmission modes with respect to the extra variables. + ## [Practical example: `PotentialPower`](@id how_to-res_funct-example) The goal is to track a resource-specific "potential" flow in parallel with standard transmission flow and enforce a mode-specific loss factor. @@ -41,7 +47,7 @@ end ### 2. Add resource-specific variables -Implement `EMB.variables_flow_resource` for both mode-level and area-level variables to introduce new variables. +Implement `EMB.variables_flow_resource` for both [`Area`] and [`Node`] to introduce new variables. ```julia function EMB.variables_flow_resource( @@ -67,40 +73,36 @@ function EMB.variables_flow_resource( ) end -# It would be cleaner to dispatch on GeoAvailability instead of Area, but Area is used to test the functionality extension implemented in this package function EMB.variables_flow_resource( m, - π’œ::Vector{<:Area}, + 𝒩::Vector{<:Node}, 𝒫::Vector{<:PotentialPower}, 𝒯, modeltype::EnergyModel, ) - 𝒩ᡃᡛ = [availability_node(a) for a ∈ π’œ] - @variable(m, lower_limit(p) <= energy_potential_node_in[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] <= upper_limit(p)) - @variable(m, lower_limit(p) <= energy_potential_node_out[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] <= upper_limit(p)) + @variable(m, lower_limit(p) <= energy_potential_node_in[n ∈ 𝒩, 𝒯, p ∈ 𝒫] <= upper_limit(p)) + @variable(m, lower_limit(p) <= energy_potential_node_out[n ∈ 𝒩, 𝒯, p ∈ 𝒫] <= upper_limit(p)) end ``` -### 3. Use the new variables in a custom transmission mode +### 3. Use the new variables in the function -Apply the resource-specific variable in the mode balance implementation. +Apply the resource-specific variable in the function [`EMB.constraints_resource`](@ref). +You must be careful when defining the internal constraints due to potential changes in the variables. ```julia -function EMG.constraints_trans_balance( +function EMB.constraints_resource( m, tm::PotentialLossMode, 𝒯::TimeStructure, + 𝒫::Vector{<:PotentialPower}, modeltype::EnergyModel, ) - @constraint(m, [t ∈ 𝒯], - m[:trans_out][tm, t] == m[:trans_in][tm, t] - m[:trans_loss][tm, t] - ) @constraint(m, [t ∈ 𝒯, p ∈ outputs(tm)], m[:energy_potential_trans_out][tm, t, p] == tm.loss_factor * m[:energy_potential_trans_in][tm, t, p] ) end - ``` ### 4. Couple variables between area and transmission mode diff --git a/docs/src/library/internals/methods_EMB.md b/docs/src/library/internals/methods_EMB.md index 77faab2..1c3c303 100644 --- a/docs/src/library/internals/methods_EMB.md +++ b/docs/src/library/internals/methods_EMB.md @@ -13,6 +13,7 @@ EMB.create_node EMB.objective_operational EMB.emissions_operational EMB.constraints_elements +EMB.constraints_resource EMB.constraints_couple EMB.constraints_couple_resource ``` diff --git a/src/model.jl b/src/model.jl index 2480d33..a47a2ee 100644 --- a/src/model.jl +++ b/src/model.jl @@ -57,8 +57,8 @@ function EMB.variables_capacity(m, π’œ::Vector{<:Area}, π’³α΅›α΅‰αΆœ, 𝒯, mo EMB.variables_flow(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, π’³α΅›α΅‰αΆœ, 𝒯, modeltype::EnergyModel) EMB.variables_flow(m, π’œ::Vector{<:Area}, π’³α΅›α΅‰αΆœ, 𝒯, modeltype::EnergyModel) -Declaration of flow OPEX variables for the element types introduced in -`EnergyModelsGeography`. `EnergyModelsGeography` introduces two elements for an energy system, and +Declaration of flow variables for the element types introduced in `EnergyModelsGeography`. +`EnergyModelsGeography` introduces two elements for an energy system, and hence, provides the user with two individual methods: !!! tip "Transmission variables" @@ -69,11 +69,15 @@ hence, provides the user with two individual methods: resources of transmission mode `m` are extracted using the function [`inputs`](@ref). - `trans_out[tm, t]` is the flow _**from**_ mode `tm` in operational period `t`. The outflow resources of transmission mode `m` are extracted using the function [`outputs`](@ref). + - call of the function [`EMB.variables_flow_resource`](@ref) for introducing resource + specific flow variables. !!! note "Area variables" - `area_exchange[a, t, p]` is the exchange of resource `p` by area `a` in operational period `t`. The exchange resources are extracted using the function [`exchange_resources`](@ref) + - call of the function [`EMB.variables_flow_resource`](@ref) for introducing resource + specific flow variables. """ function EMB.variables_flow(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, π’³α΅›α΅‰αΆœ, 𝒫, 𝒯, modeltype::EnergyModel) # Extract the individual transmission modes @@ -98,7 +102,6 @@ function EMB.variables_flow(m, π’œ::Vector{<:Area}, π’³α΅›α΅‰αΆœ, 𝒫, 𝒯, end end - """ EMB.variables_flow_resource(m, π’œ::Vector{<:TransmissionMode}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) EMB.variables_flow_resource(m, β„’::Vector{<:Area}, 𝒫::Vector{Resource}, 𝒯, modeltype::EnergyModel) @@ -110,6 +113,11 @@ The methods are called from [`EMB.variables_flow`](@ref) after segmenting `𝒫` The default methods are empty and intended to be implemented in extension packages that add resource-specific variables. + +!!! warning "Resource flow variables for Areas" + We strongly advise against creating new variables for `Area`s. Instead, it is prefered + to create the variables for the respective nodes to couple the local energy system with + th transmission corridors. """ function EMB.variables_flow_resource(m, π’œ::Vector{<:TransmissionMode}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end function EMB.variables_flow_resource(m, π’œ::Vector{<:Area}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end @@ -358,14 +366,48 @@ function EMB.constraints_elements(m, π’œ::Vector{<:Area}, π’³α΅›α΅‰αΆœ, 𝒫, ℒᡗʳᡃⁿ˒ = get_transmissions(π’³α΅›α΅‰αΆœ) for a ∈ π’œ create_area(m, a, 𝒯, ℒᡗʳᡃⁿ˒, modeltype) + + # Constraints based on the resource types + n = availability_node(a) + area_resources = Vector{Resource}(unique(vcat(inputs(n), outputs(n)))) + for π’«Λ’α΅˜α΅‡ ∈ EMB.res_types_vec(area_resources) + EMB.constraints_resource(m, a, 𝒯, π’«Λ’α΅˜α΅‡, modeltype) + end end end function EMB.constraints_elements(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, π’³α΅›α΅‰αΆœ, 𝒫, 𝒯, modeltype::EnergyModel) for tm ∈ modes(ℒᡗʳᡃⁿ˒) create_transmission_mode(m, tm, 𝒯, modeltype) + + # Constraints based on the resource types + mode_resources = Vector{Resource}(unique(vcat(inputs(tm), outputs(tm)))) + for π’«Λ’α΅˜α΅‡ ∈ EMB.res_types_vec(mode_resources) + EMB.constraints_resource(m, tm, 𝒯, π’«Λ’α΅˜α΅‡, modeltype) + end end end +""" + EMB.constraints_resource(m, a::Area, 𝒯, 𝒫::Vector{<:Resource}, modeltype::EnergyModel) + EMB.constraints_resource(m, tm::TransmissionMode, 𝒯, 𝒫::Vector{<:Resource}, modeltype::EnergyModel) + +Create constraints for the flow of resources through an [`AbstractElement`](@ref) for +specific resource types. In `EnergyModelsBase`, this method is provided for +[`Node`](@ref EnergyModelsBase.Node) and [`Link`](@ref). + +The function is empty by default and can be implemented in extension packages. + +!!! warning + While we allow the method to be also used for [`Area`](@ref)s, we strongly advise against + introducing new variables for an `Area` as it would require more steps to introduce new + variables. It is instead easier to access in the function [`EMB.constraints_couple`](@ref) + the relevant `Availability` node. + + This approach allows you to couple the local energy system with the transmission modes. +""" +function EMB.constraints_resource(m, n::Area, 𝒯, 𝒫::Vector{<:Resource}, modeltype::EnergyModel) end +function EMB.constraints_resource(m, tm::TransmissionMode, 𝒯, 𝒫::Vector{<:Resource}, modeltype::EnergyModel) end + """ EMB.constraints_couple(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{Transmission}, 𝒫, 𝒯, modeltype::EnergyModel) EMB.constraints_couple(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, π’œ::Vector{<:Area}, 𝒫, 𝒯, modeltype::EnergyModel) diff --git a/test/test_resource_flow.jl b/test/test_resource_flow.jl index e47d51b..583e5e4 100644 --- a/test/test_resource_flow.jl +++ b/test/test_resource_flow.jl @@ -107,6 +107,18 @@ function resource_flow_case_with_loss(loss_factor::Float64) return case, modeltype end +# Declare new variables for the potential power resource +function EMB.variables_flow_resource( + m, + π’œ::Vector{<:Area}, + 𝒫::Vector{<:PotentialPower}, + 𝒯, + modeltype::EnergyModel, +) + 𝒩ᡃᡛ = [availability_node(a) for a ∈ π’œ] + @variable(m, lower_limit(p) ≀ energy_potential_node_in[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] ≀ upper_limit(p)) + @variable(m, lower_limit(p) ≀ energy_potential_node_out[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] ≀ upper_limit(p)) +end function EMB.variables_flow_resource( m, β„³::Vector{<:TransmissionMode}, @@ -130,33 +142,33 @@ function EMB.variables_flow_resource( ) end -function EMB.variables_flow_resource( +# Declare new constraints for the potential power resource using the newly declared variables +function EMB.constraints_resource( m, - π’œ::Vector{<:Area}, + a::Area, + 𝒯::TimeStructure, 𝒫::Vector{<:PotentialPower}, - 𝒯, modeltype::EnergyModel, ) - 𝒩ᡃᡛ = [availability_node(a) for a ∈ π’œ] - @variable(m, lower_limit(p) ≀ energy_potential_node_in[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] ≀ upper_limit(p)) - @variable(m, lower_limit(p) ≀ energy_potential_node_out[n ∈ 𝒩ᡃᡛ, 𝒯, p ∈ 𝒫] ≀ upper_limit(p)) + n = availability_node(a) + @constraint(m, [t ∈ 𝒯, p ∈ 𝒫], + m[:energy_potential_node_in][n, t, p] == m[:energy_potential_node_out][n, t, p] + ) end - -function EMG.constraints_trans_balance( +function EMB.constraints_resource( m, tm::PotentialLossMode, 𝒯::TimeStructure, + 𝒫::Vector{<:PotentialPower}, modeltype::EnergyModel, ) - @constraint(m, [t ∈ 𝒯], - m[:trans_out][tm, t] == m[:trans_in][tm, t] - m[:trans_loss][tm, t] - ) @constraint(m, [t ∈ 𝒯, p ∈ outputs(tm)], m[:energy_potential_trans_out][tm, t, p] == tm.loss_factor * m[:energy_potential_trans_in][tm, t, p] ) end +# Declare new coupling constraints for the potential power resource function EMB.constraints_couple_resource( m, π’œ::Vector{<:Area}, @@ -187,35 +199,63 @@ function EMB.constraints_couple_resource( end @testset "Resource flow | PotentialPower" begin + # Create and run the case case, modeltype = resource_flow_case_with_loss(0.9) + m = optimize(case, modeltype) + general_tests(m) + + # Exctract the case data pp, co2 = get_products(case) 𝒯 = get_time_struct(case) n_t = length(𝒯) - π’œ = get_areas(case) + area_from, area_to = get_areas(case) + n_from = availability_node(area_from) + n_to = availability_node(area_to) ℒᡗʳᡃⁿ˒ = get_transmissions(case) tm = modes(ℒᡗʳᡃⁿ˒)[1] - area_from, area_to = π’œ - - m = optimize(case, modeltype) - general_tests(m) + # Variable testing (calling of the correct function) + # - EMB.variables_flow + # Check that the variables are created @test haskey(m, :energy_potential_trans_in) @test haskey(m, :energy_potential_trans_out) @test haskey(m, :energy_potential_node_in) @test haskey(m, :energy_potential_node_out) + ## Check that the variables have the correct length @test length(m[:energy_potential_trans_in]) == n_t @test length(m[:energy_potential_trans_out]) == n_t @test length(m[:energy_potential_node_in]) == 2 * n_t @test length(m[:energy_potential_node_out]) == 2 * n_t + ## Check that the bounds of the variables are enforced @test all(value(m[:energy_potential_trans_in][tm, t, pp]) β‰₯ lower_limit(pp) for t ∈ 𝒯) @test all(value(m[:energy_potential_trans_in][tm, t, pp]) ≀ upper_limit(pp) for t ∈ 𝒯) @test all(value(m[:energy_potential_trans_out][tm, t, pp]) β‰₯ lower_limit(pp) for t ∈ 𝒯) @test all(value(m[:energy_potential_trans_out][tm, t, pp]) ≀ upper_limit(pp) for t ∈ 𝒯) + # Test that the resource constraints arre correctly enforced + # - EMB.constraints_resource + @test all(value(m[:trans_in][tm, t]) β‰ˆ value(m[:trans_out][tm, t]) for t ∈ 𝒯) + @test all(value(m[:energy_potential_trans_in][tm, t, pp]) < value(m[:trans_in][tm, t]) for t ∈ 𝒯) + @test all(value(m[:energy_potential_trans_out][tm, t, pp]) < value(m[:trans_out][tm, t]) for t ∈ 𝒯) @test all( - value(m[:energy_potential_node_out][availability_node(area_from), t, pp]) β‰ˆ + value(m[:energy_potential_trans_out][tm, t, pp]) β‰ˆ + 0.9 * value(m[:energy_potential_trans_in][tm, t, pp]) + for t ∈ 𝒯) + @test all( + value(m[:energy_potential_node_out][n_from, t, pp]) β‰ˆ + value(m[:energy_potential_node_in][n_from, t, pp]) + for t ∈ 𝒯) + @test all( + value(m[:energy_potential_node_out][n_to, t, pp]) β‰ˆ + value(m[:energy_potential_node_in][n_to, t, pp]) + for t ∈ 𝒯) + + # Test that the coupling constraints are correctly enforced + # - EMB.constraints_couple_resource + @test all( + value(m[:energy_potential_node_out][n_from, t, pp]) β‰ˆ value(m[:energy_potential_trans_in][tm, t, pp]) for t ∈ 𝒯) @test all( @@ -223,15 +263,12 @@ end 0.9 * value(m[:energy_potential_trans_in][tm, t, pp]) for t ∈ 𝒯) @test all( - value(m[:energy_potential_node_in][availability_node(area_to), t, pp]) β‰ˆ + value(m[:energy_potential_node_in][n_to, t, pp]) β‰ˆ value(m[:energy_potential_trans_out][tm, t, pp]) for t ∈ 𝒯) @test all( - value(m[:energy_potential_node_in][availability_node(area_to), t, pp]) < - value(m[:energy_potential_node_out][availability_node(area_from), t, pp]) + value(m[:energy_potential_node_in][n_to, t, pp]) < + value(m[:energy_potential_node_out][n_from, t, pp]) for t ∈ 𝒯) - @test all(value(m[:trans_in][tm, t]) β‰ˆ value(m[:trans_out][tm, t]) for t ∈ 𝒯) - @test all(value(m[:energy_potential_trans_in][tm, t, pp]) < value(m[:trans_in][tm, t]) for t ∈ 𝒯) - @test all(value(m[:energy_potential_trans_out][tm, t, pp]) < value(m[:trans_out][tm, t]) for t ∈ 𝒯) end From d4485820bfadb8d63548f9efd85fc4c3888e3d07 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Tue, 14 Apr 2026 17:13:51 +0200 Subject: [PATCH 14/15] Fixed problems in the documentation --- docs/src/how-to/extend-resource-functionality.md | 4 ++-- src/model.jl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/how-to/extend-resource-functionality.md b/docs/src/how-to/extend-resource-functionality.md index 20ce0f9..12c0076 100644 --- a/docs/src/how-to/extend-resource-functionality.md +++ b/docs/src/how-to/extend-resource-functionality.md @@ -1,6 +1,6 @@ # [Extend Resource functionality](@id how_to-res_funct) -This guide is the `EnergyModelsGeography` counterpart to the resource functionality *[introduced in `EnergyModelsBase`](@ref EnergyModelsBase how_to-res_funct)*. +This guide is the `EnergyModelsGeography` counterpart to the resource functionality *[introduced in `EnergyModelsBase`](@extref EnergyModelsBase how_to-res_funct)*. It shows how that same pattern is used for geography-specific coupling through a concrete example from `test_resource_flow.jl`: a `PotentialPower` resource with dedicated flow variables and coupling constraints. @@ -18,7 +18,7 @@ The goal is to track a resource-specific "potential" flow in parallel with stand !!! tip You can use the same resource type as declared in `EnergyModelsBase` or any other package. - This corresponds to *[step 1 in the example of `EnergyModelsBase`](@ref EnergyModelsBase how_to-res_funct-example)*. + This corresponds to *[step 1 in the example of `EnergyModelsBase`](@extref EnergyModelsBase how_to-res_funct-example)*. ```julia struct PotentialPower <: Resource diff --git a/src/model.jl b/src/model.jl index a47a2ee..b542ce7 100644 --- a/src/model.jl +++ b/src/model.jl @@ -392,8 +392,8 @@ end EMB.constraints_resource(m, tm::TransmissionMode, 𝒯, 𝒫::Vector{<:Resource}, modeltype::EnergyModel) Create constraints for the flow of resources through an [`AbstractElement`](@ref) for -specific resource types. In `EnergyModelsBase`, this method is provided for -[`Node`](@ref EnergyModelsBase.Node) and [`Link`](@ref). +specific resource types. In `EnergyModelsGeography`, this method is provided for +[`Area`](@ref) and [`TransmissionMode`](@ref). The function is empty by default and can be implemented in extension packages. From a4b94364f8629b2ac23fbe5f25ca754107fb5e75 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Tue, 14 Apr 2026 17:19:28 +0200 Subject: [PATCH 15/15] Fixed external reference --- src/model.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/model.jl b/src/model.jl index b542ce7..77a4b9d 100644 --- a/src/model.jl +++ b/src/model.jl @@ -391,9 +391,9 @@ end EMB.constraints_resource(m, a::Area, 𝒯, 𝒫::Vector{<:Resource}, modeltype::EnergyModel) EMB.constraints_resource(m, tm::TransmissionMode, 𝒯, 𝒫::Vector{<:Resource}, modeltype::EnergyModel) -Create constraints for the flow of resources through an [`AbstractElement`](@ref) for -specific resource types. In `EnergyModelsGeography`, this method is provided for -[`Area`](@ref) and [`TransmissionMode`](@ref). +Create constraints for the flow of resources through an +[`AbstractElement`](@extref EnergyModelsBase.AbstractElement) for specific resource types. +In `EnergyModelsGeography`, this method is provided for [`Area`](@ref) and [`TransmissionMode`](@ref). The function is empty by default and can be implemented in extension packages.