diff --git a/NEWS.md b/NEWS.md index a82f399..9dd53b3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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. diff --git a/Project.toml b/Project.toml index 30fc07d..a636660 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "EnergyModelsBase" uuid = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" authors = ["Lars Hellemo , Julian Straus "] -version = "0.9.3" +version = "0.9.4" [deps] JuMP = "4076af6c-e467-56ae-b986-b466b2749572" diff --git a/src/checks.jl b/src/checks.jl index e3f050a..0b16967 100644 --- a/src/checks.jl +++ b/src/checks.jl @@ -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, @@ -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) @@ -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) diff --git a/src/structures/link.jl b/src/structures/link.jl index 41ecf69..f6dc64e 100644 --- a/src/structures/link.jl +++ b/src/structures/link.jl @@ -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)) @@ -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) @@ -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) diff --git a/test/test_checks.jl b/test/test_checks.jl index b2c03a2..0104f57 100644 --- a/test/test_checks.jl +++ b/test/test_checks.jl @@ -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) @@ -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. @@ -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)] @@ -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) @@ -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 diff --git a/test/test_links.jl b/test/test_links.jl index 4d1b5eb..40f3cac 100644 --- a/test/test_links.jl +++ b/test/test_links.jl @@ -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)), @@ -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) @@ -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]) diff --git a/test/test_nodes.jl b/test/test_nodes.jl index 5f0b23d..3de7d85 100644 --- a/test/test_nodes.jl +++ b/test/test_nodes.jl @@ -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(