Storage
Storage
nodes are subtypes of Storage
as they have in general an input and output (except for permanent CO₂ storage). Storages require additional variables and parameters. As a consequence, a new abstract type is specified.
Philosophy of Storage nodes
Storage
nodes differ from the other nodes as they are designed per default as parametric types using the concept of EnergyModelsBase.StorageBehavior
. In addition, capacities and operational expenses are not included at the first level of the composite type, but instead on a lower level.
Parametric implementation
The parametric input is not applied for any field, but instead for allowing simplified dispatch on the individual storage behavior of a Storage
node. As TimeStruct
, and hence, EnergyModelsBase
supports the inclusion of both representative periods and operational scenarios, it was the aim in the design to provide a reusable approach for calculating the level balances. The structure of the level balance calculation is explained on Storage level constraints while you can find the mathematical description in the Section Level constraints.
We differentiate between Accumulating
and Cyclic
storage behaviors. The former allows for a net change of the storage level within an investment period, while the latter requires a cyclic behavior for the level balance.
A single concrete type is included for Accumulating
using AccumulatingEmissions
. This type was introduced for ResourceEmit
resources to represent a permanent storage node. It was initially utilized for CO₂ storage.
Two concrete types are included for Cyclic
, CyclicRepresentative
and CyclicStrategic
. These two types differ only if the time structure includes representative periods. If not, they are equivalent. In the case of inclusion of representative periods, CyclicRepresentative
enforces the cyclic constraint within a representative period while CyclicStrategic
enforces the cyclic constraint within the investment period. In the case of CyclicStrategic
, we hence allow for a net change in the storage level within a representative period. This net change is then used for the scaling.
Capacities
Storage nodes can have up to three capacities, charge
, storage level
, and discharge
. In practice, a storage allways requires a level capacity corresponding to the maximum amount of stored energy. However, it is not necessary to include charge
and discharge
capacities if they are
- not representing an additional cost and
- it is possible to charge/discharge the storage within a single operational period.
In this case, the Storage
implementation allows the user to specify EnergyModelsBase.AbstractStorageParameters
reflecting the required input. We allow for multiple combinations within EnergyModelsBase.AbstractStorageParameters
containing a capacity, a variable OPEX, and/or a fixed OPEX. This is beneficial for ,e.g., compressed hydrogen storage in which the charge capacity requires investments in compressors, while the discharge capacity is purely limited by the structural limits.
The individual types are
StorCapOpex
- the capacity includes a capacity as well as a fixed and variable OPEX,StorCap
- the capacity only includes a capacity,StorCapOpexVar
- the capacity includes a capacity as well as a variable OPEX,StorCapOpexFixed
- the capacity includes a capacity as well as a fixed variable OPEX, andStorOpexVar
- the capacity includes only a variable OPEX.
EnergyModelsBase
provides although union types for simplifying providing new dispatch. These are EnergyModelsBase.UnionOpexFixed
, EnergyModelsBase.UnionOpexVar
, and EnergyModelsBase.UnionCapacity
.
Introduced type and its fields
The RefStorage
node is implemented as a reference node that can be used for a Storage
. It includes basic functionalities common to most energy system optimization models.
The fields of a RefStorage
are given as:
id
:
The fieldid
is only used for providing a name to the node.charge::AbstractStorageParameters
:
More information can be found on storage parameters.level::UnionCapacity
:
The level storage parameters must include a capacity. More information can be found on storage parameters.Permitted values for storage parameters in `charge` and `level` If the node should contain investments through the application of
EnergyModelsInvestments
, it is important to note that you can only useFixedProfile
orStrategicProfile
for the capacity, but notRepresentativeProfile
orOperationalProfile
. Similarly, you can only useFixedProfile
orStrategicProfile
for the fixed OPEX, but notRepresentativeProfile
orOperationalProfile
. The variable operating expenses can be provided asOperationalProfile
as well. In addition, all capacity and fixed OPEX values have to be non-negative.stor_res::ResourceEmit
:
Thestor_res
is the storedResource
.input::Dict{<:Resource,<:Real}
andoutput::Dict{<:Resource,<:Real}
:
Both fields describe theinput
andoutput
Resource
s with their corresponding conversion factors as dictionaries. It is not necessary to specify the storedResource
(outlined above), but it is in general advisable.
All values have to be non-negative.Ratios for Storage In the current implementation, we do not consider
output
conversion factors for the outflow from theRefStorage
node. Similarly, we do not consider theinput
conversion factor of the stored resource. Instead, it is assumed that there is no loss of the stored resource in the storage.data::Vector{Data}
:
An entry for providing additional data to the model. In the current version, it is used for both providingEmissionsData
and additional investment data whenEnergyModelsInvestments
is used.Note The field
data
is not required as we include a constructor when the value is excluded.
RefStorage
nodes do not include a discharge capacity or corresponding operating expenses. Instead, it is possible to empty the storage within a single operational period. If you need to specify a discharge capacity (or want to implement it as a ratio to the charge capacity), you have to create a new Storage
type. This is explain on Advanced creation of new nodes.
In practice, the key change would be to provide an additional field called discharge
to the new Storage
type.
Mathematical description
In the following mathematical equations, we use the name for variables and functions used in the model. Variables are in general represented as
$\texttt{var\_example}[index_1, index_2]$
with square brackets, while functions are represented as
$func\_example(index_1, index_2)$
with paranthesis.
Variables
The variables of Storage
s include:
- $\texttt{opex\_var}$
- $\texttt{opex\_fixed}$
- $\texttt{stor\_level\_inst}$
- $\texttt{stor\_level}$
- $\texttt{stor\_charge\_inst}$ if the
Storage
has the fieldcharge
with a capacity - $\texttt{stor\_charge\_use}$
- $\texttt{stor\_discharge\_inst}$ if the
Storage
has the fielddischarge
with a capacity - $\texttt{stor\_discharge\_use}$
- $\texttt{flow\_in}$
- $\texttt{flow\_out}$
- $\texttt{stor\_level\_Δ\_op}$
- $\texttt{stor\_level\_Δ\_rp}$ if the
TimeStruct
includesRepresentativePeriods
- $\texttt{emissions\_node}$ if specified through the function
has_emissions
or if you use aRefStorage{AccumulatingEmissions}
.
Constraints
A qualitative overview of the individual constraints can be found on Constraint functions. This section focuses instead on the mathematical description of the individual constraints. It omits the direction inclusion of the vector of network nodes (or all nodes, if nothing specific is implemented). Instead, it is implicitly assumed that the constraints are valid $\forall n ∈ N^{\text{Storage}}$ for all Storage
types if not stated differently. In addition, all constraints are valid $\forall t \in T$ (that is in all operational periods) or $\forall t_{inv} \in T^{Inv}$ (that is in all investment periods).
The following standard constraints are implemented for a Storage
node. Storage
nodes utilize the declared method for all nodes 𝒩. The constraint functions are called within the function create_node
. Hence, if you do not have to call additional functions, but only plan to include a method for one of the existing functions, you do not have to specify a new create_node
method.
constraints_capacity
:\[\begin{aligned} \texttt{stor\_level\_use}[n, t] & \leq \texttt{stor\_level\_inst}[n, t] \\ \texttt{stor\_charge\_use}[n, t] & \leq \texttt{stor\_charge\_inst}[n, t] \\ \texttt{stor\_discharge\_use}[n, t] & \leq \texttt{stor\_discharge\_inst}[n, t] \end{aligned}\]
constraints_capacity_installed
:\[\begin{aligned} \texttt{stor\_level\_inst}[n, t] & = capacity(level(n), t) \\ \texttt{stor\_charge\_inst}[n, t] & = capacity(charge(n), t) \\ \texttt{stor\_discharge\_inst}[n, t] & = capacity(discharge(n), t) \end{aligned}\]
Using investments The function
constraints_capacity_installed
is also used inEnergyModelsInvestments
to incorporate the potential for investment. Nodes with investments are then no longer constrained by the parameter capacity.constraints_flow_in
:
The auxiliary resource constraints are independent of the chosen storage behavior:\[\texttt{flow\_in}[n, t, p] = inputs(n, p) \times \texttt{flow\_in}[n, stor\_res(n)] \qquad \forall p \in inputs(n) \setminus \{stor\_res(n)\}\]
The stored resource constraints are depending on the chosen storage behavior. If no behavior is specified, it is given by
\[\texttt{flow\_in}[n, t, stor\_res(n)] = \texttt{stor\_charge\_use}[n, t]\]
If the storage behavior is
AccumulatingEmissions
, it is given by\[\texttt{flow\_in}[n, t, stor\_res(n)] = \texttt{stor\_charge\_use}[n, t] - \texttt{emissions\_node}[n, t, stor\_res(n)]\]
This allows the storage node to provide a soft constraint for emissions.
constraints_flow_out
:\[\texttt{flow\_out}[n, t, stor\_res(n)] = \texttt{stor\_discharge\_use}[n, t]\]
Behavior in the case of `AccumulatingEmissions` In this case, the constraints are still declared. The variables are however fixed to 0. Hence, it will have no impact.
constraints_level
:
The level constraints are more complex compared to the standard constraints. They are explained in detail below in Level constraints.constraints_opex_fixed
:\[\begin{aligned} \texttt{opex\_fixed}&[n, t_{inv}] = \\ & opex\_fixed(level(n), t_{inv}) \times \texttt{stor\_level\_inst}[n, first(t_{inv})] + \\ & opex\_fixed(charge(n), t_{inv}) \times \texttt{stor\_charge\_inst}[n, first(t_{inv})] + \\ & opex\_fixed(discharge(n), t_{inv}) \times \texttt{stor\_discharge\_inst}[n, first(t_{inv})] \end{aligned}\]
Why do we use `first()` The variables $\texttt{stor\_level\_inst}$ are declared over all operational periods (see the section on Capacity variables for further explanations). Hence, we use the function $first(t_{inv})$ to retrieve the installed capacities in the first operational period of a given investment period $t_{inv}$ in the function
constraints_opex_fixed
.constraints_opex_var
:\[\begin{aligned} \texttt{opex\_var}&[n, t_{inv}] = \\ \sum_{t \in t_{inv}}& opex\_var(level(n), t) \times \texttt{stor\_level}[n, t] \times scale\_op\_sp(t_{inv}, t) + \\ & opex\_var(charge(n), t) \times \texttt{stor\_charge\_use}[n, t] \times scale\_op\_sp(t_{inv}, t) + \\ & opex\_var(discharge(n), t) \times \texttt{stor\_discharge\_use}[n, t] \times scale\_op\_sp(t_{inv}, t) \end{aligned}\]
The function `scale_op_sp` The function $scale\_op\_sp(t_{inv}, t)$ 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.
constraints_data
:
This function is only called for specified data of the storage node, see above.
The capacity constraints, both constraints_capacity
and constraints_capacity_installed
are only set for capacities that are included through the corresponding field and if the corresponding storage parameters have a field capacity
. Otherwise, they are omitted. The field level
is required to have a storage parameter with capacity.
Even if a Storage
node includes the corresponding capacity field (i.e., charge
, level
, and discharge
), we only include the fixed and variable OPEX constribution for the different capacities if the corresponding storage parameters have a field opex_fixed
and opex_var
, respectively. Otherwise, they are omitted.
Level constraints
The overall structure is outlined on Constraint functions. The level constraints are called through the function constraints_level
which then calls additional functions depending on the chosen time structure (whether it includes representative periods and/or operational scenarios) and the chosen storage behaviour.
The constraints introduced in constraints_level_aux
are given by
\[\texttt{stor\_level\_Δ\_op}[n, t] = \texttt{stor\_charge\_use}[n, t] - \texttt{stor\_discharge\_use}[n, t]\]
corresponding to the change in the storage level in an operational period. If the storage behavior is AccumulatingEmissions
, it is instead given by
\[\texttt{stor\_level\_Δ\_op}[n, t] = \texttt{stor\_charge\_use}[n, t]\]
In this case, we also fix variables and provide lower bounds:
\[\begin{aligned} & \texttt{emissions\_node}[n, t, stor\_res(n)] \geq 0 \\ & \texttt{emissions\_node}[n, t, p_{em}] = 0 \qquad & \forall p_{em} \in P^{em} \setminus \{stor\_res(n)\} \\ & \texttt{stor\_level\_Δ\_op}[n, t] \geq 0 \\ & \texttt{stor\_discharge\_use}[n, t] = 0 \\ & \texttt{flow\_out}[n, t, p] = 0 \qquad & \forall p \in output(n) \end{aligned}\]
If the time structure includes representative periods, we calculate the change of the storage level in each representative period within the function constraints_level_iterate
:
\[\texttt{stor\_level\_Δ\_rp}[n, t_{rp}] = \sum_{t \in t_{rp}} \texttt{stor\_level\_Δ\_op}[n, t] \times scale_op_sp(t_{rp}, t)\]
In the case of CyclicStrategic
, we add an additional constraint to the change in the function constraints_level_rp
:
\[\sum_{t_{rp} \in T^{rp}} \texttt{stor\_level\_Δ\_rp}[n, t_{rp}] = 0\]
while we fix the value in the case of CyclicRepresentative
to 0:
\[\texttt{stor\_level\_Δ\_rp}[n, t_{rp}] = 0\]
Accumulating
storage behaviors do not add any constraint for the variable $\texttt{stor\_level\_Δ\_rp}$.
If the time structure includes operational scenarios using CyclicRepresentative
, we enforce that the last value in each operational scenario is the same within the function constraints_level_scp
.
The general level constraint is eventually calculated in the function constraints_level_iterate
:
\[\texttt{stor\_level}[n, t] = prev\_level + \texttt{stor\_level\_Δ\_op}[n, t] \times duration(t)\]
in which the value $prev\_level$ is depending on the type of the previous operational ($t_{prev}$) and strategic level ($t_{inv,prev}$) (as well as the previous representative period ($t_{rp,prev}$)). It is calculated through the function previous_level
.
We can distinguish the following cases:
The first operational period (in the first representative period) in an investment period (given by $typeof(t_{prev}) = typeof(t_{rp, prev}) = = nothing$). In this situation, the previous level is dependent on the chosen storage behavior. In the default case of a
Cyclic
behaviors, it is given by the last operational period of either the strategic or representative period:\[\begin{aligned} prev\_level & = \texttt{stor\_level}[n, last(t_{sp})] prev\_level & = \texttt{stor\_level}[n, last(t_{rp})] \end{aligned}\]
If the storage behavior is instead given by
CyclicStrategic
and the time structure includes representative periods, we calculate the previous level instead as:\[\begin{aligned} t_{rp,last} = & last(repr\_periods(t_{sp})) \\ prev\_level = & \texttt{stor\_level}[n, first(t_{rp,last})] - \\ & \texttt{stor\_level\_Δ\_op}[n, first(t_{rp,last})] \times duration(first(t_{rp,last})) + \\ & \texttt{stor\_level\_Δ\_rp}[n, t_{rp,last}] \end{aligned}\]
$t_{rp,last}$ corresponds in this situation to the last representative period in the current investment period.
If the storage behavior is instead given by
CyclicStrategic
, the previous level is set to 0:\[prev\_level = 0\]
The first operational period in subsequent representative periods in any investment period (given by $typeof(t_{prev}) = nothing$). The previous level is again dependent on the chosen storage behavior. The default approach calculates it as:
\[\begin{aligned} prev\_level = & \texttt{stor\_level}[n, first(t_{rp,prev})] - \\ & \texttt{stor\_level\_Δ\_op}[n, first(t_{rp,prev})] \times duration(first(t_{rp,prev})) + \\ & \texttt{stor\_level\_Δ\_rp}[n, t_{rp,prev}] \end{aligned}\]
while a
CyclicRepresentative
storage behavior calculates it as:\[prev\_level = \texttt{stor\_level}[n, last(t_{rp})]\]
This situation only occurs in cases in which the time structure includes representative periods.
All other operational periods:
\[ prev\_level = \texttt{stor\_level}[n, t_{prev}]\]