Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
# Release notes

## Unversioned
## Version 0.9.4 (2025-11-26)

### Bugfixes

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

### Minor updates

* Added checks
* that the `inputs` and `outputs` resources of a `Link` are present in the connected nodes.
* that a `Direct` link has actually `inputs` and `outputs`,

## Version 0.9.3 (2025-10-23)

## Bugfixes
### Bugfixes

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

Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "EnergyModelsBase"
uuid = "5d7e687e-f956-46f3-9045-6f5a5fd49f50"
authors = ["Lars Hellemo <Lars.Hellemo@sintef.no>, Julian Straus <Julian.Straus@sintef.no>"]
version = "0.9.3"
version = "0.9.4"

[deps]
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
Expand Down
47 changes: 38 additions & 9 deletions src/checks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,14 @@ and Vector{<:Link}.
- [`check_time_structure`](@ref) to identify time profiles at the highest level that
are not equivalent to the provided timestructure.

In addition, all links are directly checked to have in the fields `:from` and `:to` nodes
that are present in the Node vector as extracted through the function [`get_nodes`](@ref)
and that these nodes have input (`:to`) or output (`:from`).
In addition, all links are directly checked:

- The nodes in the fields `:from` and `:to` are present in the Node vector as extracted
through the function [`get_nodes`](@ref).
- The node in the field `:from` has output and the node in the field `:to` has input.
- The [`inputs`](@ref) of the link are included in the [`outputs`](@ref) of the `:from`
node and the [`outputs`](@ref) of the link are included in the [`inputs`](@ref) of the
`:to` node.
"""
function check_elements(
log_by_element,
Expand Down Expand Up @@ -237,19 +242,31 @@ function check_elements(
"The node in the field `:from` is not included in the Node vector. As a consequence," *
"the link would not be utilized in the model."
)
@assert_or_log(
has_output(l.from),
"The node in the field `:from` does not allow for outputs."
)
@assert_or_log(
all(p_in ∈ outputs(l.from) for p_in ∈ inputs(l)),
"Not all resources specifed as `inputs` of the link are specified as `outputs` " *
"of the node in the field `:from`. As a consequence, the link could potentially " *
"be not utilized in the model."
)
@assert_or_log(
l.to ∈ 𝒩,
"The node in the field `:to` is not included in the Node vector. As a consequence," *
"the link would not be utilized in the model."
)
@assert_or_log(
has_output(l.from),
"The node in the field `:from` does not allow for outputs."
)
@assert_or_log(
has_input(l.to),
"The node in the field `:to` does not allow for inputs."
)
@assert_or_log(
all(p_out ∈ inputs(l.to) for p_out ∈ outputs(l)),
"Not all resources specifed as `outputs` of the link are specified as `inputs` " *
"of the node in the field `:to`. As a consequence, the link could potentially " *
"not be utilized in the model."
)

# Check the links, the link data, and the time structure
check_link(l, 𝒯, modeltype, check_timeprofiles)
Expand Down Expand Up @@ -985,17 +1002,29 @@ function check_node_data(
end

"""
check_link(n::Link, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool)
check_link(l::Link, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool)
check_link(l::Direct, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool)

Check that the fields of a [`Link`](@ref) corresponds to required structure. The default
functionality does not check anthing, aside from the checks performed in [`check_elements`](@ref).

## Checks `Direct`
- The functions [`inputs`](@ref) and [`outputs`](@ref) must be non-empty.

!!! tip "Creating a new link type"
When developing a new link with new checks, it is important to create a new method for
`check_link`.
"""
check_link(n::Link, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool) = nothing
check_link(l::Link, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool) = nothing
function check_link(l::Direct, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool)

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

Expand Down
16 changes: 16 additions & 0 deletions src/structures/link.jl
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ Return the resources transported for a given link `l`.

The default approach is to use the intersection of the inputs of the `to` node and the
outputs of the `from` node.

!!! danger
This function is only internal and should not be used in other packages. Its behaviour
may not be the expected when used outside `EnergyModelsBase` for new [`Link`](@ref) types/
"""
link_res(l::Link) = intersect(inputs(l.to), outputs(l.from))

Expand All @@ -111,6 +115,12 @@ link_res(l::Link) = intersect(inputs(l.to), outputs(l.from))
Returns the input resources of a link `l`.

The default approach is to use the function [`link_res(l::Link)`](@ref).

!!! note "New Links"
This function should receive a new method when you define a new [`Link`](@ref) type in
which you specify the transported resources.

