Skip to content

Commit 2de6e3d

Browse files
committed
add resource-type dispatch support and functional resource tests
- add resource-type dispatch integration in `model.jl` for variable creation and coupling constraints - add resource type helpers in `resource.jl` - extend functional docs index and internals references in `make.jl`, `index.md`, and `functions.md` - add/expand end-to-end resource dispatch tests, including node/resource unpacking and bound constraints, in `test_resource.jl` - add release note update in `NEWS.md` - add new how-to page `extend-resource-functionality.md`
1 parent 4cffb4b commit 2de6e3d

8 files changed

Lines changed: 504 additions & 22 deletions

File tree

NEWS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Release notes
22

3-
## Version 0.9.5 (2025-03-23)
3+
## Unversioned
44

55
* 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.
66
* New function to indentify the unique resource types of a vector of resources

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ makedocs(
5858
"How to" => Any[
5959
"Create a new element"=>"how-to/create_new_element.md",
6060
"Create a new node"=>"how-to/create-new-node.md",
61+
"Extend resource functionality"=>"how-to/extend-resource-functionality.md",
6162
"Utilize TimeStruct"=>"how-to/utilize-timestruct.md",
6263
"Update models"=>"how-to/update-models.md",
6364
"Contribute to EnergyModelsBase"=>"how-to/contribute.md",
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
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+
```

docs/src/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Depth = 1
6161
Pages = [
6262
"how-to/create_new_element.md",
6363
"how-to/create-new-node.md",
64+
"how-to/extend-resource-functionality.md",
6465
"how-to/utilize-timestruct.md",
6566
"how-to/update-models.md",
6667
"how-to/contribute.md",

docs/src/library/internals/functions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,5 @@ res_sub
100100
collect_types
101101
sort_types
102102
res_types
103-
res_types_seg
103+
res_types_vec
104104
```

src/model.jl

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ function variables_flow(m, 𝒩::Vector{<:Node}, 𝒳ᵛᵉᶜ, 𝒫, 𝒯, mode
251251
end
252252

253253
# Create new flow variables for specific resource types
254-
for p_sub in res_types_seg(𝒫)
254+
for p_sub in res_types_vec(𝒫)
255255
variables_flow_resource(m, 𝒩, p_sub, 𝒯, modeltype)
256256
end
257257

@@ -274,11 +274,16 @@ function variables_flow(m, ℒ::Vector{<:Link}, 𝒳ᵛᵉᶜ, 𝒫, 𝒯, model
274274
end
275275

276276
# Create new flow variables for specific resource types
277-
for p_sub in res_types_seg(𝒫)
277+
for p_sub in res_types_vec(𝒫)
278278
variables_flow_resource(m, ℒ, p_sub, 𝒯, modeltype)
279279
end
280280
end
281281

282+
# 5-parameter backward compatibility wrapper (for extension packages with old signature)
283+
function variables_flow(m, 𝒳::Vector{<:AbstractElement}, 𝒳ᵛᵉᶜ, 𝒯, modeltype::EnergyModel)
284+
variables_flow(m, 𝒳, 𝒳ᵛᵉᶜ, Resource[], 𝒯, modeltype)
285+
end
286+
282287
"""
283288
variables_flow_resource(m, ℒ::Vector{<:Link}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel)
284289
variables_flow_resource(m, 𝒩::Vector{<:Node}, 𝒫::Vector{<:Resource}, 𝒯, modeltype::EnergyModel)
@@ -605,7 +610,7 @@ function create_element(m, n::Node, 𝒯, 𝒫, modeltype::EnergyModel)
605610

606611
# Constraints based on the resource types
607612
node_resources = Vector{Resource}(unique(vcat(inputs(n), outputs(n))))
608-
for 𝒫ˢᵘᵇ in res_types_seg(node_resources)
613+
for 𝒫ˢᵘᵇ in res_types_vec(node_resources)
609614
constraints_resource(m, n, 𝒯, 𝒫ˢᵘᵇ, modeltype)
610615
end
611616
end
@@ -615,7 +620,7 @@ function create_element(m, l::Link, 𝒯, 𝒫, modeltype::EnergyModel)
615620
create_link(m, l, 𝒯, 𝒫, modeltype)
616621

617622
# Constraints based on the resource types
618-
for 𝒫ˢᵘᵇ in res_types_seg(link_res(l))
623+
for 𝒫ˢᵘᵇ in res_types_vec(link_res(l))
619624
constraints_resource(m, l, 𝒯, 𝒫ˢᵘᵇ, modeltype)
620625
end
621626
end
@@ -666,7 +671,7 @@ function constraints_couple(m, 𝒩::Vector{<:Node}, ℒ::Vector{<:Link}, 𝒫,
666671
end
667672

668673
# Create new constraints for specific resource types
669-
for p_sub in res_types_seg(𝒫)
674+
for p_sub in res_types_vec(𝒫)
670675
constraints_couple_resource(m, 𝒩, ℒ, p_sub, 𝒯, modeltype)
671676
end
672677
end

src/structures/resource.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ Return the unique resource types in an Vector of resources `𝒫`.
9595
res_types(𝒫::Vector{<:Resource}) = unique(map(x -> typeof(x), 𝒫))
9696

9797
"""
98-
res_types_seg(𝒫::Vector{<:Resource})
98+
res_types_vec(𝒫::Vector{<:Resource})
9999
100-
Return a Vector-of-Vectors of resources segmented by the sub-types.
100+
Return a Vector-of-Vectors of resources by the concrete sub-types, if the input is empty it returns an empty Vector.
101101
"""
102-
res_types_seg(𝒫::Vector{<:Resource}) = [Vector{rt}(filter(x -> isa(x, rt), 𝒫)) for rt in res_types(𝒫)]
102+
res_types_vec(𝒫::Vector{<:Resource}) = [Vector{rt}(filter(x -> isa(x, rt), 𝒫)) for rt in res_types(𝒫)]

0 commit comments

Comments
 (0)