|
| 1 | +# [Extend resource functionality](@id how_to-extend-resource-functionality) |
| 2 | + |
| 3 | +```@meta |
| 4 | +CurrentModule = EMB |
| 5 | +``` |
| 6 | + |
| 7 | +This guide shows how to extend resource functionality by adding a custom resource |
| 8 | +type and connecting it to custom variables and constraints through |
| 9 | +resource-dispatch functions. This is useful for modelling more complex |
| 10 | +resource behavior that cannot be captured by the default resource types where the standard |
| 11 | +behavior is built around energy or mass flow. |
| 12 | + |
| 13 | +The pattern follows the same structure as the resource dispatch test in |
| 14 | +`test/test_resource.jl`: |
| 15 | + |
| 16 | +1. Define a resource subtype with extra parameters. |
| 17 | +2. Optionally create a custom node subtype that uses the resource. |
| 18 | +3. Add resource-specific variables with `variables_flow_resource`. |
| 19 | +4. Add resource-specific constraints with `constraints_resource`. |
| 20 | +5. Couple node and link resource variables with `constraints_couple_resource`. |
| 21 | + |
| 22 | +The example in the test suite defines a `PotentialPower` resource that has a potential, |
| 23 | +with upper and lower bounds, in addition to energy flow. The flow of this potential |
| 24 | +in and out of junctions follow equality constraints, as opposed to the energy and mass flow |
| 25 | +which follow sum constraints. |
| 26 | + |
| 27 | +The notation below follows the same conventions as the implementation and tests: |
| 28 | + |
| 29 | +- `𝒩` for nodes |
| 30 | +- `ℒ` for links |
| 31 | +- `𝒫` for resources |
| 32 | +- `𝒯` for the time structure |
| 33 | +- `ℒᶠʳᵒᵐ`, `ℒᵗᵒ` for outgoing and incoming links of a node |
| 34 | +- `𝒫ᵒᵘᵗ`, `𝒫ⁱⁿ`, `𝒫ˡⁱⁿᵏ` for resource subsets on outputs, inputs, and links |
| 35 | + |
| 36 | +## 1. Define a special resource |
| 37 | + |
| 38 | +Create a subtype of [`Resource`](@ref) and keep `co2_int` as the second field for |
| 39 | +consistency with existing resource structures. |
| 40 | + |
| 41 | +```julia |
| 42 | +struct PotentialPower <: Resource |
| 43 | + id::String |
| 44 | + co2_int::Float64 |
| 45 | + potential_lower::Float64 |
| 46 | + potential_upper::Float64 |
| 47 | +end |
| 48 | + |
| 49 | +EMB.is_resource_emit(::PotentialPower) = false |
| 50 | +lower_limit(p::PotentialPower) = p.potential_lower |
| 51 | +upper_limit(p::PotentialPower) = p.potential_upper |
| 52 | +``` |
| 53 | + |
| 54 | +## 2. Define a custom node (optional) |
| 55 | + |
| 56 | +If your resource needs dedicated node behavior, create a custom node subtype. |
| 57 | +If the node subtype is parametrized, it can handle different types of resources |
| 58 | +in different ways without defining multiple node types. In the dispatch test, |
| 59 | +the custom node is an intermediate `NetworkNode` with a potential loss, but |
| 60 | +without a loss in energy flow. |
| 61 | + |
| 62 | +```julia |
| 63 | +struct PotentialLossNode{T<:PotentialPower} <: NetworkNode |
| 64 | + id::Any |
| 65 | + cap::TimeProfile |
| 66 | + opex_var::TimeProfile |
| 67 | + opex_fixed::TimeProfile |
| 68 | + resource::T |
| 69 | + input::Dict{<:Resource,<:Real} |
| 70 | + output::Dict{<:Resource,<:Real} |
| 71 | + data::Vector{<:ExtensionData} |
| 72 | + loss_factor::Float64 |
| 73 | +end |
| 74 | + |
| 75 | +function PotentialLossNode( |
| 76 | + id, |
| 77 | + cap::TimeProfile, |
| 78 | + opex_var::TimeProfile, |
| 79 | + opex_fixed::TimeProfile, |
| 80 | + resource::T, |
| 81 | + loss_factor::Float64, |
| 82 | +) where {T<:PotentialPower} |
| 83 | + return PotentialLossNode{T}( |
| 84 | + id, |
| 85 | + cap, |
| 86 | + opex_var, |
| 87 | + opex_fixed, |
| 88 | + resource, |
| 89 | + Dict(resource => 1.0), |
| 90 | + Dict(resource => 1.0), |
| 91 | + ExtensionData[], |
| 92 | + loss_factor, |
| 93 | + ) |
| 94 | +end |
| 95 | +``` |
| 96 | + |
| 97 | +## 3. Declare resource-specific variables |
| 98 | + |
| 99 | +Use [`variables_flow_resource`](@ref) to create resource variables. |
| 100 | + |
| 101 | +Important: |
| 102 | +- Declare each variable name once. |
| 103 | +- Filter `𝒩` and `ℒ` down to the subsets that actually use the special resource. |
| 104 | +- Keep bounds in `constraints_resource` when they depend on dispatch logic. |
| 105 | + |
| 106 | +```julia |
| 107 | +function EMB.variables_flow_resource( |
| 108 | + m, |
| 109 | + 𝒩::Vector{<:EMB.Node}, |
| 110 | + 𝒫::Vector{<:PotentialPower}, |
| 111 | + 𝒯, |
| 112 | + modeltype::EnergyModel, |
| 113 | +) |
| 114 | + output_nodes = filter(n -> any(p ∈ 𝒫 for p ∈ outputs(n)), 𝒩) |
| 115 | + input_nodes = filter(n -> any(p ∈ 𝒫 for p ∈ inputs(n)), 𝒩) |
| 116 | + |
| 117 | + @variable( |
| 118 | + m, energy_potential_node_out[ |
| 119 | + n ∈ output_nodes, t ∈ 𝒯, p ∈ 𝒫; p ∈ outputs(n) |
| 120 | + ] |
| 121 | + ) |
| 122 | + |
| 123 | + @variable( |
| 124 | + m, energy_potential_node_in[ |
| 125 | + n ∈ input_nodes, t ∈ 𝒯, p ∈ intersect(inputs(n), 𝒫) |
| 126 | + ] |
| 127 | + ) |
| 128 | +end |
| 129 | + |
| 130 | +function EMB.variables_flow_resource( |
| 131 | + m, |
| 132 | + ℒ::Vector{<:Link}, |
| 133 | + 𝒫::Vector{<:PotentialPower}, |
| 134 | + 𝒯, |
| 135 | + modeltype::EnergyModel, |
| 136 | +) |
| 137 | + ℒᵉᵖ = filter(l -> any(p ∈ 𝒫 for p ∈ EMB.link_res(l)), ℒ) |
| 138 | + @variable(m, energy_potential_link_in[ℒᵉᵖ, 𝒯, 𝒫]) |
| 139 | + @variable(m, energy_potential_link_out[ℒᵉᵖ, 𝒯, 𝒫]) |
| 140 | +end |
| 141 | +``` |
| 142 | + |
| 143 | +## 4. Add resource-specific constraints |
| 144 | + |
| 145 | +Use [`constraints_resource`](@ref) for custom node or link behavior. |
| 146 | + |
| 147 | +```julia |
| 148 | +function EMB.constraints_resource( |
| 149 | + m, |
| 150 | + n::PotentialLossNode, |
| 151 | + 𝒯, |
| 152 | + 𝒫::Vector{<:PotentialPower}, |
| 153 | + modeltype::EnergyModel, |
| 154 | +) |
| 155 | + 𝒫ᵒᵘᵗ = filter(p -> p ∈ 𝒫, outputs(n)) |
| 156 | + 𝒫ⁱⁿ = filter(p -> p ∈ 𝒫, inputs(n)) |
| 157 | + |
| 158 | + @constraint(m, [t ∈ 𝒯, p ∈ 𝒫ᵒᵘᵗ], |
| 159 | + m[:energy_potential_node_out][n, t, p] == |
| 160 | + n.loss_factor * m[:energy_potential_node_in][n, t, p] |
| 161 | + ) |
| 162 | + |
| 163 | + # Bounds are added as constraints because they rely on `p`, |
| 164 | + # which is an index in `energy_potential` variables. |
| 165 | + @constraint(m, [t ∈ 𝒯, p ∈ 𝒫ᵒᵘᵗ], |
| 166 | + m[:energy_potential_node_out][n, t, p] >= lower_limit(p) |
| 167 | + ) |
| 168 | + @constraint(m, [t ∈ 𝒯, p ∈ 𝒫ᵒᵘᵗ], |
| 169 | + m[:energy_potential_node_out][n, t, p] <= upper_limit(p) |
| 170 | + ) |
| 171 | + @constraint(m, [t ∈ 𝒯, p ∈ 𝒫ⁱⁿ], |
| 172 | + m[:energy_potential_node_in][n, t, p] >= lower_limit(p) |
| 173 | + ) |
| 174 | + @constraint(m, [t ∈ 𝒯, p ∈ 𝒫ⁱⁿ], |
| 175 | + m[:energy_potential_node_in][n, t, p] <= upper_limit(p) |
| 176 | + ) |
| 177 | + |
| 178 | +end |
| 179 | + |
| 180 | +function EMB.constraints_resource( |
| 181 | + m, |
| 182 | + n::EMB.Node, |
| 183 | + 𝒯, |
| 184 | + 𝒫::Vector{<:PotentialPower}, |
| 185 | + modeltype::EnergyModel, |
| 186 | +) |
| 187 | + 𝒫ᵒᵘᵗ = filter(p -> p ∈ 𝒫, outputs(n)) |
| 188 | + 𝒫ⁱⁿ = filter(p -> p ∈ 𝒫, inputs(n)) |
| 189 | + |
| 190 | + # Bounds are added as constraints because they rely on `p`, |
| 191 | + # which is an index in `energy_potential` variables. |
| 192 | + @constraint(m, [t ∈ 𝒯, p ∈ 𝒫ᵒᵘᵗ], |
| 193 | + m[:energy_potential_node_out][n, t, p] >= lower_limit(p) |
| 194 | + ) |
| 195 | + @constraint(m, [t ∈ 𝒯, p ∈ 𝒫ᵒᵘᵗ], |
| 196 | + m[:energy_potential_node_out][n, t, p] <= upper_limit(p) |
| 197 | + ) |
| 198 | + @constraint(m, [t ∈ 𝒯, p ∈ 𝒫ⁱⁿ], |
| 199 | + m[:energy_potential_node_in][n, t, p] >= lower_limit(p) |
| 200 | + ) |
| 201 | + @constraint(m, [t ∈ 𝒯, p ∈ 𝒫ⁱⁿ], |
| 202 | + m[:energy_potential_node_in][n, t, p] <= upper_limit(p) |
| 203 | + ) |
| 204 | +end |
| 205 | + |
| 206 | +function EMB.constraints_resource( |
| 207 | + m, |
| 208 | + l::Link, |
| 209 | + 𝒯, |
| 210 | + 𝒫::Vector{<:PotentialPower}, |
| 211 | + modeltype::EnergyModel, |
| 212 | +) |
| 213 | + 𝒫ˡⁱⁿᵏ = filter(p -> p ∈ 𝒫, EMB.link_res(l)) |
| 214 | + @constraint(m, [t ∈ 𝒯, p ∈ 𝒫ˡⁱⁿᵏ], |
| 215 | + m[:energy_potential_link_in][l, t, p] == |
| 216 | + m[:energy_potential_link_out][l, t, p] |
| 217 | + ) |
| 218 | +end |
| 219 | +``` |
| 220 | + |
| 221 | +## 5. Couple node and link variables |
| 222 | + |
| 223 | +Use [`constraints_couple_resource`](@ref) to connect node and link resource variables. |
| 224 | + |
| 225 | +```julia |
| 226 | +function EMB.constraints_couple_resource( |
| 227 | + m, |
| 228 | + 𝒩::Vector{<:EMB.Node}, |
| 229 | + ℒ::Vector{<:Link}, |
| 230 | + 𝒫::Vector{<:PotentialPower}, |
| 231 | + 𝒯, |
| 232 | + modeltype::EnergyModel, |
| 233 | +) |
| 234 | + for n ∈ 𝒩 |
| 235 | + ℒᶠʳᵒᵐ, ℒᵗᵒ = EMB.link_sub(ℒ, n) |
| 236 | + 𝒫ᵒᵘᵗ = filter(p -> p ∈ 𝒫, outputs(n)) |
| 237 | + 𝒫ⁱⁿ = filter(p -> p ∈ 𝒫, inputs(n)) |
| 238 | + |
| 239 | + @constraint(m, [t ∈ 𝒯, p ∈ 𝒫ᵒᵘᵗ, l ∈ ℒᶠʳᵒᵐ], |
| 240 | + m[:energy_potential_node_out][n, t, p] == |
| 241 | + m[:energy_potential_link_in][l, t, p] |
| 242 | + ) |
| 243 | + |
| 244 | + @constraint(m, [t ∈ 𝒯, p ∈ 𝒫ⁱⁿ, l ∈ ℒᵗᵒ], |
| 245 | + m[:energy_potential_link_out][l, t, p] == |
| 246 | + m[:energy_potential_node_in][n, t, p] |
| 247 | + ) |
| 248 | + end |
| 249 | +end |
| 250 | +``` |
0 commit comments