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
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unversioned

* Added the function `resource_family(p::Resource)` for grouping resource types in the segmentation (`res_types`, `res_types_vec`).
* Added the function `check_resources(case, 𝒫ˢᵘᵇ, modeltype, check_timeprofiles)` for every resource family of the case products.

## Version 0.10.8 (2026-09-30)

Expand Down
25 changes: 24 additions & 1 deletion docs/src/how-to/extend-resource-functionality.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,27 @@ 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.
Resources of other packages are not affected, as the default returns the concrete type.

## [Checking resources](@id how_to-res_funct-checks)

The parameters of a new resource type and its combination with the elements carrying it can be checked through a method of [`check_resources`](@ref).
It is called from `check_case_data` for every resource family of the case products with the `case` as argument, so that the nodes and links can be inspected as well.

```julia
function EMB.check_resources(
case,
𝒫ˢᵘᵇ::Vector{<:PotentialPower},
modeltype::EnergyModel,
check_timeprofiles::Bool,
)
for p ∈ 𝒫ˢᵘᵇ
@assert_or_log(
lower_limit(p) < upper_limit(p),
"The lower limit of the potential of `$(p)` must be below its upper limit."
)
end
end
```

Checks that depend only on the type of a node or link, *e.g.*, the fields of a custom node, belong in [`check_node`](@ref) or [`check_link`](@ref) of the package introducing the element type.
1 change: 1 addition & 0 deletions docs/src/library/internals/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ variables_element_ext_data
```@docs
check_data
check_case_data
check_resources
check_model
check_elements
check_node
Expand Down
30 changes: 27 additions & 3 deletions src/checks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function check_data(case, modeltype::EnergyModel, check_timeprofiles::Bool)

# Check the case data. If the case data is not in the correct format, the overall check
# is cancelled as extractions would not be possible
check_case_data(case)
check_case_data(case, modeltype, check_timeprofiles)
log_by_element["Case data"] = logs
if ASSERTS_AS_LOG
compile_logs(case, log_by_element)
Expand Down Expand Up @@ -121,16 +121,18 @@ function compile_logs(case, log_by_element)
end

"""
check_case_data(case)
check_case_data(case, modeltype::EnergyModel, check_timeprofiles::Bool)

Checks the `case` dictionary is in the correct format.

## Checks
- The individual elements vector must be unique, that it is not possible to have two vector
of nodes within the elements vector.
- Check that the coupling functions do return elements and not only an empty vector
- Call of [`check_resources`](@ref) for every resource type segment
([`res_types_vec`](@ref)) of the products of the case.
"""
function check_case_data(case)
function check_case_data(case, modeltype::EnergyModel, check_timeprofiles::Bool)
𝒳ᵛᵉᶜ = get_elements_vec(case)
get_vect_type(vec::Vector{T}) where {T} = T
vec_types = [get_vect_type(x) for x ∈ 𝒳ᵛᵉᶜ]
Expand All @@ -156,6 +158,28 @@ function check_case_data(case)
end
end

# Check the resources of the case per resource family
for 𝒫ˢᵘᵇ ∈ res_types_vec(get_products(case))
check_resources(case, 𝒫ˢᵘᵇ, modeltype, check_timeprofiles)

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.

The question is whether that should be invoked directly here or could be a different PR. Given the description, it would not be necessary to have it here while I agree that Issue #107 states that it should be included. In this case, however, the question is where it is based located. As this is still up to debate, I would leave it at the moment be and ignore this comment.

end
Comment on lines +162 to +164

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.

See previous answer.

end

"""
check_resources(case, 𝒫ˢᵘᵇ::Vector{<:Resource}, modeltype::EnergyModel, check_timeprofiles::Bool)

Check the resources `𝒫ˢᵘᵇ` of a single resource type segment ([`res_types_vec`](@ref)) of
the `case`.

The default method does not check anything; extension packages that introduce resource types
provide methods for their resource types.
"""
function check_resources(
case,
𝒫ˢᵘᵇ::Vector{<:Resource},
modeltype::EnergyModel,
check_timeprofiles::Bool,
)
return nothing
end

"""
Expand Down
67 changes: 67 additions & 0 deletions test/test_checks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -947,4 +947,71 @@ end
end

# Set the global again to false
@testset "Checks - resources" begin
# A resource with a parameter that has to be positive and that may be carried by at most
# one node of the case
struct CheckedResource <: Resource
id::String
co2_int::Float64
parameter::Float64
end
function EMB.check_resources(
case,
𝒫ˢᵘᵇ::Vector{<:CheckedResource},
modeltype::EnergyModel,
check_timeprofiles::Bool,
)
for p ∈ 𝒫ˢᵘᵇ
@assert_or_log(
p.parameter > 0,
"The parameter of the resource `$(p)` must be positive."
)
𝒩ᵖ = filter(n -> p ∈ vcat(inputs(n), outputs(n)), get_nodes(case))
@assert_or_log(
length(𝒩ᵖ) ≤ 1,
"The resource `$(p)` can only be carried by a single node."
)
end
end

# Function for setting up the system
function check_graph_res(p_checked)
CO2 = ResourceEmit("CO2", 1.0)
resources = [p_checked, CO2]
T = TwoLevel(2, 2, SimpleTimes(5, 2); op_per_strat = 10)

source = RefSource(
"source",
FixedProfile(4),
FixedProfile(0),
FixedProfile(10),
Dict(p_checked => 1),
)
sink = RefSink(
"sink",
FixedProfile(3),
Dict(:surplus => FixedProfile(-4), :deficit => FixedProfile(4)),
Dict(p_checked => 1),
)
nodes = [source, sink]
links = [Direct(12, source, sink)]
case = Case(T, resources, [nodes, links], [[get_nodes, get_links]])
model = OperationalModel(
Dict(CO2 => FixedProfile(100)),
Dict(CO2 => FixedProfile(0)),
CO2,
)
return case, model
end

# Both checks of the resource method fail in the standard graph (the resource is
# carried by two nodes, and the parameter is not positive)
case, model = check_graph_res(CheckedResource("checked", 0.0, -1.0))
@test_throws AssertionError create_model(case, model)

# A positive parameter leaves the node check, which still fails
case, model = check_graph_res(CheckedResource("checked", 0.0, 1.0))
@test_throws AssertionError create_model(case, model)

end
EMB.TEST_ENV = false
Loading