Skip to content

Commit 3de31b3

Browse files
authored
Improved checks for links (#76)
* Added general checks for `Link`s * Added checks for `Direct`
1 parent 464f8bc commit 3de31b3

7 files changed

Lines changed: 92 additions & 24 deletions

File tree

NEWS.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
# Release notes
22

3-
## Unversioned
3+
## Version 0.9.4 (2025-11-26)
44

55
### Bugfixes
66

77
* Fix a bug in which field names for the capacities of a `Storage` node resulted in error message for the investment data checks.
88

9+
### Minor updates
10+
11+
* Added checks
12+
* that the `inputs` and `outputs` resources of a `Link` are present in the connected nodes.
13+
* that a `Direct` link has actually `inputs` and `outputs`,
14+
915
## Version 0.9.3 (2025-10-23)
1016

11-
## Bugfixes
17+
### Bugfixes
1218

1319
* Fix a bug in which field names for the capacities of a `Storage` node resulted in unconstrained capacities.
1420

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "EnergyModelsBase"
22
uuid = "5d7e687e-f956-46f3-9045-6f5a5fd49f50"
33
authors = ["Lars Hellemo <Lars.Hellemo@sintef.no>, Julian Straus <Julian.Straus@sintef.no>"]
4-
version = "0.9.3"
4+
version = "0.9.4"
55

66
[deps]
77
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"

src/checks.jl

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,14 @@ and Vector{<:Link}.
183183
- [`check_time_structure`](@ref) to identify time profiles at the highest level that
184184
are not equivalent to the provided timestructure.
185185
186-
In addition, all links are directly checked to have in the fields `:from` and `:to` nodes
187-
that are present in the Node vector as extracted through the function [`get_nodes`](@ref)
188-
and that these nodes have input (`:to`) or output (`:from`).
186+
In addition, all links are directly checked:
187+
188+
- The nodes in the fields `:from` and `:to` are present in the Node vector as extracted
189+
through the function [`get_nodes`](@ref).
190+
- The node in the field `:from` has output and the node in the field `:to` has input.
191+
- The [`inputs`](@ref) of the link are included in the [`outputs`](@ref) of the `:from`
192+
node and the [`outputs`](@ref) of the link are included in the [`inputs`](@ref) of the
193+
`:to` node.
189194
"""
190195
function check_elements(
191196
log_by_element,
@@ -237,19 +242,31 @@ function check_elements(
237242
"The node in the field `:from` is not included in the Node vector. As a consequence," *
238243
"the link would not be utilized in the model."
239244
)
245+
@assert_or_log(
246+
has_output(l.from),
247+
"The node in the field `:from` does not allow for outputs."
248+
)
249+
@assert_or_log(
250+
all(p_in outputs(l.from) for p_in inputs(l)),
251+
"Not all resources specifed as `inputs` of the link are specified as `outputs` " *
252+
"of the node in the field `:from`. As a consequence, the link could potentially " *
253+
"be not utilized in the model."
254+
)
240255
@assert_or_log(
241256
l.to 𝒩,
242257
"The node in the field `:to` is not included in the Node vector. As a consequence," *
243258
"the link would not be utilized in the model."
244259
)
245-
@assert_or_log(
246-
has_output(l.from),
247-
"The node in the field `:from` does not allow for outputs."
248-
)
249260
@assert_or_log(
250261
has_input(l.to),
251262
"The node in the field `:to` does not allow for inputs."
252263
)
264+
@assert_or_log(
265+
all(p_out inputs(l.to) for p_out outputs(l)),
266+
"Not all resources specifed as `outputs` of the link are specified as `inputs` " *
267+
"of the node in the field `:to`. As a consequence, the link could potentially " *
268+
"not be utilized in the model."
269+
)
253270

254271
# Check the links, the link data, and the time structure
255272
check_link(l, 𝒯, modeltype, check_timeprofiles)
@@ -985,17 +1002,29 @@ function check_node_data(
9851002
end
9861003

9871004
"""
988-
check_link(n::Link, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool)
1005+
check_link(l::Link, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool)
1006+
check_link(l::Direct, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool)
9891007
9901008
Check that the fields of a [`Link`](@ref) corresponds to required structure. The default
9911009
functionality does not check anthing, aside from the checks performed in [`check_elements`](@ref).
9921010
1011+
## Checks `Direct`
1012+
- The functions [`inputs`](@ref) and [`outputs`](@ref) must be non-empty.
1013+
9931014
!!! tip "Creating a new link type"
9941015
When developing a new link with new checks, it is important to create a new method for
9951016
`check_link`.
9961017
"""
997-
check_link(n::Link, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool) = nothing
1018+
check_link(l::Link, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool) = nothing
1019+
function check_link(l::Direct, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool)
9981020

1021+
@assert_or_log(
1022+
!isempty(link_res(l)),
1023+
"The functions `inputs` and `outputs` return an empty `Vector`. This implies that " *
1024+
"the nodes in the fields `:from` and `:to` do not have common `Resources` as " *
1025+
"`outputs` and `inputs`, respectively. Hence, the link will not be used."
1026+
)
1027+
end
9991028
"""
10001029
check_link_data(l::Link, data::ExtensionData, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool)
10011030

src/structures/link.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ Return the resources transported for a given link `l`.
102102
103103
The default approach is to use the intersection of the inputs of the `to` node and the
104104
outputs of the `from` node.
105+
106+
!!! danger
107+
This function is only internal and should not be used in other packages. Its behaviour
108+
may not be the expected when used outside `EnergyModelsBase` for new [`Link`](@ref) types/
105109
"""
106110
link_res(l::Link) = intersect(inputs(l.to), outputs(l.from))
107111

@@ -111,6 +115,12 @@ link_res(l::Link) = intersect(inputs(l.to), outputs(l.from))
111115
Returns the input resources of a link `l`.
112116
113117
The default approach is to use the function [`link_res(l::Link)`](@ref).
118+
119+
!!! note "New Links"
120+
This function should receive a new method when you define a new [`Link`](@ref) type in
121+
which you specify the transported resources.
122+
123+
The new method *must* return a `Vector{<:Resource}`
114124
"""
115125
inputs(l::Link) = link_res(l)
116126

@@ -120,6 +130,12 @@ inputs(l::Link) = link_res(l)
120130
Returns the output resources of a link `l`.
121131
122132
The default approach is to use the function [`link_res(l::Link)`](@ref).
133+
134+
!!! note "New Links"
135+
This function should receive a new method when you define a new [`Link`](@ref) type in
136+
which you specify the transported resources.
137+
138+
The new method *must* return a `Vector{<:Resource}`
123139
"""
124140
outputs(l::Link) = link_res(l)
125141

test/test_checks.jl

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -702,8 +702,19 @@ end
702702
Power = ResourceCarrier("Power", 0.0)
703703
CO2 = ResourceEmit("CO2", 1.0)
704704

705+
# Auxiliary link used in the tests
706+
struct CheckLink <: Link
707+
id::Any
708+
from::EMB.Node
709+
to::EMB.Node
710+
p::Resource
711+
end
712+
EMB.formulation(l::CheckLink) = Linear()
713+
EMB.inputs(l::CheckLink) = Resource[l.p]
714+
EMB.outputs(l::CheckLink) = Resource[l.p]
715+
705716
# Function for setting up the system for testing `Sink` and `Source`
706-
function simple_graph()
717+
function check_links(; res_src=Power, res_snk=Power)
707718
resources = [Power, CO2]
708719
ops = SimpleTimes(5, 2)
709720
T = TwoLevel(2, 2, ops; op_per_strat = 10)
@@ -713,7 +724,7 @@ end
713724
"sink",
714725
OperationalProfile([6, 8, 10, 6, 8]),
715726
Dict(:surplus => FixedProfile(4), :deficit => FixedProfile(10)),
716-
Dict(Power => 1),
727+
Dict(res_src => 1),
717728
)
718729

719730
# Test that a wrong capacity is caught by the checks.
@@ -722,7 +733,7 @@ end
722733
FixedProfile(4),
723734
FixedProfile(10),
724735
FixedProfile(0),
725-
Dict(Power => 1),
736+
Dict(res_snk => 1),
726737
)
727738
nodes = [av, source, sink]
728739
links = [Direct(12, source, sink)]
@@ -737,7 +748,7 @@ end
737748

738749
# Test that the from and to fields are correctly checked
739750
# - check_elements(log_by_element, ℒ::Vector{<:Link}}, 𝒳ᵛᵉᶜ, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool)
740-
case, model = simple_graph()
751+
case, model = check_links()
741752
av, source, sink = get_nodes(case)
742753
case.elements[2] = [Direct(12, GenAvailability("test", get_products(case)), sink)]
743754
@test_throws AssertionError create_model(case, model)
@@ -747,6 +758,18 @@ end
747758
@test_throws AssertionError create_model(case, model)
748759
case.elements[2] = [Direct(12, sink, av)]
749760
@test_throws AssertionError create_model(case, model)
761+
762+
case, model = check_links(; res_snk = CO2)
763+
@test_throws AssertionError create_model(case, model)
764+
av, source, sink = get_nodes(case)
765+
case.elements[2] = [CheckLink(12, sink, av, Power)]
766+
@test_throws AssertionError create_model(case, model)
767+
768+
case, model = check_links(; res_src = CO2)
769+
av, source, sink = get_nodes(case)
770+
case.elements[2] = [CheckLink(12, sink, av, Power)]
771+
@test_throws AssertionError create_model(case, model)
772+
750773
end
751774

752775
# Set the global again to false

test/test_links.jl

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
links = [
4242
Direct(12, source, network)
4343
Direct(23, network, sink)
44-
Direct(23, source, sink)
4544
]
4645
model = OperationalModel(
4746
Dict(CO2 => FixedProfile(100), NG => FixedProfile(100)),
@@ -75,11 +74,6 @@
7574
@test inputs(ℒ[1]) == outputs(𝒩[1])
7675
@test outputs(ℒ[1]) == inputs(𝒩[2])
7776

78-
# Test that the function `link_res` does not return a transported resources for the
79-
# 3ʳᵈ link
80-
@test isempty(EMB.link_res(ℒ[3]))
81-
@test isempty(EMB.link_res(ℒ[3]))
82-
8377
# Test that the constructor for a direct link is working and that the function
8478
# formulation is working
8579
@test isa(formulation(ℒ[1]), Linear)
@@ -102,7 +96,7 @@
10296
)
10397

10498
# Test that `emissions_link`, `link_opex_var`, `link_opex_fixed`, and `link_cap_inst`
105-
#are empty
99+
# are empty
106100
@test isempty(m[:emissions_link])
107101
@test isempty(m[:link_opex_var])
108102
@test isempty(m[:link_opex_fixed])

test/test_nodes.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ end
444444
Dict(CO2 => 1, Power => 0.02),
445445
)
446446
push!(nodes, CO2_stor)
447-
append!(links, [Direct(14, source, CO2_stor), Direct(24, network, CO2_stor)])
447+
append!(links, [Direct(24, network, CO2_stor)])
448448
end
449449

450450
model = OperationalModel(

0 commit comments

Comments
 (0)