Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2f62de0
Introduced new type of resource that allow for energy potential transfer
espenbodal Mar 14, 2025
8524f62
Added functions for new flow variables and constraints
espenbodal Mar 19, 2025
2d76031
Removed added resource type function on node-link coupling
espenbodal Mar 19, 2025
01cdf0c
Small changes to comments and documentation
espenbodal Mar 19, 2025
7a82100
Changed to a design that mostly dispatches original functions
espenbodal Mar 20, 2025
08b8f5d
Resource segmentation moved down to constraint level
espenbodal Mar 21, 2025
ad926cd
Reorganize for minimal changes to existing code
espenbodal Jul 9, 2025
5540458
Moved the location of resource constraints
espenbodal Sep 30, 2025
49c7fa5
Small notation change for resource subset
espenbodal Oct 23, 2025
f494f9c
Merge branch 'main' of https://github.com/EnergyModelsX/EnergyModelsB…
espenbodal Nov 18, 2025
9bff364
Updated and added tests for new resource functions
espenbodal Mar 23, 2026
b77124e
Updated Project.toml for tests
espenbodal Mar 23, 2026
81fc9b0
Merge branch 'main' of https://github.com/EnergyModelsX/EnergyModelsB…
espenbodal Mar 23, 2026
11263a2
Updated version number and News.md
espenbodal Mar 23, 2026
0692c5c
Revert some unnecessary changes and fix doc output
espenbodal Mar 23, 2026
9f58fc9
Add new functions to docs and reset inline comments
espenbodal Mar 23, 2026
4cffb4b
Updated docs-strings
espenbodal Mar 23, 2026
2de6e3d
add resource-type dispatch support and functional resource tests
espenbodal Mar 24, 2026
e71bbde
Updated create_elements docstring
espenbodal Mar 24, 2026
d659861
Review updates
JulStraus Mar 25, 2026
9113b56
Minor fix
JulStraus Mar 25, 2026
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
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Release notes

## Unversioned

* New functions (`variables_flow_resource()`, `constraints_resource()`, `constraints_couple_resource()`) that dispatch on resource types, which allow for creation of new resource-specific variables and constraints in extension packages.
* New function to indentify the unique resource types of a vector of resources
* New function that segments the vector of resources into sub-vectors based on each resource type

## Version 0.9.4 (2025-11-26)

### Bugfixes
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.4"
version = "0.9.5"

