Extend resource functionality
Concept
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:
- Define a resource subtype with extra parameters.
- (Optionally) create a custom node subtype that uses the resource.
- Add resource-specific variables with
variables_flow_resource. - Add resource-specific constraints with
constraints_resource. - Couple node and link resource variables with
constraints_couple_resource.
Example
The following example illustrates the different steps that are required for creating a new resource with additional properties. It defines a PotentialPower resource which has as property a potential with upper and lower bounds in addition to its energy flow. The flow of this potential in and out of junctions follows 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, and𝒫ᵒᵘᵗ,𝒫ⁱⁿ,𝒫ˡⁱⁿᵏfor resource subsets on outputs, inputs, and links.
1. Define a special resource
Create a subtype of Resource and keep co2_int as the second field for consistency with existing resource structures. Alternatively, you can create a new method for the internal function co2_int.
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_upper2. 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.
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,
)
end3. Declare resource-specific variables
Use variables_flow_resource to create resource variables.
Important:
- Declare each variable name once.
- Filter
𝒩andℒdown to the subsets that actually use the special resource. - You can create resource dependent bounds as well.
function EMB.variables_flow_resource(
m,
𝒩::Vector{<:EMB.Node},
𝒫::Vector{<:PotentialPower},
𝒯,
modeltype::EnergyModel,
)
𝒩ᵒᵘᵗ = filter(n -> any(p ∈ 𝒫 for p ∈ outputs(n)), 𝒩)
𝒩ⁱⁿ = filter(n -> any(p ∈ 𝒫 for p ∈ inputs(n)), 𝒩)
@variable(m,
lower_limit(p) ≤
energy_potential_node_out[n ∈ 𝒩ᵒᵘᵗ, 𝒯, p ∈ intersect(outputs(n), 𝒫)] ≤
upper_limit(p)
)
@variable(m,
lower_limit(p) ≤
energy_potential_node_in[n ∈ 𝒩ⁱⁿ, 𝒯, p ∈ intersect(inputs(n), 𝒫)] ≤
upper_limit(p)
)
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[ℒᵉᵖ, 𝒯, 𝒫])
end4. Add resource-specific constraints
Create a new method constraints_resource for custom node or link behavior. These methods can be either for the complete set of Node and Links or alternatively for only a specified subset of nodes. If you only specify it for a subset of nodes, it is important that the new resource is only an input or output of this subset.
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]
)
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]
)
end5. Couple node and link variables
Use constraints_couple_resource to connect node and link resource variables.
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