Previously we described the proposed modeling of a CES. However, we did not specify how to implement the described functions and how to calculate possible flow constellations. Therefore we design a calculation procedure that utilizes the recursive structure of the CES. Another goal for the creation of the general principle is that we are able to realize different control strategies without major changes.
Discrete time step simulation
The flows determined via the calc_f() and calc_z() functions calculate the flows for a given time step. However, we are interested in the system’s behavior over time. The main idea in the CES simulation is to recalculate the flows in fixed time steps. The time between any recalculations should be chosen such that the overall computation time of the simulation is not too expensive, but the calculations are accurate enough. The recalculations are managed by a sequencer that initializes the flow balance calculation from the top EC iteratively. At first, it advances one time step in the simulation by updating the SoC of the batteries and statistics. After that, the flows will be reset and then recalculated on the new status of the system by invoking calc_z() at the top EC.
Hierarchical Controller routine
For an HC, we have to implement two functions calc_z() and calc_f(). Both maintain a variable balance defined as:
$$\begin{aligned} \textit{balance} = \sum (\textit{incoming flows}) - \sum (\textit{outgoing flows}) \end{aligned}$$
(3)
At the end of a calculation step, the balance has to be 0 to guarantee that there is no energy excess or demand.
Since calc_z() (see Algorithm 1) is the first function called per component during a time step, it is responsible for preparing its children by calling calc_z(). The resulting imbalance is redirected to the caller to ensure that \(\textit{balance} = 0\). However, the current flows could be optimized (e.g., the energy should be managed as locally as possible), whereas calc_z() calls calc_f() requesting no incoming or outgoing flow. If this can be achieved, the EC can work fully self-sufficient.
The function calc_f() (see Algorithm 2) starts with a possible constellation of incoming and outgoing flows fulfilling that \(\textit{balance} = 0\). However, they are probably not optimal as the energy may be managed more locally, so the function tries to improve them. It considers the initial request and tries to fulfill it by requesting the children. The order and magnitude are determined by the strategy. The effects on the balance are then redirected to the caller.
Figure 4 illustrates the idea behind the two functions when applied to a small system consisting of an HC and a consumer, a producer, and a storage unit attached. We are interested in how the system behaves when it tries to be as self-sufficient as possible. Therefore we start with the calculation of calc_z() at the top HC. At first, the controller requests the original flow from each component (figure above). Then, the consumer and producer return their production or demand. The imbalance can be temporarily redirected to the caller of the HC. After that, the controller asks its children to adjust their flow to a new value. Only the battery in this example can store the surplus (figure below).
Local Controller routine
Similar to the HC, we can realize the calc_z() and calc_neigh_z() functions as shown in Algorithm 3 and Algorithm 4. For both, we have to prepare the only children, and for calc_z() we have to additionally take care of the neighbors. The imbalance in calc_neigh_z(), however, is redirected to the overlying EC and not to the neighbor, as flows between neighbors are only determined via calc_neigh_f(). In any case, we have to ensure that an LC is only prepared once per time step, as the initialization can both be initiated by a parent or neighbor.
For calc_f() (see Algorithm 5) it is our goal to handle the request at first only by the usage of the subordinate EC, as we want to handle the energy as local as possible. Only then the neighbors are considered, in a similar manner as for the HC. The calc_neigh_f() function (see Algorithm 6) first checks, if calc_f() has already been called from the top. If not, the flows have not been sent to the overlying EC. Then, if applicable, we can redirect parts of these flows to the neighbor. Afterwards, the controller tries to resolve the initial request with the child EC.
Minimum/Maximum request estimation
As mentioned in the definition of the Interfaces, we can determine flow estimators with the meaning r_min as well as r_max describing a minimum and maximum energy flow an EC can offer. Note that while these values are perhaps not exact, they are still very helpful for estimating the possible capacity of an EC which can be used for control strategies.
For consumers and producers, the corresponding flows are fixed, which means that sending special requests would not change the flows. Therefore, the calculation of the r_min and r_max flows can be omitted here. For our storage unit models, we set minimum and maximum flows in the incoming as well as outgoing directions. However, also more complex battery models can be implemented which consider external factors such as the working temperature.
The capacity of controllers is dependent on their subordinate ECs. For the r_min flow, we only have to be aware of the subordinate EC with the lowest corresponding r_min information: we can redirect all energy to or from this EC. When we calculate the maximum possible flow, we consider the left buffer (e.g. difference between the r_max estimator and the current flow) for each subordinate EC. The sum of those buffers indicates how much power we can redirect additionally into the current EC. Therefore this sum can be added to the existing incoming or outgoing flow to obtain the maximum request. For LCs we only consider the subordinate EC but not the neighbors, as we do not see neighbors as part of the current EC.
Strategies
Until now we did not specify how exactly the requests are sent to the subordinate ECs (in case of the HC) or the neighbors (in case of the LC). Depending on the chosen strategy the behavior of the system can differ vastly. In this paper, we present three simple strategies with different goals:
-
Greedy One simple strategy to determine the requests is the Greedy strategy: after calculating the initial flows from and to the attached ECs, the controller tries to compensate for the complete current energy surplus or shortage using only one calc_f() call by sending a corresponding request to a child or neighbor. Ideally, the current EC can directly fulfill the current request. Otherwise, the caller has to ask another EC or let the initial request remain unsatisfied.
-
Equal request As introduced earlier, we calculate flows of the type \(\textit{r\_max}\). Those flows give an estimation of how capable an EC is to answer a request. For example, a high value for this estimation indicates that this EC consists of many components and can therefore also provide a large amount of energy. We use this information to distribute the request equally to all neighbors but weighted according to the \(\textit{r\_max}\) values. Therefore, we ask each candidate once, evaluate its answer, rearrange the new target request and go on with the next EC. We further divided this strategy into two variants:
-
In variant 1, we split the difference between the initial request and the actual flow. For every child or neighbor, we use as weighting, which buffer is between the current flow and the corresponding \(\textit{r\_max}\) flow. The achieved result for the next EC is then added to the existing flows to obtain the request flows.
-
In variant 2, we redistribute the total request. Since we reorganize the complete request, we do not consider the buffer, but instead, the complete possible requests given by the \(\textit{r\_max}\) flows. The achieved result for the next EC is then considered as the new request.
-
Equal SoC In the context of this strategy, we look at some observations from Abgottspon et al. (2018). They analyze how to control a Battery Energy Storage System (BESS) and observe many parameters. One of them is the heterogeneity, describing how different the storage components regarding parameters like size or charging/discharging speed are. One result of the paper is that a homogeneous BESS can usually perform better than a heterogeneous one. The goal of our approach is to keep the SoC of the batteries on a similar level. Therefore we use variant 2 from the Equal Request strategy. Variant 1 cannot be easily adapted, as it requires knowing what part of the maximum possible flow is already used. We use the entire free storage of all attached child batteries for charging and saved energy for discharging as weighting. Due to the modular structure of the framework, further strategies are possible, also ones that consider and/or control not only the current controller but also a complete subtree. Apart from it, possible thinkable is to establish a market in which the components participate, e.g. by setting a price a controller has to pay for demanding a certain request.