[deps]
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
Expand Down
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ makedocs(
"How to" => Any[
"Create a new element"=>"how-to/create_new_element.md",
"Create a new node"=>"how-to/create-new-node.md",
"Extend resource functionality"=>"how-to/extend-resource-functionality.md",
"Utilize TimeStruct"=>"how-to/utilize-timestruct.md",
"Update models"=>"how-to/update-models.md",
"Contribute to EnergyModelsBase"=>"how-to/contribute.md",
Expand Down
207 changes: 207 additions & 0 deletions docs/src/how-to/extend-resource-functionality.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# [Extend resource functionality](@id how_to-res_funct)

```@meta
CurrentModule = EMB
```

## [Concept](@id how_to-res_funct-concept)

This guide shows how to extend resource functionality by adding a custom resource type and connecting it to custom variables and constraints through resource-dispatch functions.
This is useful for modelling more complex resource behavior that cannot be captured by the default resource types where the standard behavior is built around energy or mass flow.

The pattern follows the same structure as the resource dispatch test in `test/test_resource.jl`:

1. Define a resource subtype with extra parameters.
2. (Optionally) create a custom node subtype that uses the resource.
3. Add resource-specific variables with [`variables_flow_resource`](@ref).
4. Add resource-specific constraints with [`constraints_resource`](@ref).
5. Couple node and link resource variables with [`constraints_couple_resource`](@ref).

## [Example](@id how_to-res_funct-example)

The following example illustrates the different steps that are required for creating a new resource with additional properties.
It defines a `PotentialPower` resource which has as property a potential with upper and lower bounds in addition to its energy flow.
The flow of this potential in and out of junctions follows equality constraints, as opposed to the energy and mass flow which follow sum constraints.

The notation below follows the same conventions as the implementation and tests:

- `𝒩` for nodes,
- `ℒ` for links,
- `𝒫` for resources,
- `𝒯` for the time structure,
- `ℒᶠʳᵒᵐ`, `ℒᵗᵒ` for outgoing and incoming links of a node, and
- `𝒫ᵒᵘᵗ`, `𝒫ⁱⁿ`, `𝒫ˡⁱⁿᵏ` for resource subsets on outputs, inputs, and links.

### 1. Define a special resource

Create a subtype of [`Resource`](@ref) and keep `co2_int` as the second field for consistency with existing resource structures.
Alternatively, you can create a new method for the internal function [`co2_int`](@ref).

```julia
struct PotentialPower <: Resource
id::String
co2_int::Float64
potential_lower::Float64
potential_upper::Float64
end

EMB.is_resource_emit(::PotentialPower) = false
lower_limit(p::PotentialPower) = p.potential_lower
upper_limit(p::PotentialPower) = p.potential_upper
```

### 2. Define a custom node (optional)

If your resource needs dedicated node behavior, create a custom node subtype.
If the node subtype is parametrized, it can handle different types of resources in different ways without defining multiple node types.
In the dispatch test, the custom node is an intermediate `NetworkNode` with a potential loss, but without a loss in energy flow.

```julia
struct PotentialLossNode{T<:PotentialPower} <: NetworkNode
id::Any
cap::TimeProfile
opex_var::TimeProfile
opex_fixed::TimeProfile
resource::T
input::Dict{<:Resource,<:Real}
output::Dict{<:Resource,<:Real}
data::Vector{<:ExtensionData}
loss_factor::Float64
end

function PotentialLossNode(
id,
cap::TimeProfile,
opex_var::TimeProfile,
opex_fixed::TimeProfile,
resource::T,
loss_factor::Float64,
) where {T<:PotentialPower}
return PotentialLossNode{T}(
id,
cap,
opex_var,
opex_fixed,
resource,
Dict(resource => 1.0),
Dict(resource => 1.0),
ExtensionData[],
loss_factor,
)
end
```

### 3. Declare resource-specific variables

Use [`variables_flow_resource`](@ref) to create resource variables.

Important:

- Declare each variable name once.
- Filter `𝒩` and `ℒ` down to the subsets that actually use the special resource.
- You can create resource dependent bounds as well.

```julia
function EMB.variables_flow_resource(
m,
𝒩::Vector{<:EMB.Node},
𝒫::Vector{<:PotentialPower},
𝒯,
modeltype::EnergyModel,
)
𝒩ᵒᵘᵗ = filter(n -> any(p ∈ 𝒫 for p ∈ outputs(n)), 𝒩)
𝒩ⁱⁿ = filter(n -> any(p ∈ 𝒫 for p ∈ inputs(n)), 𝒩)

@variable(m,
lower_limit(p) ≤
energy_potential_node_out[n ∈ 𝒩ᵒᵘᵗ, 𝒯, p ∈ intersect(outputs(n), 𝒫)] ≤
upper_limit(p)
)
@variable(m,
lower_limit(p) ≤
energy_potential_node_in[n ∈ 𝒩ⁱⁿ, 𝒯, p ∈ intersect(inputs(n), 𝒫)] ≤
upper_limit(p)
)
end

function EMB.variables_flow_resource(
m,
ℒ::Vector{<:Link},
𝒫::Vector{<:PotentialPower},
𝒯,
modeltype::EnergyModel,
)
ℒᵉᵖ = filter(l -> any(p ∈ 𝒫 for p ∈ EMB.link_res(l)), ℒ)
@variable(m, energy_potential_link_in[ℒᵉᵖ, 𝒯, 𝒫])
@variable(m, energy_potential_link_out[ℒᵉᵖ, 𝒯, 𝒫])
end
```

### 4. Add resource-specific constraints

Create a new method [`constraints_resource`](@ref) for custom node or link behavior.
These methods can be either for the complete set of [`Node`](@ref EnergyModelsBase.Node) and [`Link`](@ref)s or alternatively for only a specified subset of nodes.
If you only specify it for a subset of nodes, it is important that the new resource is only an `input` or `output` of this subset.

```julia
function EMB.constraints_resource(
m,
n::PotentialLossNode,
𝒯,
𝒫::Vector{<:PotentialPower},
modeltype::EnergyModel,
)
𝒫ᵒᵘᵗ = filter(p -> p ∈ 𝒫, outputs(n))
𝒫ⁱⁿ = filter(p -> p ∈ 𝒫, inputs(n))

@constraint(m, [t ∈ 𝒯, p ∈ 𝒫ᵒᵘᵗ],
m[:energy_potential_node_out][n, t, p] ==
n.loss_factor * m[:energy_potential_node_in][n, t, p]
)
end

function EMB.constraints_resource(
m,
l::Link,
𝒯,
𝒫::Vector{<:PotentialPower},
modeltype::EnergyModel,
)
𝒫ˡⁱⁿᵏ = filter(p -> p ∈ 𝒫, EMB.link_res(l))
@constraint(m, [t ∈ 𝒯, p ∈ 𝒫ˡⁱⁿᵏ],
m[:energy_potential_link_in][l, t, p] ==
m[:energy_potential_link_out][l, t, p]
)
end
```

### 5. Couple node and link variables

Use [`constraints_couple_resource`](@ref) to connect node and link resource variables.

```julia
function EMB.constraints_couple_resource(
m,
𝒩::Vector{<:EMB.Node},
ℒ::Vector{<:Link},
𝒫::Vector{<:PotentialPower},
𝒯,
modeltype::EnergyModel,
)
for n ∈ 𝒩
ℒᶠʳᵒᵐ, ℒᵗᵒ = EMB.link_sub(ℒ, n)
𝒫ᵒᵘᵗ = filter(p -> p ∈ 𝒫, outputs(n))
𝒫ⁱⁿ = filter(p -> p ∈ 𝒫, inputs(n))

@constraint(m, [t ∈ 𝒯, p ∈ 𝒫ᵒᵘᵗ, l ∈ ℒᶠʳᵒᵐ],
m[:energy_potential_node_out][n, t, p] ==
m[:energy_potential_link_in][l, t, p]
)

@constraint(m, [t ∈ 𝒯, p ∈ 𝒫ⁱⁿ, l ∈ ℒᵗᵒ],
m[:energy_potential_link_out][l, t, p] ==
m[:energy_potential_node_in][n, t, p]
)
end
end
```
4 changes: 2 additions & 2 deletions docs/src/how-to/utilize-timestruct.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ op_number = length(op_duration)
operational_periods = SimpleTimes(op_number, op_duration)

# output
SimpleTimes{Int64}(11, [4, 2, 1, 1, 2, 4, 2, 1, 1, 2, 4])
SimpleTimes{Int64}(11, [4, 2, 1, 1, 2, 4, 2, 1, 1, 2, 4], 24)
```

In this case, we model the day not with hourly resolution, but only have hourly resolution in the morning and afternoon.
Expand All @@ -60,7 +60,7 @@ Instead, one can also write
operational_periods = SimpleTimes(op_duration)

# output
SimpleTimes{Int64}(11, [4, 2, 1, 1, 2, 4, 2, 1, 1, 2, 4])
SimpleTimes{Int64}(11, [4, 2, 1, 1, 2, 4, 2, 1, 1, 2, 4], 24)
```

and a constructor will automatically deduce that there have to be 11 operational periods.
Expand Down
1 change: 1 addition & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Depth = 1
Pages = [
"how-to/create_new_element.md",
"how-to/create-new-node.md",
"how-to/extend-resource-functionality.md",
"how-to/utilize-timestruct.md",
"how-to/update-models.md",
"how-to/contribute.md",
Expand Down
5 changes: 5 additions & 0 deletions docs/src/library/internals/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ emissions_operational
constraints_emissions
constraints_elements
constraints_couple
constraints_couple_resource
constraints_resource
constraints_level_iterate
constraints_level_rp
constraints_level_scp
Expand All @@ -39,6 +41,7 @@ constraints_level_bounds
```@docs
variables_capacity
variables_flow
variables_flow_resource
variables_opex
variables_capex
variables_emission
Expand Down Expand Up @@ -96,4 +99,6 @@ res_sub
```@docs
collect_types
sort_types
res_types
res_types_vec
```
Loading
Loading