diff --git a/NEWS.md b/NEWS.md index ad43f16..99d167f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,15 @@ # Release notes +## Version 0.11.6 (2026-04-15) + +### Add support for resource-specific constraint functions + +* 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) ### Adjustments diff --git a/Project.toml b/Project.toml index 81add88..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,8 +16,7 @@ EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" EMIExt = "EnergyModelsInvestments" [compat] -EnergyModelsBase = "0.9.1" -EnergyModelsInvestments = "0.8" +EnergyModelsBase = "0.9.5" SparseVariables = "0.7.3" JuMP = "1.5" TimeStruct = "0.9" diff --git a/docs/make.jl b/docs/make.jl index cf472c3..17c0ad3 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[ 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..12c0076 --- /dev/null +++ b/docs/src/how-to/extend-resource-functionality.md @@ -0,0 +1,164 @@ +# [Extend Resource functionality](@id 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. + +!!! 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. + +### 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`](@extref EnergyModelsBase how_to-res_funct-example)*. + +```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 [`Area`] and [`Node`] to introduce new 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 + +function EMB.variables_flow_resource( + m, + 𝒩::Vector{<:Node}, + 𝒫::Vector{<:PotentialPower}, + 𝒯, + modeltype::EnergyModel, +) + @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 the function + +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 EMB.constraints_resource( + m, + tm::PotentialLossMode, + 𝒯::TimeStructure, + 𝒫::Vector{<:PotentialPower}, + modeltype::EnergyModel, +) + @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) diff --git a/docs/src/how-to/update-models.md b/docs/src/how-to/update-models.md index ffe9ca1..714b4d8 100644 --- a/docs/src/how-to/update-models.md +++ b/docs/src/how-to/update-models.md @@ -38,7 +38,7 @@ Furthermore, we reworked the design for inclusion of emission and OPEX variables ### [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..efef321 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 diff --git a/docs/src/library/internals/methods_EMB.md b/docs/src/library/internals/methods_EMB.md index 27db5ed..1c3c303 100644 --- a/docs/src/library/internals/methods_EMB.md +++ b/docs/src/library/internals/methods_EMB.md @@ -13,7 +13,9 @@ EMB.create_node EMB.objective_operational EMB.emissions_operational EMB.constraints_elements +EMB.constraints_resource EMB.constraints_couple +EMB.constraints_couple_resource ``` ## [Variable methods](@id lib-int-met_emb-var) @@ -21,6 +23,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/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 97353ef..f096908 100644 --- a/docs/src/manual/constraint-functions.md +++ b/docs/src/manual/constraint-functions.md @@ -5,6 +5,9 @@ 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 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) ```julia diff --git a/src/model.jl b/src/model.jl index 42ad556..77a4b9d 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,25 +69,59 @@ 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) +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 ∈ EMB.res_types_vec(𝒫) + 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 ∈ EMB.res_types_vec(𝒫) + EMB.variables_flow_resource(m, π’œ, p_sub, 𝒯, modeltype) + end end +""" + 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. + +The methods are called from [`EMB.variables_flow`](@ref) after segmenting `𝒫` through +`EMB.res_types_vec(𝒫)`. + +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 + """ EMB.variables_opex(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, π’³α΅›α΅‰αΆœ, 𝒯, modeltype::EnergyModel) EMB.variables_opex(m, π’œ::Vector{<:Area}, π’³α΅›α΅‰αΆœ, 𝒯, modeltype::EnergyModel) @@ -332,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`](@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. + +!!! 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) @@ -382,11 +450,28 @@ 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 ∈ 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) return EMB.constraints_couple(m, π’œ, ℒᡗʳᡃⁿ˒, 𝒫, 𝒯, modeltype) end +""" + EMB.constraints_couple_resource(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{Transmission}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) + +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 intended to be implemented in extension packages. +""" +function EMB.constraints_couple_resource(m, π’œ::Vector{<:Area}, ℒᡗʳᡃⁿ˒::Vector{<:Transmission}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel) end + """ EMB.emissions_operational(m, ℒᡗʳᡃⁿ˒::Vector{Transmission}, 𝒫ᡉᡐ, 𝒯, modeltype::EnergyModel) @@ -458,7 +543,6 @@ 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 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..583e5e4 --- /dev/null +++ b/test/test_resource_flow.jl @@ -0,0 +1,274 @@ +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 + +# 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}, + 𝒫::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 + +# Declare new constraints for the potential power resource using the newly declared variables +function EMB.constraints_resource( + m, + a::Area, + 𝒯::TimeStructure, + 𝒫::Vector{<:PotentialPower}, + modeltype::EnergyModel, +) + 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 EMB.constraints_resource( + m, + tm::PotentialLossMode, + 𝒯::TimeStructure, + 𝒫::Vector{<:PotentialPower}, + modeltype::EnergyModel, +) + @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}, + ℒᡗʳᡃⁿ˒::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 + +@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(𝒯) + 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] + + # 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_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( + 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_in][n_to, t, pp]) β‰ˆ + value(m[:energy_potential_trans_out][tm, t, pp]) + for t ∈ 𝒯) + + @test all( + value(m[:energy_potential_node_in][n_to, t, pp]) < + value(m[:energy_potential_node_out][n_from, t, pp]) + for t ∈ 𝒯) +end