Skip to content
Open
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
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
23 changes: 23 additions & 0 deletions docs/src/how-to/extend-resource-functionality.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
14 changes: 14 additions & 0 deletions docs/src/library/public/resources.md

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put it under the header Extension functions in the internal library. My plan is to clean up a bit in the internal library and that is something users should extend. You can also remove the text around it as it is explained in the documentation.

Or do you foresee that this function should be used directly in other packages? I honestly do not see the requirement yet.

Original file line number Diff line number Diff line change
Expand Up @@ -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
```
22 changes: 18 additions & 4 deletions src/structures/resource.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Comment thread
espenbodal marked this conversation as resolved.
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(𝒫)]
res_types_vec(𝒫::Vector{<:Resource}) =
[Vector{rt}(filter(x -> resource_family(x) == rt, 𝒫)) for rt ∈ res_types(𝒫)]
76 changes: 76 additions & 0 deletions test/test_resource.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading