diff --git a/NEWS.md b/NEWS.md index 736860a..c1f3962 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # Release notes +## Unversioned + +* Added the function `resource_family(p::Resource)` for grouping resource types in the segmentation (`res_types`, `res_types_vec`). + ## Version 0.10.8 (2026-09-30) * Updated the variable page description to improve clarity regarding inclusion of scaling. diff --git a/docs/src/how-to/extend-resource-functionality.md b/docs/src/how-to/extend-resource-functionality.md index 9c341fd..fc1f57c 100644 --- a/docs/src/how-to/extend-resource-functionality.md +++ b/docs/src/how-to/extend-resource-functionality.md @@ -205,3 +205,26 @@ function EMB.constraints_couple_resource( end end ``` + +## [Grouping a family of resource types](@id how_to-res_funct-family) + +The resource-specific functions are called once for every segment of `res_types_vec(๐’ซ)`, which groups the resources of the case by their concrete type by default. +Variables created in [`variables_flow_resource`](@ref) are registered once per model, so a package introducing several concrete types with shared variables, *e.g.*, different formulations of the same physical resource, would only be able to use one of them in a case. + +The function [`resource_family`](@ref) allows you to group the concrete types of your package into a single segment: + +```julia +abstract type AbstractPotentialPower <: Resource end +struct PotentialPower <: AbstractPotentialPower + # fields as above +end +struct BoundedPotentialPower <: AbstractPotentialPower + # fields with different bounds +end + +EMB.resource_family(::AbstractPotentialPower) = AbstractPotentialPower +``` + +The resource-specific functions then receive a `Vector{<:AbstractPotentialPower}` containing all instances of both types in a single call. +Create the variables once for the whole family in this call and dispatch on the concrete types inside the constraint functions, if required. +Resources of other packages are not affected, as the default returns the concrete type. \ No newline at end of file diff --git a/docs/src/library/public/resources.md b/docs/src/library/public/resources.md index 3e5c718..8df52dc 100644 --- a/docs/src/library/public/resources.md +++ b/docs/src/library/public/resources.md @@ -34,3 +34,17 @@ If you want to introduce new `Resource` types, it is important that this functio ```@docs co2_int ``` + +```@meta +CurrentModule = EnergyModelsBase +``` + +## [Functions for grouping `Resource` types](@id lib-pub-res-fun_family) + +The resources of a case are segmented by type before the resource-specific functions ([`variables_flow_resource`](@ref), [`constraints_resource`](@ref), and [`constraints_couple_resource`](@ref)) are called. +The following function controls the segmentation. +It is only relevant if you introduce a family of resource types that should be handled together, see the page *[Extend resource functionality](@ref how_to-res_funct)*. + +```@docs +EnergyModelsBase.resource_family +``` \ No newline at end of file diff --git a/src/structures/resource.jl b/src/structures/resource.jl index b60fa43..3ca12b1 100644 --- a/src/structures/resource.jl +++ b/src/structures/resource.jl @@ -87,16 +87,30 @@ Returns all emission resources for a res_em(๐’ซ::Array{<:Resource}) = filter(is_resource_emit, ๐’ซ) res_em(๐’ซ::Dict) = filter(p -> is_resource_emit(first(p)), ๐’ซ) +""" + resource_family(p::Resource) + +Returns the family under which the resource `p` is grouped. The default is the concrete type +of `p`, so that each concrete resource type forms its own segment. + +Extension packages can introduce new methods to group multiple resources within one family. +""" +resource_family(p::Resource) = typeof(p) + """ res_types(๐’ซ::Vector{<:Resource}) -Return the unique resource types in an Vector of resources `๐’ซ`. +Return the unique resource families ([`resource_family`](@ref)) in a Vector of resources +`๐’ซ`. By default, these are the concrete resource types. """ -res_types(๐’ซ::Vector{<:Resource}) = unique(map(x -> typeof(x), ๐’ซ)) +res_types(๐’ซ::Vector{<:Resource}) = unique(map(resource_family, ๐’ซ)) """ res_types_vec(๐’ซ::Vector{<:Resource}) -Return a Vector-of-Vectors of resources by the concrete sub-types, if the input is empty it returns an empty Vector. +Return a Vector-of-Vectors of resources segmented by their resource family +([`resource_family`](@ref)), if the input is empty it returns an empty Vector. By default, +the segments correspond to the concrete sub-types. """ -res_types_vec(๐’ซ::Vector{<:Resource}) = [Vector{rt}(filter(x -> isa(x, rt), ๐’ซ)) for rt in res_types(๐’ซ)] \ No newline at end of file +res_types_vec(๐’ซ::Vector{<:Resource}) = + [Vector{rt}(filter(x -> resource_family(x) == rt, ๐’ซ)) for rt โˆˆ res_types(๐’ซ)] \ No newline at end of file diff --git a/test/test_resource.jl b/test/test_resource.jl index 25a1bf1..8574a77 100644 --- a/test/test_resource.jl +++ b/test/test_resource.jl @@ -268,3 +268,79 @@ end value(m[:flow_in][sink, t, pp]) for t โˆˆ ๐’ฏ) end + +# Group several resource types into a family and check that the family is handed to the +# resource-specific functions in a single call +@testset "Resource - families" begin + abstract type TestFamily <: Resource end + struct FamilyMemberA <: TestFamily + id::String + co2_int::Float64 + end + struct FamilyMemberB <: TestFamily + id::String + co2_int::Float64 + end + EMB.resource_family(::TestFamily) = TestFamily + + fam_a = FamilyMemberA("A", 0.0) + fam_b = FamilyMemberB("B", 0.0) + Power = ResourceCarrier("Power", 0.0) + CO2 = ResourceEmit("CO2", 1.0) + ๐’ซ = [fam_a, Power, fam_b, CO2] + + @testset "Segmentation" begin + # The two family members form one segment, the other resources keep their own + @test length(EMB.res_types(๐’ซ)) == 3 + @test TestFamily โˆˆ EMB.res_types(๐’ซ) + @test FamilyMemberA โˆ‰ EMB.res_types(๐’ซ) + + ๐’ซแต›แต‰แถœ = EMB.res_types_vec(๐’ซ) + @test length(๐’ซแต›แต‰แถœ) == 3 + ๐’ซแถ แตƒแต = only(filter(๐’ซหขแต˜แต‡ -> eltype(๐’ซหขแต˜แต‡) == TestFamily, ๐’ซแต›แต‰แถœ)) + @test ๐’ซแถ แตƒแต isa Vector{TestFamily} + @test Set(๐’ซแถ แตƒแต) == Set([fam_a, fam_b]) + + # Resources without a `resource_family` method are segmented as before + @test only(filter(๐’ซหขแต˜แต‡ -> eltype(๐’ซหขแต˜แต‡) == ResourceEmit{Float64}, ๐’ซแต›แต‰แถœ)) == [CO2] + end + + @testset "Single call per family" begin + # A variable created once for the whole family; a second call for the same name + # would error, so a successful model build proves that the family is one segment + function EMB.variables_flow_resource( + m, ๐’ฉ::Vector{<:EMB.Node}, ๐’ซ::Vector{<:TestFamily}, ๐’ฏ, modeltype::EnergyModel + ) + ๐’ฉแถ แตƒแต = filter(n -> any(p โˆˆ ๐’ซ for p โˆˆ vcat(inputs(n), outputs(n))), ๐’ฉ) + @variable(m, family_flow[๐’ฉแถ แตƒแต, ๐’ฏ, ๐’ซ]) + end + + source = RefSource( + "fam_source", + FixedProfile(4), + FixedProfile(10), + FixedProfile(0), + Dict(fam_a => 1, fam_b => 1), + ) + sink = RefSink( + "fam_sink", + FixedProfile(3), + Dict(:surplus => FixedProfile(4), :deficit => FixedProfile(100)), + Dict(fam_a => 1, fam_b => 1), + ) + ๐’ฏ = TwoLevel(2, 2, SimpleTimes(5, 2); op_per_strat = 10) + ๐’ฉ = [source, sink] + โ„’ = [Direct("src-snk", source, sink, Linear())] + modeltype = OperationalModel( + Dict(CO2 => FixedProfile(100)), + Dict(CO2 => FixedProfile(0)), + CO2, + ) + case = Case(๐’ฏ, [fam_a, fam_b, CO2], [๐’ฉ, โ„’]) + m = create_model(case, modeltype) + + @test haskey(m, :family_flow) + @test length(m[:family_flow]) == length(๐’ฉ) * length(๐’ฏ) * 2 + @test Set(axes(m[:family_flow])[3]) == Set([fam_a, fam_b]) + end +end \ No newline at end of file