The new method *must* return a `Vector{<:Resource}`
"""
inputs(l::Link) = link_res(l)

Expand All @@ -120,6 +130,12 @@ inputs(l::Link) = link_res(l)
Returns the output resources of a link `l`.

The default approach is to use the function [`link_res(l::Link)`](@ref).

!!! note "New Links"
This function should receive a new method when you define a new [`Link`](@ref) type in
which you specify the transported resources.

The new method *must* return a `Vector{<:Resource}`
"""
outputs(l::Link) = link_res(l)

Expand Down
31 changes: 27 additions & 4 deletions test/test_checks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -702,8 +702,19 @@ end
Power = ResourceCarrier("Power", 0.0)
CO2 = ResourceEmit("CO2", 1.0)

# Auxiliary link used in the tests
struct CheckLink <: Link
id::Any
from::EMB.Node
to::EMB.Node
p::Resource
end
EMB.formulation(l::CheckLink) = Linear()
EMB.inputs(l::CheckLink) = Resource[l.p]
EMB.outputs(l::CheckLink) = Resource[l.p]

# Function for setting up the system for testing `Sink` and `Source`
function simple_graph()
function check_links(; res_src=Power, res_snk=Power)
resources = [Power, CO2]
ops = SimpleTimes(5, 2)
T = TwoLevel(2, 2, ops; op_per_strat = 10)
Expand All @@ -713,7 +724,7 @@ end
"sink",
OperationalProfile([6, 8, 10, 6, 8]),
Dict(:surplus => FixedProfile(4), :deficit => FixedProfile(10)),
Dict(Power => 1),
Dict(res_src => 1),
)

# Test that a wrong capacity is caught by the checks.
Expand All @@ -722,7 +733,7 @@ end
FixedProfile(4),
FixedProfile(10),
FixedProfile(0),
Dict(Power => 1),
Dict(res_snk => 1),
)
nodes = [av, source, sink]
links = [Direct(12, source, sink)]
Expand All @@ -737,7 +748,7 @@ end

# Test that the from and to fields are correctly checked
# - check_elements(log_by_element, ℒ::Vector{<:Link}}, 𝒳ᵛᵉᶜ, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool)
case, model = simple_graph()
case, model = check_links()
av, source, sink = get_nodes(case)
case.elements[2] = [Direct(12, GenAvailability("test", get_products(case)), sink)]
@test_throws AssertionError create_model(case, model)
Expand All @@ -747,6 +758,18 @@ end
@test_throws AssertionError create_model(case, model)
case.elements[2] = [Direct(12, sink, av)]
@test_throws AssertionError create_model(case, model)

case, model = check_links(; res_snk = CO2)
@test_throws AssertionError create_model(case, model)
av, source, sink = get_nodes(case)
case.elements[2] = [CheckLink(12, sink, av, Power)]
@test_throws AssertionError create_model(case, model)

case, model = check_links(; res_src = CO2)
av, source, sink = get_nodes(case)
case.elements[2] = [CheckLink(12, sink, av, Power)]
@test_throws AssertionError create_model(case, model)

end

# Set the global again to false
Expand Down
8 changes: 1 addition & 7 deletions test/test_links.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
links = [
Direct(12, source, network)
Direct(23, network, sink)
Direct(23, source, sink)
]
model = OperationalModel(
Dict(CO2 => FixedProfile(100), NG => FixedProfile(100)),
Expand Down Expand Up @@ -75,11 +74,6 @@
@test inputs(ℒ[1]) == outputs(𝒩[1])
@test outputs(ℒ[1]) == inputs(𝒩[2])

# Test that the function `link_res` does not return a transported resources for the
# 3ʳᵈ link
@test isempty(EMB.link_res(ℒ[3]))
@test isempty(EMB.link_res(ℒ[3]))

# Test that the constructor for a direct link is working and that the function
# formulation is working
@test isa(formulation(ℒ[1]), Linear)
Expand All @@ -102,7 +96,7 @@
)

# Test that `emissions_link`, `link_opex_var`, `link_opex_fixed`, and `link_cap_inst`
#are empty
# are empty
@test isempty(m[:emissions_link])
@test isempty(m[:link_opex_var])
@test isempty(m[:link_opex_fixed])
Expand Down
2 changes: 1 addition & 1 deletion test/test_nodes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ end
Dict(CO2 => 1, Power => 0.02),
)
push!(nodes, CO2_stor)
append!(links, [Direct(14, source, CO2_stor), Direct(24, network, CO2_stor)])
append!(links, [Direct(24, network, CO2_stor)])
end

model = OperationalModel(
Expand Down