Time integration in modules works by calling the time integration functions of the child SUs (which will eventually end up in the time integration functions of Cells or SPM cells). Time integration can take multiple time steps at once, which means we allow every child SU to take this number of time steps on its own before returning to the module code. So if we want to take 10 time steps in a series module, we first take 10 time steps in SU1, then 10 steps in SU2, etc. until we have taken 10 time steps in the last SU.
Currently multithreading option is not working but when it works time integration of the different child SUs will be completely independent of each other, so a new thread is created per child SU, and the child SU is integrated over N time steps on that separate thread. When each child has completed its time integrations, the code returns to the main thread. If an error happens during time integration on a separate thread, a flag is set in the Module, such that after returning to the main thread we know something went wrong and can deal with it appropriately.
Multithreading does not make sense for small modules (modules made of cells) since the overhead to create a thread, share memory locations, etc. is much larger than the computational requirement of integrating a single cell for 10 steps. However, if you have hierarchical modules (modules made of modules made of … made of cells), then using the multithreading at the ‘outer' module will speed up the calculation dramatically (since 10 time steps of its child SUs might involve taking 10 time steps on hundreds or thousands of cells).
This function defines a parallel-connected module. This means that the voltage of all child SUs has to be the same (or within a defined tolerance), while the module current is the sum of the currents to each child SU.
Contact resistances are in parallel branches in the module. The terminals of the parallel module are before the first cell as indicated below. This means that the entire module current goes through R1, the current through R2 is the sum of the currents through SU2, SU3 and SU4. Similarly for all resistances. Note that an SU will have its own resistance (which will be in series with the OCV of the SUs). In reality, there might also be resistances at the other side of the module (i.e. on the bottom horizontal line in the circuit below), but these can be lumped into the resistances on top since exactly the same current will pass through them. Note that cell currents are always the total current passing through that cell (i.e. the sum of the current to the module terminal and the current to the other cells to balance the voltages).
To ensure that the voltages of the child-SUs are always the same, the code follows a sort of PI-controller behaviour. If the voltage of one SU is too large, the code will increase its current (i.e. charge less or discharge more) and decrease the current of the SU with the smallest voltage with the same amount. This ensures that the total module current remains the same (because current is swapped) and that the voltages converge over time. This implementation was found to be much faster and more robust than explicitly solving the equations to equalise the voltage.
There are three functions with a significant functionality:
redistributeCurrent: as the name implies, this function will redistribute the current flowing in the total module to the different child SUs in order to end up at the same voltages. This function works a bit like a PI controller, if the voltage of a cell is too large, its current is increased (i.e. the cell current is increased by a fraction which is a function of the error on the voltage). To ensure the total module current remains the same, the opposite current change is done to the cell with the lowest voltage. Note that it is not exactly a PI controller, because the amount of current changed is not directly a function of the voltage error. This is necessary because the OCV curve is very steep at some points while almost flat at others (so the same change in current will have a very different effect on the voltage depending on where in the OCV curve you are), cell resistances are not constant, and different cells can be at different states (e.g. a module with 1 degraded cell and 4 ‘good ones', then the same current change in the degraded cell will have a much larger effect than the same change in a ‘good' cell. Instead, the amount of current swapped between the cells with the highest and lowest voltage is a small fraction of the cell current. The fraction is small enough to ensure stability in almost all cases, but it does mean that if the error is large you will need a few iterations to reach the appropriate correction. The fraction will however decrease if the voltage error decreases, but according to a pre-defined step-wise approach, which ensures convergence and stability in almost all cases.
setCurrent: This is the function to change the module current, which obviously needs to be done carefully. The approach in this function is simple: allocate the current uniformly and then call redistributeCurrent()
to equalise the voltages. This works well if the differences between the child SUs are small, but it might fail for large differences (e.g. one very degraded cell with 8 good ones, then giving 1/9th of the total current to the degraded cell might push it over its voltage limit). In this case, we swap to the approach from the function below (setI_iterative). setI_Iterative is currently removed and setCurrent is equipped with a better redistributeCurrent_new()
.
Time integration in parallel modules is similar to series modules, i.e. you can take N time steps at once, which is done separately for each child SU potentially on different threads. The voltage equalisation is only done after N steps (i.e. only after time integration we call redistributeCurrent), so the voltage constrained is not imposed between those N steps.
The Battery class represent a container. It has one module (which has all cells connected to it), an inverter to convert the variable DC voltage to a fixed AC voltage, and an HVAC cooling system.
In most ways it behaves like a module, and most functions are direct pass-throughs (i.e. simply invoked on the Module). Note in particular that the getI() and getV() functions return the values of the Module, i.e. the (variable) DC voltage and DC current. This must be so because Battery is a type of StorageUnit, so e.g. we must be able to charge at a given current to a given voltage (and if getV returns the constant AC voltage, then the charge will never stop).
The converter is an instance of the class Converter, and the only function it has is to calculate the losses in the converter. These losses are not taken into account when calculating the current and voltage (which as said are the DC current and voltage) but they are stored internally such that if desired, the efficiency from the AC side can be calculated. The heat generated by the converter losses is included in the thermal model of the Battery.
The thermal model of the Battery is very similar to that of a Module, with three differences:
HVAC Coolsystem
. It is like a coolsystem, but with the addition of an AC unit which can exchange heat with the environment. A conventional coolsystem must be cooled by the parent of the module to which the coolsystem is connected. Since a Battery does not have a parent, this coolsystem must be able to cool itself. See Coolsystem_HVAC for more info.CoolSystem is the base-class for all cooling systems. Coolsystems are used by Modules to cool the child-SUs connected to the module. Coolsystems have a thermal inertia, coolant flow rate and cross-section, all of which scales linearly with the number of Cells ultimately connected to the module. The flow rate and cross section determine the speed of the coolant, which in turn determines the convective coolant constant to the child SUs.
CoolSystems can control the flow rate (e.g. by controlling a fan), which will increase or reduce the cooling to the child SUs. They also keep track of the operational power they require, which is the energy required to speed up the coolant to the given speed over the given cross section. Currently, 5 control strategies are implemented:
if this Coolsystem
is at the bottom layer (i.e. its Module
has cells), this is the same as strategy 2. But if this Module is a higher level one (i.e. its child SUs are also Modules), then strategy 2 acts based on the temperature of the Modules while 3 goes to the Cells. The local control (i.e. 2 and 4) must use the temperature of the children and not the temperature of the coolsystem because if the coolsystem is off, it won't cool the cells so it won't heat up itself. Therefore if we would use the temperature of the coolsystem, it would never switch on and the child SUs would overheat.
A Coolsystem
only cools the child SUs and must be cooled by the Coolsystem
of the parent of the Module to which it is connected. I.e. they only extract heat "from below" but accumulate it in their own thermal mass until the layer "above" them will extract their heat. Therefore, the ‘top level Module' (i.e. one without a parent) cannot have a Coolsystem, since it would just keep heating up. This top level must have an HVAC_Coolsystem (see below).
The Coolant in the coolsystem has a temperature, which is the temperature of the Module (or rather, the temperature of the module is the temperature of its coolsystem). The temperature will change due to heat exchange with the child SUs, neighbouring Modules and parent Module.
CoolSystems store usage statistics of their temperature, operating power, flow rate and the amount of heat they evacuated from their children.
This is a simplification of the Coolsystem, basically by saying there open space between the parent of this Module and its children (i.e. the children of the Module are cooled by the CoolSystem of the parent of this module). Therefore, it can only be used by ‘middle level' Modules (i.e. Modules with both children and a parent).
The coolsystem acts like an ‘aggregator' of the child-SUs by having a large convective cooling constant. Therefore, it will heat up to the same T as the children, be cooled by the parent, and then pass on this cooling to the children.
An open coolsystem does not require operating power (since it has no fan).
This child-class extends a conventional CoolSystem
and must be used by the top-level Module (or a Battery), which does not have a parent. Therefore, this coolsystem can cool itself by exchanging heat with the environment.
It has an AC unit to cool down itself from the environment. The amount of cooling we get from the environment can be controlled similar to the controls of the fan (but note that here we control the cooling power, not some flow rate). The heat exchange with the environment can happen in two ways:
On top of all the convention data storage, HVAC systems also store the heat exchange with the environment (i.e. the cooling power from the AC unit) and the operating power of the AC unit. Note that these are stored separately to the heat extracted from the child SUs and the operating power of the fan to cool the child SUs.
Cycler
implements functions to load a StorageUnit
with a constant current or constant voltage.
There is a question about what to do with voltage limits of potential children in the SU. E.g. if the SU of the Cycler
is a series-module, should we stop when one cell in the module reaches a voltage limit (Vmax or Vmin), or do we keep going until the entire module has reached its voltage limit? This is decided by the fied diagnostic
, which is a Boolean. If it is true, cycling will be interrupted when an individual cell's voltage limit is reached. This might be preferable, e.g. if you are simply fully charging and discharging a large battery, then you probably are almost fully charged when the first cell reaches its voltage limit so you can just carry on discharging. Therefore, no fault is thrown but the CC or CV function is simply terminated (and reports that this is what happens).
If it is false, you don't check the voltage limits of individual cells during cycling. However, when a cell reaches a VMIN
or VMAX
safety limit, a ‘hard fault' is thrown (an error is thrown by the cell, which is simply passed on and will probably cause the code to crash unless higher-level functions deal with it).
The main function in Cycler is CC. It has an auxiliary function to apply the specified current to the SU (which will detect if this will exceed a voltage limit and terminate immediately if it does). Otherwise, CC will keep integrating over time until the specified voltage or time limit is reached. As mentioned, if diagnostic is true, then you will also stop as soon as one cell in the StorageUnit reaches its limit.
The function CC also controls the number of time steps taken at once (see Module_s). It will start with one at a time, but if we are still far from any limit, it will be increased to a maximum of 10. As we approach the set limit, we will reduce it back to 1. There are a number of issues to bear in mind in relation to this N
(number of time steps taken at once):
It is therefore suggested that during intermediate simulations, you can leave N at 10. But when you need stable results over the entire lifetime (i.e. after the knee point), you should reduce N, potentially even keep it at 1. The simulation will be much slower (easily 5 times slower) but it might be worth it in these cases.
There is a function to do a CV, but that function should only be used for individual cells and never for compound modules (it isn't very robust or accurate). Cycler also has a simple function to measure the capacity of a cell by doing a CC cycle at low current.
Procedure is a very loose class (as in, it has got very few class variables), it more a grouping of functions. In general, Procedure implements degradation procedures as you would program them on a battery tester. There are three types of functions:
Degradation experiments
Auxiliary functions
check-up: done both for Cells and Modules. Completely analogous so I only explain the Cells one
Check-ups take quite some time to do (because you want to know the capacity of every cell of the entire battery). So from the degradation function, you make a copy of the total battery, start a new thread and do the checkup on the copy on the separate thread. This way, the main degradation function can continue straight away. [Note you do need to use some mutex to ensure two concurrent check-ups don't write data at the same time to the same file].
The following files are written (where xxx is a user-defined prefix): Data storage functions are updated and we currently do not use seperators.