diff --git a/NEWS.md b/NEWS.md index 9dd53b3e..5099c893 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # Release notes +## Unversioned + +* New functions (`variables_flow_resource()`, `constraints_resource()`, `constraints_couple_resource()`) that dispatch on resource types, which allow for creation of new resource-specific variables and constraints in extension packages. +* New function to indentify the unique resource types of a vector of resources +* New function that segments the vector of resources into sub-vectors based on each resource type + ## Version 0.9.4 (2025-11-26) ### Bugfixes diff --git a/Project.toml b/Project.toml index a6366609..1f2b997f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "EnergyModelsBase" uuid = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" authors = ["Lars Hellemo , Julian Straus "] -version = "0.9.4" +version = "0.9.5" [deps] JuMP = "4076af6c-e467-56ae-b986-b466b2749572" diff --git a/docs/make.jl b/docs/make.jl index f09693c4..9beed0a6 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -58,6 +58,7 @@ makedocs( "How to" => Any[ "Create a new element"=>"how-to/create_new_element.md", "Create a new node"=>"how-to/create-new-node.md", + "Extend resource functionality"=>"how-to/extend-resource-functionality.md", "Utilize TimeStruct"=>"how-to/utilize-timestruct.md", "Update models"=>"how-to/update-models.md", "Contribute to EnergyModelsBase"=>"how-to/contribute.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 00000000..6cbbdf89 --- /dev/null +++ b/docs/src/how-to/extend-resource-functionality.md @@ -0,0 +1,250 @@ +# [Extend resource functionality](@id how_to-extend-resource-functionality) + +```@meta +CurrentModule = EMB +``` + +This guide shows how to extend resource functionality by adding a custom resource +type and connecting it to custom variables and constraints through +resource-dispatch functions. This is useful for modelling more complex +resource behavior that cannot be captured by the default resource types where the standard +behavior is built around energy or mass flow. + +The pattern follows the same structure as the resource dispatch test in +`test/test_resource.jl`: + +1. Define a resource subtype with extra parameters. +2. Optionally create a custom node subtype that uses the resource. +3. Add resource-specific variables with `variables_flow_resource`. +4. Add resource-specific constraints with `constraints_resource`. +5. Couple node and link resource variables with `constraints_couple_resource`. + +The example in the test suite defines a `PotentialPower` resource that has a potential, +with upper and lower bounds, in addition to energy flow. The flow of this potential +in and out of junctions follow equality constraints, as opposed to the energy and mass flow +which follow sum constraints. + +The notation below follows the same conventions as the implementation and tests: + +- `๐’ฉ` for nodes +- `โ„’` for links +- `๐’ซ` for resources +- `๐’ฏ` for the time structure +- `โ„’แถ สณแต’แต`, `โ„’แต—แต’` for outgoing and incoming links of a node +- `๐’ซแต’แต˜แต—`, `๐’ซโฑโฟ`, `๐’ซหกโฑโฟแต` for resource subsets on outputs, inputs, and links + +## 1. Define a special resource + +Create a subtype of [`Resource`](@ref) and keep `co2_int` as the second field for +consistency with existing resource structures. + +```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 +``` + +## 2. Define a custom node (optional) + +If your resource needs dedicated node behavior, create a custom node subtype. +If the node subtype is parametrized, it can handle different types of resources +in different ways without defining multiple node types. In the dispatch test, +the custom node is an intermediate `NetworkNode` with a potential loss, but +without a loss in energy flow. + +```julia +struct PotentialLossNode{T<:PotentialPower} <: NetworkNode + id::Any + cap::TimeProfile + opex_var::TimeProfile + opex_fixed::TimeProfile + resource::T + input::Dict{<:Resource,<:Real} + output::Dict{<:Resource,<:Real} + data::Vector{<:ExtensionData} + loss_factor::Float64 +end + +function PotentialLossNode( + id, + cap::TimeProfile, + opex_var::TimeProfile, + opex_fixed::TimeProfile, + resource::T, + loss_factor::Float64, +) where {T<:PotentialPower} + return PotentialLossNode{T}( + id, + cap, + opex_var, + opex_fixed, + resource, + Dict(resource => 1.0), + Dict(resource => 1.0), + ExtensionData[], + loss_factor, + ) +end +``` + +## 3. Declare resource-specific variables + +Use [`variables_flow_resource`](@ref) to create resource variables. + +Important: +- Declare each variable name once. +- Filter `๐’ฉ` and `โ„’` down to the subsets that actually use the special resource. +- Keep bounds in `constraints_resource` when they depend on dispatch logic. + +```julia +function EMB.variables_flow_resource( + m, + ๐’ฉ::Vector{<:EMB.Node}, + ๐’ซ::Vector{<:PotentialPower}, + ๐’ฏ, + modeltype::EnergyModel, +) + output_nodes = filter(n -> any(p โˆˆ ๐’ซ for p โˆˆ outputs(n)), ๐’ฉ) + input_nodes = filter(n -> any(p โˆˆ ๐’ซ for p โˆˆ inputs(n)), ๐’ฉ) + + @variable( + m, energy_potential_node_out[ + n โˆˆ output_nodes, t โˆˆ ๐’ฏ, p โˆˆ ๐’ซ; p โˆˆ outputs(n) + ] + ) + + @variable( + m, energy_potential_node_in[ + n โˆˆ input_nodes, t โˆˆ ๐’ฏ, p โˆˆ intersect(inputs(n), ๐’ซ) + ] + ) +end + +function EMB.variables_flow_resource( + m, + โ„’::Vector{<:Link}, + ๐’ซ::Vector{<:PotentialPower}, + ๐’ฏ, + modeltype::EnergyModel, +) + โ„’แต‰แต– = filter(l -> any(p โˆˆ ๐’ซ for p โˆˆ EMB.link_res(l)), โ„’) + @variable(m, energy_potential_link_in[โ„’แต‰แต–, ๐’ฏ, ๐’ซ]) + @variable(m, energy_potential_link_out[โ„’แต‰แต–, ๐’ฏ, ๐’ซ]) +end +``` + +## 4. Add resource-specific constraints + +Use [`constraints_resource`](@ref) for custom node or link behavior. + +```julia +function EMB.constraints_resource( + m, + n::PotentialLossNode, + ๐’ฏ, + ๐’ซ::Vector{<:PotentialPower}, + modeltype::EnergyModel, +) + ๐’ซแต’แต˜แต— = filter(p -> p โˆˆ ๐’ซ, outputs(n)) + ๐’ซโฑโฟ = filter(p -> p โˆˆ ๐’ซ, inputs(n)) + + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซแต’แต˜แต—], + m[:energy_potential_node_out][n, t, p] == + n.loss_factor * m[:energy_potential_node_in][n, t, p] + ) + + # Bounds are added as constraints because they rely on `p`, + # which is an index in `energy_potential` variables. + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซแต’แต˜แต—], + m[:energy_potential_node_out][n, t, p] >= lower_limit(p) + ) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซแต’แต˜แต—], + m[:energy_potential_node_out][n, t, p] <= upper_limit(p) + ) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซโฑโฟ], + m[:energy_potential_node_in][n, t, p] >= lower_limit(p) + ) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซโฑโฟ], + m[:energy_potential_node_in][n, t, p] <= upper_limit(p) + ) + +end + +function EMB.constraints_resource( + m, + n::EMB.Node, + ๐’ฏ, + ๐’ซ::Vector{<:PotentialPower}, + modeltype::EnergyModel, +) + ๐’ซแต’แต˜แต— = filter(p -> p โˆˆ ๐’ซ, outputs(n)) + ๐’ซโฑโฟ = filter(p -> p โˆˆ ๐’ซ, inputs(n)) + + # Bounds are added as constraints because they rely on `p`, + # which is an index in `energy_potential` variables. + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซแต’แต˜แต—], + m[:energy_potential_node_out][n, t, p] >= lower_limit(p) + ) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซแต’แต˜แต—], + m[:energy_potential_node_out][n, t, p] <= upper_limit(p) + ) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซโฑโฟ], + m[:energy_potential_node_in][n, t, p] >= lower_limit(p) + ) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซโฑโฟ], + m[:energy_potential_node_in][n, t, p] <= upper_limit(p) + ) +end + +function EMB.constraints_resource( + m, + l::Link, + ๐’ฏ, + ๐’ซ::Vector{<:PotentialPower}, + modeltype::EnergyModel, +) + ๐’ซหกโฑโฟแต = filter(p -> p โˆˆ ๐’ซ, EMB.link_res(l)) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซหกโฑโฟแต], + m[:energy_potential_link_in][l, t, p] == + m[:energy_potential_link_out][l, t, p] + ) +end +``` + +## 5. Couple node and link variables + +Use [`constraints_couple_resource`](@ref) to connect node and link resource variables. + +```julia +function EMB.constraints_couple_resource( + m, + ๐’ฉ::Vector{<:EMB.Node}, + โ„’::Vector{<:Link}, + ๐’ซ::Vector{<:PotentialPower}, + ๐’ฏ, + modeltype::EnergyModel, +) + for n โˆˆ ๐’ฉ + โ„’แถ สณแต’แต, โ„’แต—แต’ = EMB.link_sub(โ„’, n) + ๐’ซแต’แต˜แต— = filter(p -> p โˆˆ ๐’ซ, outputs(n)) + ๐’ซโฑโฟ = filter(p -> p โˆˆ ๐’ซ, inputs(n)) + + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซแต’แต˜แต—, l โˆˆ โ„’แถ สณแต’แต], + m[:energy_potential_node_out][n, t, p] == + m[:energy_potential_link_in][l, t, p] + ) + + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซโฑโฟ, l โˆˆ โ„’แต—แต’], + m[:energy_potential_link_out][l, t, p] == + m[:energy_potential_node_in][n, t, p] + ) + end +end +``` diff --git a/docs/src/how-to/utilize-timestruct.md b/docs/src/how-to/utilize-timestruct.md index 85c5dc50..79c253da 100644 --- a/docs/src/how-to/utilize-timestruct.md +++ b/docs/src/how-to/utilize-timestruct.md @@ -39,7 +39,7 @@ op_number = length(op_duration) operational_periods = SimpleTimes(op_number, op_duration) # output -SimpleTimes{Int64}(11, [4, 2, 1, 1, 2, 4, 2, 1, 1, 2, 4]) +SimpleTimes{Int64}(11, [4, 2, 1, 1, 2, 4, 2, 1, 1, 2, 4], 24) ``` In this case, we model the day not with hourly resolution, but only have hourly resolution in the morning and afternoon. @@ -60,7 +60,7 @@ Instead, one can also write operational_periods = SimpleTimes(op_duration) # output -SimpleTimes{Int64}(11, [4, 2, 1, 1, 2, 4, 2, 1, 1, 2, 4]) +SimpleTimes{Int64}(11, [4, 2, 1, 1, 2, 4, 2, 1, 1, 2, 4], 24) ``` and a constructor will automatically deduce that there have to be 11 operational periods. diff --git a/docs/src/index.md b/docs/src/index.md index b58b721f..6a964788 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -61,6 +61,7 @@ Depth = 1 Pages = [ "how-to/create_new_element.md", "how-to/create-new-node.md", + "how-to/extend-resource-functionality.md", "how-to/utilize-timestruct.md", "how-to/update-models.md", "how-to/contribute.md", diff --git a/docs/src/library/internals/functions.md b/docs/src/library/internals/functions.md index 09cf9c18..3c9d1883 100644 --- a/docs/src/library/internals/functions.md +++ b/docs/src/library/internals/functions.md @@ -28,6 +28,8 @@ emissions_operational constraints_emissions constraints_elements constraints_couple +constraints_couple_resource +constraints_resource constraints_level_iterate constraints_level_rp constraints_level_scp @@ -39,6 +41,7 @@ constraints_level_bounds ```@docs variables_capacity variables_flow +variables_flow_resource variables_opex variables_capex variables_emission @@ -96,4 +99,6 @@ res_sub ```@docs collect_types sort_types +res_types +res_types_vec ``` diff --git a/src/model.jl b/src/model.jl index 894f5243..327ab14c 100644 --- a/src/model.jl +++ b/src/model.jl @@ -60,7 +60,7 @@ function create_model( # Declaration of element variables and constraints of the problem for ๐’ณ โˆˆ ๐’ณแต›แต‰แถœ variables_capacity(m, ๐’ณ, ๐’ณแต›แต‰แถœ, ๐’ฏ, modeltype) - variables_flow(m, ๐’ณ, ๐’ณแต›แต‰แถœ, ๐’ฏ, modeltype) + variables_flow(m, ๐’ณ, ๐’ณแต›แต‰แถœ, ๐’ซ, ๐’ฏ, modeltype) variables_opex(m, ๐’ณ, ๐’ณแต›แต‰แถœ, ๐’ฏ, modeltype) variables_capex(m, ๐’ณ, ๐’ณแต›แต‰แถœ, ๐’ฏ, modeltype) variables_emission(m, ๐’ณ, ๐’ณแต›แต‰แถœ, ๐’ซ, ๐’ฏ, modeltype) @@ -230,7 +230,7 @@ By default, all nodes `๐’ฉ` and links `โ„’` only allow for unidirectional flow. bidirectional flow through providing a method to the function [`is_unidirectional`](@ref) for new link/node types. """ -function variables_flow(m, ๐’ฉ::Vector{<:Node}, ๐’ณแต›แต‰แถœ, ๐’ฏ, modeltype::EnergyModel) +function variables_flow(m, ๐’ฉ::Vector{<:Node}, ๐’ณแต›แต‰แถœ, ๐’ซ, ๐’ฏ, modeltype::EnergyModel) # Extract the nodes with inputs and outputs ๐’ฉโฑโฟ = filter(has_input, ๐’ฉ) ๐’ฉแต’แต˜แต— = filter(has_output, ๐’ฉ) @@ -249,8 +249,14 @@ function variables_flow(m, ๐’ฉ::Vector{<:Node}, ๐’ณแต›แต‰แถœ, ๐’ฏ, modeltype: for n_out โˆˆ ๐’ฉแต’แต˜แต—โปแต˜โฟโฑ, t โˆˆ ๐’ฏ, p โˆˆ outputs(n_out) set_lower_bound(m[:flow_out][n_out, t, p], 0) end + + # Create new flow variables for specific resource types + for p_sub in res_types_vec(๐’ซ) + variables_flow_resource(m, ๐’ฉ, p_sub, ๐’ฏ, modeltype) + end + end -function variables_flow(m, โ„’::Vector{<:Link}, ๐’ณแต›แต‰แถœ, ๐’ฏ, modeltype::EnergyModel) +function variables_flow(m, โ„’::Vector{<:Link}, ๐’ณแต›แต‰แถœ, ๐’ซ, ๐’ฏ, modeltype::EnergyModel) # Create the link flow variables @variable(m, link_in[l โˆˆ โ„’, ๐’ฏ, inputs(l)]) @variable(m, link_out[l โˆˆ โ„’, ๐’ฏ, outputs(l)]) @@ -266,8 +272,34 @@ function variables_flow(m, โ„’::Vector{<:Link}, ๐’ณแต›แต‰แถœ, ๐’ฏ, modeltype:: set_lower_bound(m[:link_out][l, t, p], 0) end end + + # Create new flow variables for specific resource types + for p_sub in res_types_vec(๐’ซ) + variables_flow_resource(m, โ„’, p_sub, ๐’ฏ, modeltype) + end end +# 5-parameter backward compatibility wrapper (for extension packages with old signature) +function variables_flow(m, ๐’ณ::Vector{<:AbstractElement}, ๐’ณแต›แต‰แถœ, ๐’ฏ, modeltype::EnergyModel) + variables_flow(m, ๐’ณ, ๐’ณแต›แต‰แถœ, Resource[], ๐’ฏ, modeltype) +end + +""" + variables_flow_resource(m, โ„’::Vector{<:Link}, ๐’ซ::Vector{<:Resource}, ๐’ฏ, modeltype::EnergyModel) + variables_flow_resource(m, ๐’ฉ::Vector{<:Node}, ๐’ซ::Vector{<:Resource}, ๐’ฏ, modeltype::EnergyModel) + +Create resource-specific flow variables for links or nodes. + +This function is called from [`variables_flow`](@ref) for each subset of resources +sharing the same type. It can be used to add variables and bounds for specialized +resource classes while keeping the default flow variables unchanged. + +The default methods are empty and intended to be implemented in extension packages. +""" +function variables_flow_resource(m, โ„’::Vector{<:Link}, ๐’ซ::Vector{<:Resource}, ๐’ฏ, modeltype::EnergyModel) end +function variables_flow_resource(m, ๐’ฉ::Vector{<:Node}, ๐’ซ::Vector{<:Resource}, ๐’ฏ, modeltype::EnergyModel) end + + """ variables_opex(m, ๐’ฉ::Vector{<:Node}, ๐’ณแต›แต‰แถœ, ๐’ฏ, modeltype::EnergyModel) variables_opex(m, โ„’::Vector{<:Link}, ๐’ณแต›แต‰แถœ, ๐’ฏ, modeltype::EnergyModel) @@ -562,9 +594,8 @@ end create_element(m, n::Node, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) create_element(m, l::Link, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) -Default fallback method for an element type if no other method is defined for a given type. -This function calls subfunctions to maintain backwards compatibility and simplify the -differentiation in extension packages. +Calls the create functions for the specific elements to add element specific constraints, +also add resource specific constraints through constraints_resource. `EnergyModelsBase` provides the user with two element types, [`Link`](@ref) and [`Node`](@ref EnergyModelsBase.Node): @@ -572,11 +603,41 @@ differentiation in extension packages. - `Node` - the subfunction is [`create_node`](@ref). - `Link` - the subfunction is [`create_link`](@ref). """ -create_element(m, n::Node, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) = +function create_element(m, n::Node, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) + create_node(m, n, ๐’ฏ, ๐’ซ, modeltype) -create_element(m, l::Link, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) = + + # Constraints based on the resource types + node_resources = Vector{Resource}(unique(vcat(inputs(n), outputs(n)))) + for ๐’ซหขแต˜แต‡ in res_types_vec(node_resources) + constraints_resource(m, n, ๐’ฏ, ๐’ซหขแต˜แต‡, modeltype) + end +end + +function create_element(m, l::Link, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) + create_link(m, l, ๐’ฏ, ๐’ซ, modeltype) + # Constraints based on the resource types + for ๐’ซหขแต˜แต‡ in res_types_vec(link_res(l)) + constraints_resource(m, l, ๐’ฏ, ๐’ซหขแต˜แต‡, modeltype) + end +end + +""" + constraints_resource(m, n::Node, ๐’ฏ, ๐’ซ::Vector{<:Resource}, modeltype::EnergyModel) + constraints_resource(m, l::Link, ๐’ฏ, ๐’ซ::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. +""" +function constraints_resource(m, n::Node, ๐’ฏ, ๐’ซ::Vector{<:Resource}, modeltype::EnergyModel) end + +function constraints_resource(m, l::Link, ๐’ฏ, ๐’ซ::Vector{<:Resource}, modeltype::EnergyModel) end + """ constraints_couple(m, ๐’ฉ::Vector{<:Node}, โ„’::Vector{<:Link}, ๐’ซ, ๐’ฏ, modeltype::EnergyModel) constraints_couple(m, โ„’::Vector{<:Link}, ๐’ฉ::Vector{<:Node}, ๐’ซ, ๐’ฏ, modeltype::EnergyModel) @@ -590,6 +651,7 @@ for the coupling between a [`Link`](@ref) and a [`Node`](@ref EnergyModelsBase.N function constraints_couple(m, ๐’ฉ::Vector{<:Node}, โ„’::Vector{<:Link}, ๐’ซ, ๐’ฏ, modeltype::EnergyModel) for n โˆˆ ๐’ฉ โ„’แถ สณแต’แต, โ„’แต—แต’ = link_sub(โ„’, n) + # Constraint for output flowrate and input links. if has_output(n) @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ outputs(n)], @@ -597,6 +659,7 @@ function constraints_couple(m, ๐’ฉ::Vector{<:Node}, โ„’::Vector{<:Link}, ๐’ซ, sum(m[:link_in][l, t, p] for l โˆˆ โ„’แถ สณแต’แต if p โˆˆ inputs(l)) ) end + # Constraint for input flowrate and output links. if has_input(n) @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ inputs(n)], @@ -605,11 +668,29 @@ function constraints_couple(m, ๐’ฉ::Vector{<:Node}, โ„’::Vector{<:Link}, ๐’ซ, ) end end + + # Create new constraints for specific resource types + for p_sub in res_types_vec(๐’ซ) + constraints_couple_resource(m, ๐’ฉ, โ„’, p_sub, ๐’ฏ, modeltype) + end end function constraints_couple(m, โ„’::Vector{<:Link}, ๐’ฉ::Vector{<:Node}, ๐’ซ, ๐’ฏ, modeltype::EnergyModel) return constraints_couple(m, ๐’ฉ, โ„’, ๐’ซ, ๐’ฏ, modeltype) end +""" + constraints_couple_resource(m, ๐’ฉ::Vector{<:Node}, โ„’::Vector{<:Link}, ๐’ซ::Vector{<:Resource}, ๐’ฏ, modeltype::EnergyModel) + +Create resource-specific coupling constraints between nodes and links. + +This function is called from [`constraints_couple`](@ref) for each subset of resources +sharing the same type. It can be used to add additional coupling constraints for +specialized resource classes while keeping the default node-link flow balance unchanged. + +The default method is empty and intended to be implemented in extension packages. +""" +function constraints_couple_resource(m, ๐’ฉ::Vector{<:Node}, โ„’::Vector{<:Link}, ๐’ซ::Vector{<:Resource}, ๐’ฏ, modeltype::EnergyModel) end + """ constraints_emissions(m, ๐’ณแต›แต‰แถœ, ๐’ซ, ๐’ฏ, modeltype::EnergyModel) @@ -956,7 +1037,6 @@ function create_link(m, l::Direct, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) ) end function create_link(m, ๐’ฏ, ๐’ซ, l::Link, modeltype::EnergyModel, formulation::Formulation) - # Generic link in which each output corresponds to the input @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ link_res(l)], m[:link_out][l, t, p] == m[:link_in][l, t, p] @@ -966,4 +1046,4 @@ function create_link(m, ๐’ฏ, ๐’ซ, l::Link, modeltype::EnergyModel, formulation if has_capacity(l) constraints_capacity_installed(m, l, ๐’ฏ, modeltype) end -end +end \ No newline at end of file diff --git a/src/structures/resource.jl b/src/structures/resource.jl index 14fa0197..b60fa43a 100644 --- a/src/structures/resource.jl +++ b/src/structures/resource.jl @@ -86,3 +86,17 @@ Returns all emission resources for a """ res_em(๐’ซ::Array{<:Resource}) = filter(is_resource_emit, ๐’ซ) res_em(๐’ซ::Dict) = filter(p -> is_resource_emit(first(p)), ๐’ซ) + +""" + res_types(๐’ซ::Vector{<:Resource}) + +Return the unique resource types in an Vector of resources `๐’ซ`. +""" +res_types(๐’ซ::Vector{<:Resource}) = unique(map(x -> typeof(x), ๐’ซ)) + +""" + res_types_vec(๐’ซ::Vector{<:Resource}) + +Return a Vector-of-Vectors of resources by the concrete sub-types, if the input is empty it returns an empty Vector. +""" +res_types_vec(๐’ซ::Vector{<:Resource}) = [Vector{rt}(filter(x -> isa(x, rt), ๐’ซ)) for rt in res_types(๐’ซ)] \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index b12417d6..898f27de 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,6 +21,10 @@ ENV["EMB_TEST"] = true # Set flag for example scripts to check if they are run a include("test_data.jl") end + @testset "Base | Resource" begin + include("test_resource.jl") + end + @testset "Base | Node" begin include("test_nodes.jl") end diff --git a/test/test_resource.jl b/test/test_resource.jl new file mode 100644 index 00000000..a0651960 --- /dev/null +++ b/test/test_resource.jl @@ -0,0 +1,287 @@ + +Power = ResourceCarrier("Power", 0.0) +Heat = ResourceCarrier("Heat", 0.0) +CO2 = ResourceEmit("CO2", 1.0) + +๐’ซ = [Power, Heat, CO2] + +@testset "Resource - get resource types" begin + # returns a Vector of DataTypes + @test EMB.res_types(๐’ซ) isa Vector{DataType} + + # returns the correct number of unique resource types + @test length(EMB.res_types(๐’ซ)) == 2 +end + +@testset "Resource - get resource vectors by type" begin + # returns a Vector + @test EMB.res_types_vec(๐’ซ) isa Vector{Vector} + + # returns the correct number of segments + @test length(EMB.res_types_vec(๐’ซ)) == 2 + + # the length of the first segment should be 2 (2 ResourceCarriers) + @test length(EMB.res_types_vec(๐’ซ)[1]) == 2 + + # the length of the second segment should be 1 (1 ResourceEmit) + @test length(EMB.res_types_vec(๐’ซ)[2]) == 1 + +end + +@testset "Resource - get resource vectors by type w/ empty input" begin + + # returns an empty vector when given an empty resource vector + @test isempty(EMB.res_types_vec(Resource[])) +end + +# Add a new resource type and check that it is correctly identified by res_types and res_types_vec +struct TestResource <: Resource + id::String + a::Float64 + b::Int64 +end + +# Add a new resource of type TestResource to the resource vector +๐’ซ = vcat(๐’ซ, [TestResource("Test", 0.5, 1)]) + +@testset "Resource - get resource types w/ custom resource type" begin + # returns a Vector of DataTypes (now including TestResource) + @test EMB.res_types(๐’ซ) isa Vector{DataType} + + # returns the correct number of unique resource types (now 3) + @test length(EMB.res_types(๐’ซ)) == 3 + +end + +@testset "Resource - get resource vectors by type w/ custom resource type" begin + # returns the correct number of segments (now 3) + @test length(EMB.res_types_vec(๐’ซ)) == 3 + + # the length of the first segment should be 2 (2 ResourceCarriers) + @test length(EMB.res_types_vec(๐’ซ)[1]) == 2 + + # the length of the second segment should be 1 (1 ResourceEmit) + @test length(EMB.res_types_vec(๐’ซ)[2]) == 1 + + # the length of the third segment should be 1 (1 TestResource) + @test length(EMB.res_types_vec(๐’ซ)[3]) == 1 +end + + +# Implement a custom resource type and check that it is correctly handled in the model via dispatch +@testset "Resource - energy potential via dispatch" begin + + + 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 + + # A costum node type that represents a potential loss node + # which has an input and output resource and a loss factor that determines how much of the input potential is lost in the node + # but there is no loss in energy + struct PotentialLossNode{T <: PotentialPower} <: NetworkNode + id::Any + cap::TimeProfile + opex_var::TimeProfile + opex_fixed::TimeProfile + resource::T + input::Dict{<:Resource,<:Real} + output::Dict{<:Resource,<:Real} + data::Vector{<:ExtensionData} + loss_factor::Float64 + end + function PotentialLossNode( + id, + cap::TimeProfile, + opex_var::TimeProfile, + opex_fixed::TimeProfile, + resource::T, + loss_factor::Float64, + ) where {T <: PotentialPower} + return PotentialLossNode{T}(id, cap, opex_var, opex_fixed, resource, Dict(resource=>1.0), Dict(resource=>1.0), ExtensionData[], loss_factor) + end + + + function extension_resource_graph(loss_factor::Float64) + + pp = PotentialPower("PotentialPower", 0.0, 0.9, 1.1) + source = RefSource( + "pp_source", + FixedProfile(4), + FixedProfile(10), + FixedProfile(0), + Dict(pp => 1), + ) + loss_node = PotentialLossNode( + "pp_loss", + FixedProfile(4), + FixedProfile(0), + FixedProfile(0), + pp, + loss_factor, + ) + sink = RefSink( + "pp_sink", + FixedProfile(3), + Dict(:surplus => FixedProfile(4), :deficit => FixedProfile(100)), + Dict(pp => 1), + ) + + ops = SimpleTimes(5, 2) + T = TwoLevel(2, 2, ops; op_per_strat = 10) + nodes = [source, loss_node, sink] + links = [ + Direct("src-loss", source, loss_node, Linear()) + Direct("loss-snk", loss_node, sink, Linear()) + ] + modeltype = OperationalModel( + Dict(CO2 => FixedProfile(100)), + Dict(CO2 => FixedProfile(0)), + CO2, + ) + case = Case(T, [pp, CO2], [nodes, links], [[get_nodes, get_links]]) + + return case, modeltype + end + + # Delcare new variables for the potential power resource + function EMB.variables_flow_resource( + m, ๐’ฉ::Vector{<:EMB.Node}, ๐’ซ::Vector{<:PotentialPower}, ๐’ฏ, modeltype::EnergyModel + ) + output_nodes = filter(n -> any(p โˆˆ ๐’ซ for p โˆˆ outputs(n)), ๐’ฉ) + input_nodes = filter(n -> any(p โˆˆ ๐’ซ for p โˆˆ inputs(n)), ๐’ฉ) + + @variable( + m, energy_potential_node_out[ + n โˆˆ output_nodes, t โˆˆ ๐’ฏ, p โˆˆ ๐’ซ; p โˆˆ outputs(n) + ] + ) + + @variable( + m, energy_potential_node_in[ + n โˆˆ input_nodes, t โˆˆ ๐’ฏ, p โˆˆ intersect(inputs(n), ๐’ซ) + ] + ) + end + + function EMB.variables_flow_resource( + m, โ„’::Vector{<:Link}, ๐’ซ::Vector{<:PotentialPower}, ๐’ฏ, modeltype::EnergyModel + ) + โ„’แต‰แต– = filter(l -> any(p โˆˆ ๐’ซ for p โˆˆ EMB.link_res(l)), โ„’) + @variable(m, energy_potential_link_in[โ„’แต‰แต–, ๐’ฏ, ๐’ซ]) + @variable(m, energy_potential_link_out[โ„’แต‰แต–, ๐’ฏ, ๐’ซ]) + end + + # Declare new constraints for the potential power resource using the newly declared variables + function EMB.constraints_resource( + m, n::PotentialLossNode, ๐’ฏ, ๐’ซ::Vector{<:PotentialPower}, modeltype::EnergyModel + ) + ๐’ซแต’แต˜แต— = filter(p -> p โˆˆ ๐’ซ, outputs(n)) + ๐’ซโฑโฟ = filter(p -> p โˆˆ ๐’ซ, inputs(n)) + + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซแต’แต˜แต—], + m[:energy_potential_node_out][n, t, p] >= lower_limit(p) + ) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซแต’แต˜แต—], + m[:energy_potential_node_out][n, t, p] <= upper_limit(p) + ) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซโฑโฟ], + m[:energy_potential_node_in][n, t, p] >= lower_limit(p) + ) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซโฑโฟ], + m[:energy_potential_node_in][n, t, p] <= upper_limit(p) + ) + + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซแต’แต˜แต—], + m[:energy_potential_node_out][n, t, p] == n.loss_factor * m[:energy_potential_node_in][n, t, p] + ) + end + + function EMB.constraints_resource( + m, n::EMB.Node, ๐’ฏ, ๐’ซ::Vector{<:PotentialPower}, modeltype::EnergyModel + ) + ๐’ซแต’แต˜แต— = filter(p -> p โˆˆ ๐’ซ, outputs(n)) + ๐’ซโฑโฟ = filter(p -> p โˆˆ ๐’ซ, inputs(n)) + + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซแต’แต˜แต—], + m[:energy_potential_node_out][n, t, p] >= lower_limit(p) + ) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซแต’แต˜แต—], + m[:energy_potential_node_out][n, t, p] <= upper_limit(p) + ) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซโฑโฟ], + m[:energy_potential_node_in][n, t, p] >= lower_limit(p) + ) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซโฑโฟ], + m[:energy_potential_node_in][n, t, p] <= upper_limit(p) + ) + end + + function EMB.constraints_resource( + m, l::Link, ๐’ฏ, ๐’ซ::Vector{<:PotentialPower}, modeltype::EnergyModel + ) + ๐’ซหกโฑโฟแต = filter(p -> p โˆˆ ๐’ซ, EMB.link_res(l)) + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซหกโฑโฟแต], + m[:energy_potential_link_in][l, t, p] == m[:energy_potential_link_out][l, t, p] + ) + end + + function EMB.constraints_couple_resource( + m, ๐’ฉ::Vector{<:EMB.Node}, โ„’::Vector{<:Link}, + ๐’ซ::Vector{<:PotentialPower}, ๐’ฏ, modeltype::EnergyModel + ) + for n โˆˆ ๐’ฉ + โ„’แถ สณแต’แต, โ„’แต—แต’ = EMB.link_sub(โ„’, n) + ๐’ซแต’แต˜แต— = filter(p -> p โˆˆ ๐’ซ, outputs(n)) + ๐’ซโฑโฟ = filter(p -> p โˆˆ ๐’ซ, inputs(n)) + + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซแต’แต˜แต—, l โˆˆ โ„’แถ สณแต’แต], + m[:energy_potential_node_out][n, t, p] == m[:energy_potential_link_in][l, t, p] + ) + + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซโฑโฟ, l โˆˆ โ„’แต—แต’], + m[:energy_potential_link_out][l, t, p] == m[:energy_potential_node_in][n, t, p] + ) + end + end + + + case, modeltype = extension_resource_graph(0.9) + pp, co2 = get_products(case) + source, loss_node, sink = get_nodes(case) + + m = run_model(case, modeltype, HiGHS.Optimizer) + ๐’ฏ = get_time_struct(case) + โ„’ = get_links(case) + n_t = length(๐’ฏ) + + @test haskey(m, :energy_potential_node_in) + @test haskey(m, :energy_potential_node_out) + @test haskey(m, :energy_potential_link_in) + @test haskey(m, :energy_potential_link_out) + + @test length(m[:energy_potential_node_in]) == 2 * n_t + @test length(m[:energy_potential_node_out]) == 2 * n_t + @test length(m[:energy_potential_link_in]) == length(โ„’) * n_t + @test length(m[:energy_potential_link_out]) == length(โ„’) * n_t + + @test all(value(m[:energy_potential_node_out][source, t, pp]) >= lower_limit(pp) for t โˆˆ ๐’ฏ) + @test all(value(m[:energy_potential_node_out][source, t, pp]) <= upper_limit(pp) for t โˆˆ ๐’ฏ) + @test all(value(m[:energy_potential_node_in][sink, t, pp]) >= lower_limit(pp) for t โˆˆ ๐’ฏ) + @test all(value(m[:energy_potential_node_in][sink, t, pp]) <= upper_limit(pp) for t โˆˆ ๐’ฏ) + + @test all(value(m[:energy_potential_node_out][source, t, pp]) โ‰ˆ value(m[:energy_potential_link_in][โ„’[1], t, pp]) for t โˆˆ ๐’ฏ) + @test all(value(m[:energy_potential_link_out][โ„’[1], t, pp]) โ‰ˆ value(m[:energy_potential_node_in][loss_node, t, pp]) for t โˆˆ ๐’ฏ) + @test all(value(m[:energy_potential_node_out][loss_node, t, pp]) โ‰ˆ loss_node.loss_factor * value(m[:energy_potential_node_in][loss_node, t, pp]) for t โˆˆ ๐’ฏ) + @test all(value(m[:energy_potential_node_out][loss_node, t, pp]) โ‰ˆ value(m[:energy_potential_link_in][โ„’[2], t, pp]) for t โˆˆ ๐’ฏ) + @test all(value(m[:energy_potential_link_out][โ„’[2], t, pp]) โ‰ˆ value(m[:energy_potential_node_in][sink, t, pp]) for t โˆˆ ๐’ฏ) + @test all(value(m[:energy_potential_node_out][loss_node, t, pp]) < value(m[:energy_potential_node_in][loss_node, t, pp]) for t โˆˆ ๐’ฏ) + @test all(value(m[:energy_potential_node_out][source, t, pp]) < value(m[:flow_out][source, t, pp]) for t โˆˆ ๐’ฏ) + @test all(value(m[:energy_potential_node_in][sink, t, pp]) < value(m[:flow_in][sink, t, pp]) for t โˆˆ ๐’ฏ) +end