ThermalEnergyStorage

ThermalEnergyStorage works mostly like a RefStorage with the additional option to include thermal energy losses. For the ThermalEnergyStorage node, heat losses are additionally quantified through a heat loss factor that describes the amount of thermal energy that is lost in relation to the storage level of the respective operational period. The main difference to RefStorage is that these heat losses occur independently of the storage use, i.e., in every operational period unless the storage level is zero. In practice, this approach with a constant relative heat loss factor does not accurately represent heat loss mechanisms based on temperature difference between the inside and outside of the TES. However, it is a reasonable approximation that at least reflects the dependence of the SOC on the absolute losses of the storage.

StorageBehavior for ThermalEnergyStorage

ThermalEnergyStorage can only utilize Cyclic storage behaviors. When using RepresentativePeriods, this is furthermore reduced toCyclicRepresentative. The reason for this limitation is that we have not yet implemented heat losses over a representative period.

Introduced type and its fields

ThermalEnergyStorage is similar to a RefStorage. Hence, the majority of the fields are the same.

Standard fields

The standard fields are given as:

  • id :
    The field id is only used for providing a name to the storage. This is similar to the approach utilized in EnergyModelsBase.

  • charge::AbstractStorageParameters :
    The charging parameters of the ThermalEnergyStorage. Depending on the chosen type, the charge parameters can include variable OPEX, fixed OPEX, and/or a capacity. More information can be found on storage parameters.

  • level::UnionCapacity :
    The level parameters of the ThermalEnergyStorage. The level storage parameters must include a capacity. Depending on the chosen type, the level parameters can include in addition variable OPEX and/or fixed OPEX. 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 use FixedProfile or StrategicProfile for the capacity, but not RepresentativeProfile or OperationalProfile. Similarly, you can only use FixedProfile or StrategicProfile for the fixed OPEX, but not RepresentativeProfile or OperationalProfile. The variable operating expenses can be provided as OperationalProfile as well. In addition, all capacity and fixed OPEX values have to be non-negative.

  • stor_res::Resource :
    The stor_res is the stored Resource. In the case of a ThermalEnergyStorage, this resource should be a ResourceHeat.

  • input::Dict{<:Resource,<:Real} and output::Dict{<:Resource,<:Real}:
    Both fields describe the input and output Resources with their corresponding conversion factors as dictionaries. All values have to be non-negative.

    Conversion factors

    While the field input includes a proper conversion factor and may allow for multiple resources, the value of the output Resources is not relevant.

  • data::Vector{<:Data} :
    An entry for providing additional data to the model. In the current version, it is used for providing additional investment data when EnergyModelsInvestments is used.

    Note

    The field data is not required as we include a constructor when the value is excluded.

Additional fields

ThermalEnergyStorage nodes add one additional field compared to RefStorage nodes:

  • heat_loss_factor::Float64 :
    The heat loss factore describes the heat loss relative to the storage level. It corresponds to the loss between two operational periods for a given operational duration of 1 (see Utilize TimeStruct for an explanation).

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 ThermalEnergyStorage utilizes all standard variables from RefStorage, as described on the page Optimization variables:

Constraints

ThermalEnergyStorage nodes utilize in general the standard constraints described in Constraint functions for Storage nodes. ThermalEnergyStorage nodes utilize the declared method for all nodes 𝒩. The following standard constraints are implemented for a ThermalEnergyStorage node.

  • constraints_capacity:

    \[\begin{aligned} \texttt{stor\_level\_use}[n, t] & ≤ \texttt{stor\_level\_inst}[n, t] \\ \texttt{stor\_charge\_use}[n, t] & ≤ \texttt{stor\_charge\_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) \end{aligned}\]

    Using investments

    The function constraints_capacity_installed is also used in EnergyModelsInvestments 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 do not include an efficiency:

    \[\texttt{flow\_in}[n, t, stor\_res(n)] = \texttt{stor\_charge\_use}[n, t]\]

  • constraints_flow_out:

    \[\texttt{flow\_out}[n, t, stor\_res(n)] = \texttt{stor\_discharge\_use}[n, t]\]

  • 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 strategic 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) \end{aligned}\]

    The function `scale_op_sp`

    The function $scale\_op\_sp(t_{inv}, t)$ calculates the scaling factor between operational and strategic 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.

Implementation of capacity and OPEX

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.

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. Note: ThermalEnergyStorage only makes changes to the constraint_level_iteratefunction when CyclicStrategic is chosen as 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]\]

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\]

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) - prev\_level \times heat\_loss\_factor(n) \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.

Changes in ThermalEnergyStorage

This constraint is the only constraint adjusted by [ThermalEnergyStorage] nodes. All other functions and constraints are unchanged.

We can distinguish the following cases:

  1. The first operational period (in the first representative period) in a strategic 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 strategic period.

    If the storage behavior is instead given by CyclicStrategic, the previous level is set to 0:

    \[prev\_level = 0\]

  2. The first operational period in subsequent representative periods in any strategic 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.

  3. All other operational periods:

    \[ prev\_level = \texttt{stor\_level}[n, t_{prev}]\]