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
7 changes: 6 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@

#### Rework of `PeriodDemandSink`

* Rewrote `PeriodDemandSink` with `PeriodPartition` (introduced in `TimeStruct` 0.9.12) to increase flexibility of node with respect to the time structure.
* Rewrote `PeriodDemandSink` with `PeriodPartition` (introduced in *[`TimeStruct` 0.9.12](https://github.com/sintefore/TimeStruct.jl/releases/tag/v0.9.12)*) to increase flexibility of the node with respect to the time structure.
* Rewriting changed input arguments as well as behavior of the node.

#### Rework of `CapacityCostLink`

* Rewrote `CapacityCostLink` with `PeriodPartition` (introduced in *[`TimeStruct` 0.9.12](https://github.com/sintefore/TimeStruct.jl/releases/tag/v0.9.12)*) to increase flexibility of the link with respect to the time structure.
* Rewriting requires adjustment of the parameters due to changed meaning of some of the values.

## Version 0.3.0 (2026-04-16)

### Breaking changes
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
> As a consequence, it is advised to read the documentation for each node to identify their usefulness.
> Is is planned to remove some nodes and rewrite the behaviour of other nodes to improve their flexibility.
>
> Among others, using `PeriodDemandSink` in combination with `EnergyModelsGUI` results in errors when trying to access field values with `PartitionProfile`.
> Among others, using `PeriodDemandSink` and `CapacityCostLink` in combination with `EnergyModelsGUI` results in errors when trying to access fields that have as values `PartitionProfile`.
> The same holds for variables that are defined over `PeriodPartition`s where you cannot see the results.

## Usage

Expand Down
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ using Literate

const EMB = EnergyModelsBase
const EMF = EnergyModelsFlex
const TS = TimeStruct

DocMeta.setdocmeta!(
EnergyModelsFlex,
Expand Down
66 changes: 66 additions & 0 deletions docs/src/how-to/update-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ This document is designed to provide users with information regarding how they h

## [Adjustments from 0.3.0](@id how_to-update-03)

### [Changed `PeriodDemandSink`](@id how_to-update-03-PeriodDemandSink)

The introduction of `PartitionProfile` in *[`TimeStruct` v0.9.12](https://github.com/sintefore/TimeStruct.jl/releases/tag/v0.9.12)* allowed a rewrite of `PeriodDemandSink`:

```julia
Expand All @@ -31,3 +33,67 @@ PeriodDemandSink(
data
)
```

!!! note "`period_length` meaning"
The meaning of `period_length` has changed in addition to its name.
While it previously defined the number of periods, it defines in the new implementation the total duration of the operational periods that are within a single demand period.
Consider the following example in which we want three demand periods consisting of two operational periods each:

```julia
# Time structure with 1 strategic period of duration 1 and 6 operational periods of duration 2
𝒯 = TwoLevel(1, 1, SimpleTimes(6, 2))

# OLD: Previously, we had to specify
period_length = 2
# implying 2 operational periods

# NEW: Now, we have to specify
period_length = 4
# implying a total duration of 4 for the 2 periods
```

THis is reflected by the renaming from `period_length` to `period_duration`.

### [Changed `CapacityCostLink`](@id how_to-update-03-CapacityCostLink)

The introduction of `PartitionProfile` in *[`TimeStruct` v0.9.12](https://github.com/sintefore/TimeStruct.jl/releases/tag/v0.9.12)* allowed a rewrite of `CapacityCostLink` which changed the model behavior:

The field `cap_price_periods` was renamed to `cap_period_duration` and its meaning was changed when moving from 0.3 to 0.4.
The reason for this change is to make the link behavior less dependent on the operational resolution.
The following updated must hence be performed as it is not possible to create respective constructor methods:

1. When specifying a number, the previous meaning of the number of operational periods was changed to the sum of the durations of the operational periods.
The following change is hence required if you have operational durations differing from `1`:

```julia
# time structure
ts = TwoLevel(2, 1, SimpleTimes(10, 2); op_per_strat=8760.0)

# old behavior, corresponding to 5 periods
cap_price_periods = 5

# new behavior, corresponding to periods whose duration sums to at least 4
cap_period_duration = 4
```

Both cases create five price periods with a duration of `4`, but instead of specifying the number of periods periods, we now define the duration of each price period.

2. When specifying a vector, the previous scaling based on the chosen value of `op_per_strat` was removed as it is in our opinion more straightforward to base it on the actual operational time structure.
The following change is hence required:

```julia
# time structure
ts = TwoLevel(2, 1, SimpleTimes(10, 2); op_per_strat=8760.0)

# old behavior, corresponding to 5 periods of a total duration of duration based on `op_per_strat`
cap_period_duration = [1752, 1752, 1752, 1752, 1752]

# new behavior, corresponding to 5 periods of a total duration of 4 based on `SimpleTimes`
cap_period_duration = [4, 4, 4, 4, 4]
```

With the scaling factor ``8760 / (10 * 2) = 438``, each new value of 4 corresponds to the previously used value of ``4 * 438 = 1752``, *i.e.*, the same five partitions of two time periods each.

!!! note
Both examples above result in exactly the same behavior.
They illustrate the different approaches that could and can be used for defining a [`CapacityCostLink`](@ref).
3 changes: 0 additions & 3 deletions docs/src/library/internals/methods-EMF.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,10 @@ Pages = ["methods-EMF.md"]
```@docs
EMF.check_limits_default
EMF.check_input
EMF.check_cap_price_periods
```

## [Utility functions](@id lib-int-links-fun_utils)

```@docs
EMF.get_avg_cap_price
EMF.get_sub_periods
EMF.get_sub_pers_durations
```
21 changes: 11 additions & 10 deletions docs/src/library/internals/methods-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,30 @@ Pages = ["methods-fields.md"]
## [`PeriodDemandSink` types](@id lib-int-met_field-PeriodDemandSink)

```@docs
EnergyModelsFlex.period_demand
EnergyModelsFlex.period_duration
EnergyModelsFlex.periods
EnergyModelsFlex.number_of_periods
EMF.period_demand
EMF.period_duration(n::EMF.AbstractPeriodDemandSink)
EMF.periods(n::EMF.AbstractPeriodDemandSink, ts::TS.TimeStructure)
EMF.number_of_periods
```

## [`ActivationCostNode` types](@id lib-int-met_field-ActivationCostNode)

```@docs
EnergyModelsFlex.activation_consumption
EMF.activation_consumption
```

## [`CapacityCostLink` types](@id lib-int-met_field-CapacityCostLink)

```@docs
EnergyModelsFlex.cap_price_periods
EnergyModelsFlex.cap_resource
EnergyModelsFlex.cap_price
EMF.cap_price
EMF.period_duration(l::CapacityCostLink)
EMF.periods(l::CapacityCostLink, ts::TS.TimeStructure)
EMF.cap_resource
```

## [`Combustion` types](@id lib-int-met_field-Combustion)

```@docs
EnergyModelsFlex.limits
EnergyModelsFlex.heat_resource
EMF.limits
EMF.heat_resource
```
85 changes: 66 additions & 19 deletions docs/src/links/capacitycostlink.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,48 @@ This is useful for applications such as transmission networks, pipelines, or int

In addition, they only allow the transport of a single, specified [`Resource`](@extref EnergyModelsBase.Resource).

!!! warning "`CapacityCostLink` and `EnergyModelsGUI`"
Some of the fields of this link cannot be represented in `EnergyModelsGUI`.
The reason for that limitation is that `EnergyModelsGUI` does not yet support partitions of `TimePeriod`s.
`EnergyModelsGUI` can still be utilized for all other fields.

!!! warning "Changed behavior between 0.3 and 0.4"
The meaning of the field `cap_period_duration` was changed when moving from 0.3 to 0.4:

1. When specifying a number, the previous meaning of the number of operational periods was changed to the sum of the durations of the operational periods.
The reason for this change is to make the link behavior less dependent on the operational resolution.
The following change is hence required if you have operational durations differing from 1:

```julia
# time structure
ts = SimpleTimes(10, 2)

# old behavior, corresponding to 2 periods
cap_period_duration = 2

# new behavior, corresponding to periods whocse duration sums to at least 4
cap_period_duration = 4
```

2. When specifying a vector, the previous scaling based on the chosen value of `op_per_strat` was removed as it is in our opinion more straightforward to base it on the actual operational time structure.
The following change is hence required:

```julia
# time structure
ts = Twolevel(2, 1, SimpleTimes(10, 2); op_per_strat=8760.0)

# old behavior, corresponding to 5 periods a 1752 duration based on `op_per_strat`
cap_period_duration = [1752, 1752, 1752, 1752, 1752]

# new behavior, corresponding to 5 periods a 4 duration based on `SimpleTimes`
cap_period_duration = [4, 4, 4, 4, 4]
```

## [Introduced type and its fields](@id links-CapacityCostLink-fields)

[`CapacityCostLink`](@ref) is implemented as equivalent to an abstract type [`Link`](@extref EnergyModelsBase.Link).
Hence, it utilizes the same functions declared in `EnergyModelsBase`.

!!! warning "Application of the link"
The current implementation is not very flexible with respect to the chosen time structure.
Specifically, if you use [`OperationalScenarios`](@extref TimeStruct.OperationalScenarios), [`RepresentativePeriods`](@extref TimeStruct.RepresentativePeriods), or differing operational structures within your [`TwoLevel`](@extref TimeStruct.TwoLevel), you must be careful when choosing the parameter `cap_price_periods`.

### [Standard fields](@id links-CapacityCostLink-fields-stand)

[`CapacityCostLink`](@ref) has the following standard fields, equivalent to a [`Direct`](@extref EnergyModelsBase.Direct) link:
Expand Down Expand Up @@ -50,19 +83,33 @@ The following additional fields are included for [`CapacityCostLink`](@ref) link
Capacity costs are calculated per sub-period and then summed over the strategic period.
This means a constant value (*e.g.,* €/GW/year) is effectively applied once for each sub-period (based on the peak within that sub-period), and is not automatically scaled by sub-period duration.

Example: With `cap_price = 100 €/GW/year`, `cap_price_periods = 12`, and a peak usage of 1 GW in each month, the model computes a total cost of 12 × 100 = 1200 €/year.
**Example:**

```julia
# Modelling a full year with hourly resolution
ts = TwoLevel(1, 1, SimpleTimes(8760, 1); op_per_strat=8760.0)

# 12 price periods corresponding to months
cap_price_periods = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] .* 24

cap_price = 100 # €/GW/year
```

If the peak usage is 1 GW in each month, the model computes a total cost of 12 × 100 = 1200 €/year.

To achieve seasonal/monthly peak charges, define multiple `cap_price_periods` and provide `cap_price` values that represent the intended charge per sub-period (or scale the values accordingly).

It is planned to change this behavior in the future.
The change corresponds to a breaking change as we change the behavior of the model.
- **`cap_price_periods::Union{Int64, Vector{<:Number}}`** :\
The number of sub-periods within a year for which the capacity cost is calculated (if specifying an `Int64`) or the duration of the individual sub periods (if specifying a `Vector{<:Number}`).
This allows modeling of varying peak demands across seasons.
The value must be positive if your are using an `Int64` (and hence specifiy the number of periods) or all values of be positive and summing up to the specified scaling factor between operational and strategic period durations (the parameter `op_per_strat`) if you are using a `Vector{<:Number}`.

!!! tip "Number of sub-periods"
For investment periods with many operational periods, consider increasing the number of `cap_price_periods`.
- **`cap_period_duration::TimeProfile`** :\
Defines the total duration of a capacity price period.\
For instance, if the duration of 1 of the operational time structure is 1 hour and `period_duration = FixedProfile(24)`, then each capacity price period spans one day.
The capacity price of this node (for a given day, see below) is the given by the maximum capacity usage within a day.\
Due to a constructor, it can either be specified as number (the same duration in all capacity price periods), as a vector (varying duration of each capacity price period), or as a time profile (*e.g.*, varying period durations due to varying operational time structures).
It cannot be specified as `OperationalProfile` and must be positive for each individual value.

!!! tip "Duration of capacity price periods"
For investment periods with many operational periods, consider decreasing `cap_period_duration`.
The [`CapacityCostLink`](@ref) capacity constraints couple operational periods and can significantly increase solve time.
Splitting the horizon into multiple sub-periods reduces this coupling and often makes the problem much easier to solve.
In some cases, this also means using more than one capacity price period even if capacity costs occur only annually in reality, depending on model size and complexity.
Expand Down Expand Up @@ -103,8 +150,8 @@ with parantheses.

Two additional variables track capacity utilization and associated costs over sub-periods:

- ``\texttt{ccl\_cap\_use\_max}[l, t_{sub}]``: Maximum capacity usage in sub-period ``t_{sub}`` for link ``l``.
- ``\texttt{ccl\_cap\_use\_cost}[l, t_{sub}]``: Operational cost in sub-period ``t_{sub}`` for link ``l``.
- ``\texttt{ccl\_cap\_use\_max}[l, t_{pd}]``: Maximum capacity usage in sub-period ``t_{pd}`` for link ``l``.
- ``\texttt{ccl\_cap\_use\_cost}[l, t_{pd}]``: Operational cost in sub-period ``t_{pd}`` for link ``l``.

### [Constraints](@id links-CapacityCostLink-math-con)

Expand All @@ -126,28 +173,28 @@ and the no-loss constraint

All additional constraints are created within a new method for the function [`create_link`](@extref EnergyModelsBase.create_link).

The capacity utilization constraint tracks the maximum usage within each sub-period:
The capacity utilization constraint tracks the maximum usage within each sub-period ``t_{sub}``:

```math
\texttt{link\_in}[l, t, cap\_resource(l)] \leq \texttt{ccl\_cap\_use\_max}[l, t_{sub}]
\texttt{link\_in}[l, t, cap\_resource(l)] \leq \texttt{ccl\_cap\_use\_max}[l, t_{pd}]
```

The capacity cost is calculated as:

```math
\texttt{ccl\_cap\_use\_cost}[l, t_{sub}] = \texttt{ccl\_cap\_use\_max}[l, t_{sub}] \times \overline{cap\_price}(l, t_{sub})
\texttt{ccl\_cap\_use\_cost}[l, t_{pd}] = \texttt{ccl\_cap\_use\_max}[l, t_{pd}] \times \overline{cap\_price}(l, t_{pd})
```

where ``\overline{cap\_price}`` is the average capacity price over the sub-period calculated as:

```math
\overline{cap\_price}(l, t_{sub}) = \frac{\sum_{t \in t_{sub}} cap\_price(l, t) \times duration(t)}{\sum_{t \in t_{sub}} duration(t)}
\overline{cap\_price}(l, t_{pd}) = \frac{\sum_{t \in t_{pd}} cap\_price(l, t) \times duration(t)}{\sum_{t \in t_{pd}} duration(t)}
```

Finally, costs are aggregated to each strategic period:

```math
\texttt{link\_opex\_var}[l, t_{inv}] = \sum_{t_{sub} \in t_{inv}} \texttt{ccl\_cap\_use\_cost}[l, t_{sub}]
\texttt{link\_opex\_var}[l, t_{inv}] = \sum_{t_{pd} \in t_{inv}} \texttt{ccl\_cap\_use\_cost}[l, t_{pd}]
```

In addition, the energy flow of the constrained resource should not exceed the maximum capacity, which is included through the following constraint:
Expand Down
2 changes: 1 addition & 1 deletion src/legacy_constructors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function PeriodDemandSink(
"3ʳᵈ position, and can accept as well a `Vector` or `TimeProfile`s as input, and\n" *
" 3. `period_demand` is moved to the 4ᵗʰ position and requires as input a " *
"`PartitionProfile` of the previously provided `Vector`.\n" *
"See the documentation (https://energymodelsx.github.io/EnergyModelsFlex.jl/stable/how-to/update-models/#Adjustments-from-0.3.0) " *
"See the documentation (https://energymodelsx.github.io/EnergyModelsFlex.jl/stable/how-to/update-models/03/PeriodDemandSink) " *
"on how to update your model to the latest version.",
maxlog = 1
)
Expand Down
64 changes: 19 additions & 45 deletions src/link/checks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,59 +6,33 @@ This method checks that the *[`CapacityCostLink`](@ref)* link is valid.
## Checks
- The field `cap` is required to be non-negative.
- The field `cap_price` is required to be non-negative.
- The field `check_cap_price_periods` must follow the rules oulined in the subfunction
[`check_cap_price_periods`](@ref), that is it must either be a positive number or the
durations must sum up to the value `op_per_strat` of the time structure.
- The individual `period_duration`s must all satisfy the specified duration(s) and be
positive.
"""
function EMB.check_link(l::CapacityCostLink, 𝒯, ::EnergyModel, ::Bool)
𝒯ᵖᵈ = periods(l, 𝒯)

@assert_or_log(
all(capacity(l, t) ≥ 0 for t ∈ 𝒯),
"The capacity must be non-negative."
)
@assert_or_log(
all(cap_price(l)[t] ≥ 0 for t ∈ 𝒯),
"The capacity price must be non-negative."
"The capacity prices must be non-negative."
)
check_cap_price_periods(l, 𝒯, cap_price_periods(l))
end

"""
check_cap_price_periods(l::CapacityCostLink, 𝒯, price_pers::Int64)
check_cap_price_periods(l::CapacityCostLink, 𝒯, price_pers::Vector{<:Number})

This method checks that the field `check_cap_price_periods` is correct.

## Checks
- If the field is an `Int64`, the field `check_cap_price_periods` is required to be positive.
- If the field is an `Vector{<:Number}`, the field `check_cap_price_periods` is required to sum
up to the value `op_per_strat` and each value in the `Vector` must be positive.
- The individual capacity price periods must be able to represent the operational time
structure
"""
function check_cap_price_periods(l::CapacityCostLink, 𝒯, price_pers::Int64)
@assert_or_log(
price_pers > 0,
"The number of sub periods of a strategic period must be positive."
)
price_pers > 0 && @assert_or_log(
vcat(get_sub_periods(l, 𝒯)...) == collect(𝒯),
"The operational period durations could not accumulate into `cap_price_periods =
$(price_pers)` sub periods of each strategic period."
)
end
function check_cap_price_periods(l::CapacityCostLink, 𝒯, price_pers::Vector{<:Number})
@assert_or_log(
sum(price_pers) ≈ 𝒯.op_per_strat,
"The duration of all sub periods must be equal to the value `op_per_strat` of the " *
"time structure."
)
@assert_or_log(
all(price_pers .> 0),
"Each sub period must have a positive duration."
)
@assert_or_log(
vcat(get_sub_periods(l, 𝒯)...) == collect(𝒯),
"The operational period durations could not accumulate into `cap_price_periods =
$(length(price_pers))` sub periods of each strategic period."
)
message = "are not allowed for the field `:period_duration`."
bool = EMB.check_partition_profile(period_duration(l), message)
if bool
@assert_or_log(
all(sum(duration(t) for t ∈ t_pd) ≥ period_duration(l, t_pd) for t_pd ∈ 𝒯ᵖᵈ),
"The duration of the last period on the `SimpleTimes` level is shorter than " *
"specified. This is caused by inconsistently specified `period_duration` and" *
"time structure."
)
@assert_or_log(
all(period_duration(l, t_pd) > 0 for t_pd ∈ 𝒯ᵖᵈ),
"Each value in the field `period_duration` must be positive."
)
end
end
Loading
Loading