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
11 changes: 10 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Release notes

## Unversioned

### Breaking changes

#### Rework of `PeriodDemandSink`

* Rewrote `PeriodDemandSink` with `PeriodPartition` (introduced in `TimeStruct` 0.9.12) to increase flexibility of node with respect to the time structure.
* Rewriting changed input arguments as well as behavior of the node.

## Version 0.3.0 (2026-04-16)

### Breaking changes
Expand All @@ -9,7 +18,7 @@
* Early retirement changes the model behavior.
* Model worked without adjustments except for compatibility updates.
* Breaking change still included to maintain the possibility to do bug fixes in version 0.1.x for existing models with `EnergyModelsBase` v0.9.x.
* UpdMoved from `Data` to `ExtensionData` in fields and tests.
* Moved from `Data` to `ExtensionData` in fields and tests.
* Improved `MinUpDownTimeNode`:
* Rewrote the function `constraints_capacity` based on core functionality.
* Updated the test set to identify all potential problems with the node.
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
TimeStruct = "f9ed5ce0-9f41-4eaa-96da-f38ab8df101c"

[compat]
EnergyModelsBase = "0.10"
EnergyModelsBase = "0.10.5"
EnergyModelsRenewableProducers = "0.7"
JuMP = "1.23"
TimeStruct = "0.9"
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
> The different node types are partly experimental.
> They have furthermore some limitations with respect to the chosen `TimeStructure` or whether they are able to handle investments.
> As a consequence, it is advised to read the documentation for each node to identify their usefulness.
> Is is planned to removesome nodes and rewrite the behaviour of other nodes to improve their flexibility.
> Is is planned to remove some nodes and rewrite the behaviour of other nodes to improve their flexibility.
>
> Version 0.2.8 is based on [`EnergyModelsBase` v0.8](https://energymodelsx.github.io/EnergyModelsBase.jl/v0.8/) while version 0.2.9 is adding support for [`EnergyModelsBase` v0.9](https://energymodelsx.github.io/EnergyModelsBase.jl/v0.9/).
> Among others, using `PeriodDemandSink` in combination with `EnergyModelsGUI` results in errors when trying to access field values with `PartitionProfile`.

## Usage

Expand All @@ -26,7 +26,7 @@ Please refer to the *[documentation](https://energymodelsx.github.io/EnergyModel

## Cite

If you find `EnergyModelsFlex` useful in your work, we kindly request that you cite the following [publication](https://doi.org/10.69997/sct.119164):
If you find `EnergyModelsFlex` useful in your work, we kindly request that you cite the following *[publication](https://doi.org/10.69997/sct.119164)*:

```bibtex
@article{venas2025energymodelsflex,
Expand Down
6 changes: 4 additions & 2 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ makedocs(
"Links" => Any[
"CapacityCostLink"=>"links/capacitycostlink.md",
],
"How-to" =>
Any["Contribute"=>"how-to/contribute.md"],
"How-to" => Any[
"Contribute"=>"how-to/contribute.md",
"Update models"=>"how-to/update-models.md",
],
"Library" => Any[
"Public"=>"library/public.md",
"Internals"=>String[
Expand Down
33 changes: 33 additions & 0 deletions docs/src/how-to/update-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# [Update your model to the latest versions](@id how_to-update)

`EnergyModelsFlex` is still in a pre-release version.
Hence, there are frequently breaking changes occuring, although we plan to keep backwards compatibility.
This document is designed to provide users with information regarding how they have to adjust their nodes to keep compatibility to the latest changes.

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

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
# The previous nodal description for a `PeriodDemandSink` was given by:
PeriodDemandSink(
id::Any,
period_length::Int,
period_demand::Array{<:Real},
cap::TimeProfile,
penalty::Dict{Symbol,<:TimeProfile},
input::Dict{<:Resource,<:Real},
data::Vector{<:ExtensionData} = ExtensionData[],
)

# This translates to the following new version
PeriodDemandSink(
id,
cap,
FixedProfile(period_length),
PartitionProfile(period_demand),
penalty,
input,
data
)
```
2 changes: 1 addition & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ Depth = 1

```@contents
Pages = [
"how-to/update-models.md",
"how-to/contribute.md",
"how-to/utilize.md",
]
Depth = 1
```
Expand Down
1 change: 0 additions & 1 deletion docs/src/library/internals/methods-EMF.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ Pages = ["methods-EMF.md"]
## [Check methods](@id lib-int-met-check)

```@docs
EMF.check_period_ts
EMF.check_limits_default
EMF.check_input
EMF.check_cap_price_periods
Expand Down
6 changes: 3 additions & 3 deletions docs/src/library/internals/methods-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ Pages = ["methods-fields.md"]

```@docs
EnergyModelsFlex.period_demand
EnergyModelsFlex.period_length
EnergyModelsFlex.period_index
EnergyModelsFlex.period_duration
EnergyModelsFlex.periods
EnergyModelsFlex.number_of_periods
```

Expand All @@ -35,4 +35,4 @@ EnergyModelsFlex.cap_price
```@docs
EnergyModelsFlex.limits
EnergyModelsFlex.heat_resource
```
```
79 changes: 48 additions & 31 deletions docs/src/nodes/sink/perioddemand.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ This node can, *e.g.*, be combined with [`MinUpDownTimeNode`](@ref), to allow pr

!!! warning "TimeStructure for node"
This node is designed for **uniform or repetitive duration of operational periods**.
Irregular durations may cause misalignment of shifted loads, especially if the field `period_length` does not align with the chosen [`SimpleTimes`](@extref TimeStruct.SimpleTimes) structure representing the operational periods.
Irregular durations may cause misalignment of shifted loads, especially if the field `period_duration` does not align with the chosen [`SimpleTimes`](@extref TimeStruct.SimpleTimes) structure representing the operational periods.

!!! warning "`PeriodDemandSink` and `EnergyModelsGUI`"
Some of the fields of this node 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.

## [Introduced type and its fields](@id nodes-perioddemandsink-fields)

Expand All @@ -33,12 +38,11 @@ The standard fields are given as:
- **`penalty::Dict{Symbol,<:TimeProfile}`**:\
The penalty dictionary is used for providing penalties for soft constraints to allow for both over and under delivering the demand.\
It must include the fields `:surplus` and `:deficit`.
In addition, it is crucial that the sum of both values is larger than 0 to avoid an unconstrained model.
In addition, it is crucial that the sum of both values in each demand period is larger than 0 to avoid an unconstrained model.

!!! warning "Chosen values"
The current implementation does not represent the proper cost due to the summation.
Instead, you must consider the duration of an operational period and the field `period_length` when providing a value.
In this case, the value should be multiplied by ``1/period\_length(n) \times duration(t)``.
The implementation is relative to the chosen `period_duration` (see below).
If the period duration is ``24``, then the cost is for the unsatisfied demand within the ``24`` demand period, multiplied with the probability and the repetitons within a strategic period.

- **`input::Dict{<:Resource,<:Real}`**:\
The field `input` includes [`Resource`](@extref EnergyModelsBase.Resource)s with their corresponding conversion factors as dictionaries.\
Expand All @@ -60,18 +64,27 @@ The standard fields are given as:

[`AbstractPeriodDemandSink`](@ref EnergyModelsFlex.AbstractPeriodDemandSink)s require additional fields to specify both the periods and their respective demands:

- **`period_length::Int`**:\
Defines how many operational periods are included in a single demand period.\
For instance, if the duration of the operational periods is 1 hour and `period_length = 24`, then each demand period spans one day.
The demand of this node (for a given day, see below) must then be filled on a daily basis, without any restrictions on *when* during the day the demand must be filled.
- **`period_duration::TimeProfile`**:\
Defines the total duration of a single demand period.\
For instance, if the duration of 1 of the operational time structure is 1 hour and `period_duration = FixedProfile(24)`, then each demand period spans one day.
The demand of this node (for a given day, see below) must then be filled on a daily basis, without any restrictions on *when* during the day the demand must be filled given the available capacity.\
Due to a constructor, it can either be specified as number (the same duration in all demand periods), as a vector (varying duration of each demand period), or as a time profile (*e.g.*, varying period durations due to varying operational time structures).
It cannot be specified as `OperationalProfile`.

- **`period_demand::TimeProfile`**:\
The total demand to be met during each demand period.
The length of this time profile should match the number of periods (*e.g.*, days) in the time structure.
If the time structure represents one year with hourly resolution and the demand periods correspond to a day, this time profile must then have 365 elements.

- **`period_demand::Vector{<:Real}`**:\
The total demand to be met during each demand period. The length of this vector should match the number of periods (*e.g.*, days) in the time structure. If the time structure represents on year with hourly resolution, this vector must then have 365 elements.
It is best to utilize the [`PartitionProfile`](@extref TimeStruct.PartitionProfile) type if the demand is varying.
If it is constant, you can also utilize [`StrategicProfile`][@extref TimeStruct.StrategicProfile], [`RepresentativeProfile`][@extref TimeStruct.RepresentativeProfile], or [`ScenarioProfile`][@extref TimeStruct.ScenarioProfile], depending on your chosen time structure.
It cannot be specified as `OperationalProfile`.

!!! warning "Time consistency"
Ensure that the `period_demand` vector length aligns with the total time horizon divided by `period_length`. Mismatches can lead to indexing errors or inconsistent demand enforcement.
Ensure that the `period_demand` time profile length aligns with the operational time horizon duration divided by `period_duration`
Mismatches can lead to indexing errors or inconsistent demand enforcement.

These fields are at the 2ⁿᵈ and 3ʳᵈ position below the field `id` as shown in [`PeriodDemandSink`](@ref).
These fields are at the 3ʳᵈ and 4ᵗʰ position below the field `cap` as shown in [`PeriodDemandSink`](@ref).

## [Mathematical description](@id nodes-perioddemandsink-math)

Expand Down Expand Up @@ -105,12 +118,12 @@ The variables include:
#### [Additional variables](@id nodes-perioddemandsink-math-add)

[`AbstractPeriodDemandSink`](@ref EnergyModelsFlex.AbstractPeriodDemandSink) nodes declare in addition several variables through dispatching on the method [`EnergyModelsBase.variables_element()`](@ref) for including constraints for deficits and surplus for individual resources as well as what the fraction satisfied by each resource.
These variables are for a [`AbstractPeriodDemandSink`](@ref EnergyModelsFlex.AbstractPeriodDemandSink) node ``n`` in demand periods ``i``:
These variables are for a [`AbstractPeriodDemandSink`](@ref EnergyModelsFlex.AbstractPeriodDemandSink) node ``n`` in demand periods ``t_pd``:

- ``\texttt{demand\_sink\_surplus}[n, i]``:\
Surplus of energy delivered beyond the required `period_demand` in demand period `i`.
- ``\texttt{demand\_sink\_deficit}[n, i]``:\
Deficit of energy delivered relative to the `period_demand` in period `i`.
- ``\texttt{demand\_sink\_surplus}[n, t_pd]``:\
Surplus of energy delivered beyond the required `period_demand` of demand period `t_pd` .
- ``\texttt{demand\_sink\_deficit}[n, t_pd]``:\
Deficit of energy delivered relative to the `period_demand` of demand period `t_pd` .

### [Constraints](@id nodes-perioddemandsink-math-con)

Expand Down Expand Up @@ -155,33 +168,37 @@ These standard constraints are:
- `constraints_data`:\
This function is only called for specified additional data, see above.

The function `constraints_capacity` is extended with a new method to account for the calculation of the period demand deficit and surplus through:
The function `constraints_capacity` is extended with a new method to account for the calculation of the period demand deficit and surplus.

The overall balance is modified as

```math
\texttt{cap\_use}[n, t] + \texttt{sink\_deficit}[n, t] = \texttt{cap\_inst}[n, t] + \texttt{sink\_surplus}[n, t]
\texttt{cap\_use}[n, t] + \texttt{sink\_deficit}[n, t] = \texttt{cap\_inst}[n, t]
```

while operational period surplus ``\texttt{sink\_surplus}[n, t]`` is fixed to 0.

The surplus and deficit of the demand period can then be calculated as

```math
\begin{aligned}
\texttt{demand\_sink\_deficit}[n, i] + & \sum_{t \in P_i} \texttt{​cap\_use}[n,t] = \\
& \texttt{demand\_sink\_surplus}[n, i] + period\_demand(n, i)
\texttt{demand\_sink\_deficit}[n, t_{pd}] + & \sum_{t \in t_{pd}} \texttt{​cap\_use}[n, t] \times duration(t) = \\
& \texttt{demand\_sink\_surplus}[n, t_{pd}] + period\_demand(n, t_{pd})
\end{aligned}
```

where ``P_i`` is the set of operational periods in demand period ``i``.
where ``t_{pd}`` is the demand period consisting of a set of operational periods.

As a consequence, `constraints_opex_var` requires as well a new method as we only consider the deficit within a complete period:

```math
\begin{aligned}
\texttt{opex\_var}[n, t_{inv}] = \sum_{t ∈ t_{inv}}(& \texttt{demand\_sink\_surplus}[n, i_t] \times \texttt{surplus\_penalty}(n, t) + \\
& \texttt{demand\_sink\_deficit}[n, i_t] \times \texttt{deficit\_penalty}(n, t)) \times \\
& scale\_op\_sp(t_{inv}, t)
\texttt{opex\_var}[n, t_{inv}] = \sum_{t_{pd}periods(t_{inv})}(& \texttt{demand\_sink\_surplus}[n, t_{pd}] \times \texttt{surplus\_penalty}(n, t_{pd}) + \\
& \texttt{demand\_sink\_deficit}[n, t_{pd}] \times \texttt{deficit\_penalty}(n, t_{pd})) \times \\
& scale\_op\_sp(t_{inv}, first(t_{pd})) / duration(first(t_{pd}))
\end{aligned}
```

where ``i_t`` is the period index such that ``t \in P_{i_t}``.

!!! tip "The function `scale_op_sp`"
The function [``scale\_op\_sp(t_{inv}, t)``](@extref EnergyModelsBase.scale_op_sp) calculates the scaling factor between operational and investment periods.
It also takes into account potential operational scenarios and their probability as well as representative periods.
!!! note "`scale_op_sp` and `duration`"
The function [`scale_op_sp(t_inv, t)`](@extref EnergyModelsBase.scale_op_sp) calculates the scaling factor between operational and investment periods including the `duration` of each operational period.
It hence must be divided by the `duration` of the first operational period to avoid including the duration of the operational period.
12 changes: 6 additions & 6 deletions examples/flexible_demand.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ CO2 = ResourceEmit("CO2", 0)
# This would be a huge incentive to produce during the weekend, if we allowed the
# `PeriodDemandSink` capacity during the weekend.
day = [1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4, 3, 2]
el_cost = [repeat(day, 5)..., fill(0, 2 * 24)...]
el_cost = vcat(repeat(day, 5), zeros(48))

grid = RefSource(
"grid",
Expand All @@ -41,16 +41,16 @@ grid = RefSource(
# The production can only run between 6-20 on weekdays, with a capacity of 300 kW.
# First, define the maximum capacity for a regular weekday (24 hours).
# The capacity is 0 between 0 am and 6 am, 300 kW between 6 am and 8 pm, and 0 again between 8 pm and midnight.
weekday_prod = [fill(0, 6)..., fill(300, 14)..., fill(0, 4)...]
@assert length(weekday_prod) == 24
weekday_prod = vcat(zeros(6), fill(300, 14), zeros(4))

# Repeat a weekday 5 times, for a workweek, then no production on the weekends.
week_prod = [repeat(weekday_prod, 5)..., fill(0, 2 * 24)...]
week_prod = vcat(repeat(weekday_prod, 5), fill(0, 2 * 24))

demand = PeriodDemandSink(
"demand_product",
24, # 24 hours per day.
[fill(1500, 5)..., 0, 0], # Demand of 1500 units per day, and nothing (0) in the weekend.
OperationalProfile(week_prod), # kW - installed capacity
24, # 24 hours per day.
PartitionProfile(vcat(ones(5)*1500, [0, 0])), # Demand of 1500 units per day, and nothing (0) in the weekend.
Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e8)), # € / Demand - Price for not delivering products
Dict(Product => 1),
)
Expand Down
2 changes: 2 additions & 0 deletions src/EnergyModelsFlex.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ for node_type ∈ ["source", "sink", "network", "storage", "link"]
include("$node_type/checks.jl")
end

include("legacy_constructors.jl")

export MinUpDownTimeNode, ActivationCostNode, ElectricBattery, LoadShiftingNode
export PeriodDemandSink, MultipleInputSink
export PayAsProducedPPA, StorageEfficiency, LimitedFlexibleInput, Combustion
Expand Down
35 changes: 35 additions & 0 deletions src/legacy_constructors.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## Sinks

function PeriodDemandSink(
id::Any,
period_length::Int,
period_demand::Array{<:Real},
cap::TimeProfile,
penalty::Dict{Symbol,<:TimeProfile},
input::Dict{<:Resource,<:Real},
data::Vector{<:ExtensionData} = ExtensionData[],
)
@warn(
"The used implementation of a `PeriodDemandSink` will be discontinued in the near " *
"future. The new implementation uses the approach of `PeriodPartition` with " *
"a change in the field positions.\n" *
"In practice, three changes have to be incorporated: \n 1. the capacity is moved to " *
"the 2ⁿᵈ position, \n 2. `period_length` is renamed `period_duration`, moved to the " *
"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) " *
"on how to update your model to the latest version.",
maxlog = 1
)

return PeriodDemandSink(
id,
cap,
FixedProfile(period_length),
PartitionProfile(period_demand),
penalty,
input,
data
)
end
Loading
Loading