Safe fast charging with aging
Battery management strategies are typically developed and validated under beginning-of-life (BOL) conditions, but the real-world performance of these strategies often deteriorates under ageing conditions. A key challenge is evaluating how control strategies (e.g., fast charge protocols) behave as the cell ages. This is especially difficult when considering the numerous ways a customer might use their device, meaning the ageing mechanisms induced can vary wildly from person to person.
Current limitation: Exhaustive cycling-based validation is costly and time-intensive. Without a way to screen strategies for ageing behaviour quickly, poor-performing strategies may only be identified after expensive testing campaigns. Alternatively, fast charging might only be validated in limited number of ageing conditions, meaning blind spots to the risk of elevated plating might be present in edge case or even normal use case scenarios which were not tested for.
Problem statement
We need a method to screen control strategies across multiple degradation scenarios (e.g., high calendar ageing, high-temp exposure, manufacturing quality issues, frequent fast charging) without relying entirely on full-cycle validation. The goal is to evaluate how strategies respond when the cell is aged due to:
- Calendar ageing
- Cycling ageing
- Anode delamination (manufacturing quality issues)
- Frequent fast charge cycling
- High-temperature exposure
Approach Using Breathe Model
With the Breathe Model, we can screen a fast charge protocol’s robustness under this range of ageing conditions. This is possible with Breathe Model’s capability to adjust degradation modes:
- LLI: Loss of Lithium Inventory
- LAM_NE: Loss of Active Material, of the Negative Electrode
- LAM_PE: Loss of Active Material, of the Positive Electrode
Setting these values adjusts the Breathe Model thermodynamics and kinetics accordingly, and can be done:
- Programmatically: by declaring LLI, LAMNE, and LAMPE values in your workspace
- Graphically: opening the Breathe Model block in Simulink and entering values for each degradation mode.
With this feature, we will simulate fast charge behaviour of the cell with the following degradation mode settings:
clc, clear % refresh workspace
% Set the model parameter file number BMJN
bmjn = 1000;
% Define ageing scenarios
ageingScenarios = [
struct('name', 'Calendar Ageing', 'LLI', 0.30, 'LAM_anode', 0.05, 'LAM_cathode', 0.05),
struct('name', 'Cycling Ageing', 'LLI', 0.30, 'LAM_anode', 0.23, 'LAM_cathode', 0.23),
struct('name', 'Anode Delamination', 'LLI', 0.05, 'LAM_anode', 0.30, 'LAM_cathode', 0.05),
struct('name', 'Frequent Fast Charge Cycling', 'LLI', 0.30, 'LAM_anode', 0.18, 'LAM_cathode', 0.12),
struct('name', 'High-Temperature Exposure', 'LLI', 0.30, 'LAM_anode', 0.10, 'LAM_cathode', 0.10)
]
We will identify if any of the settings demonstrate an elevated risk of plating.
We will then use the insight offered into electrode OCV’s Breathe Model provides to identify the best route for improvement.
Finally, we will develop a new fast charge protocol which alleviates the risk of plating without trade off to performance.
Define the simulation initial and terminating conditions
% Initial conditions
initiConds.z0 = 0.1; % Initial SoC [-] (0.0 <= z0 <= 1.0)
initiConds.T0 = 25; % Initial cell temperature [degC] (0 <= T0 <= 50)
% Set the thermal boundary condition
thermLoad.Tamb = 25; % Ambient temperature [degC] (0 <= Tamb <= 50)
thermLoad.htc = 35; % Assumed heat transfer coefficient [W/m^2/K]
% Terminating conditions
termCondition.Vmin = 2.50; % Lower cell voltage end condition [V]
termCondition.Vmax_safety = 4.25; % Upper cell voltage end condition [V]
termCondition.minSoC = 0.10; % Lower state of charge end condition [-]
termCondition.maxSoC = 0.80; % Upper state of charge end condition [-]
termCondition.maxTemperature = 60.0; % Upper cell temperature end condition [degC]
Using the baseline fast charging strategy
A hypothetical cell supplier has provided the following fast charge strategy with their cell:
| # | SoC breakpoint | Charing C-rate |
| 1 | 0.10 | 2.9 |
| 2 | 0.60 | 2.7 |
| 3 | 0.65 | 1.3 |
| 4 | 0.70 | 0.7 |
Define the fast charge strategy:
chargeLUT.socBps = [0.1 0.6 0.65 0.70]; % SoC steps for the fast step-charge C-rates
chargeLUT.lutData = [2.9 2.7 1.3 0.7]; % The fast charge C-rates
Call the Breathe Model step-charge simulation, each time initialising a different LLI, LAM_NE, and LAM_PE value depending on the ageing scenario.
The simulation will model fast charging from 10% to 80% SoC at 25°C.
[simOutResults, timeCharge_baseline, tempMax_baseline] = Run_Charge_Simulation(initiConds,chargeLUT,thermLoad,termCondition,ageingScenarios);
Running simulation: Calendar Ageing
Running simulation: Cycling Ageing
Running simulation: Anode Delamination
Running simulation: Frequent Fast Charge Cycling
Running simulation: High-Temperature Exposure
Insight:
In the majority of ageing scenarios, the anode potential never falls below the threshold of elevated plating risk of < 0.03 V(shaded in red). However, in the case of Anode Delamination, it is apparent that the elevated risk to plating is present. This is insightful as it is not straightforward to induce this scenario (loss of anode active material, LAM_NE, without notable loss of lithium inventory, LLI). However, this is one that manufacturing quality issues could cause and therefore having a resilient charging strategy to all grades of cell quality builds upon the layers of safety of a device.
Derating the charging protocol to not encroach on the safety margin
Based on the previous performance, we can adjust the fast charge strategy. The troublesome region to address is between SoC 0.55 and 0.70.
The original charging table:
| # | SoC breakpoint | Charing C-rate |
| 1 | 0.10 | 2.9 |
| 2 | 0.60 | 2.7 |
| 3 | 0.65 | 1.3 |
| 4 | 0.70 | 0.7 |
Investigating electrode OCV's
Due to Breathe Model being a physics-based model, we can see how the changes to the degradation modes impact the cell's electrode potentials. The change to the shape of the anode OCV potential, as a function of state of charge, can provide insight into how we must adapt the charging strategy to avoid crossing the safety margin threshold with our charge protocol.
We can load the simulation results from the first run, as we saved them, and load the ocvAnode signal:
% iteratively load and plot over each ageing scenario:
for i = 1:numel(ageingScenarios)
% Extract scenario name
scenario = ageingScenarios(i);
% load results
simData = simOutResults{i}.logsout.extractTimetable;
% plot
plot(simData.("<socModel>"),simData.("<ocvAnode>"), DisplayName = scenario.name), hold on; grid on;
end
% format axis
ylabel("Anode OCV [V]","FontWeight","bold")
xlabel("State of charge [-]","FontWeight","bold")
xlim([0.1, 0.8])
ylim([0, 0.3])
legend()
This provides the insight needed for us to adapt our state of charge breakpoints. We can see that in the Anode Delamination case the phase transitions of the electrode occur earlier in the state of charge window (where we see steep gradients between the stable plateaus). The most critical of which is the lower anode potential between SoC 0.55 to 0.80. We can account for this in our updated fast charge protocol by implementing a step prior to this phase change, at 0.50, and then another at 0.60.
Therefore, the proposed revisions to derate the charging protocol, to not exceed the safety margin are as follows:
- Reduce the SoC window of step #1 to: 0.10 to 0.50.
- Reduce the SoC window of step #2 to: 0.50 to 0.55.
- Reduce the C-rate of step #3 to 1.3C in the range of 0.55 to 0.70.
Which results in the derated charging table:
| # | SoC breakpoint | Charing C-rate |
| 1 | 0.10 | 2.9 |
| 2 | 0.50 | 2.7 |
| 3 | 0.55 | 1.3 |
| 4 | 0.70 | 0.7 |
Note, this is 'derated' because it is known that this will be slower than the original. The intent is to demonstrate these reductions avoid the anode potential safety margin threshold, so that changes to the C-rates of each step (increases) can be made to bring the charging speed back in line with the original protocol.
Simulating the intermediary charge protocol
Defining the look up strategy:
chargeLUT.socBps = [0.10 0.50 0.55 0.70]; % SoC steps for the fast step-charge C-rates
chargeLUT.lutData = [2.9 2.7 1.3 0.7]; % The fast charge C-rates
[~, timeCharge_derate, tempMax_derate] = Run_Charge_Simulation(initiConds,chargeLUT,thermLoad,termCondition,ageingScenarios);
Running simulation: Calendar Ageing
Running simulation: Cycling Ageing
Running simulation: Anode Delamination
Running simulation: Frequent Fast Charge Cycling
Running simulation: High-Temperature Exposure
This is a great result, it shows that in all ageing scenarios the reduced fast charge protocol remains outside of the plating safety margin, meaning there is never an excessive risk to lithium plating.
We can now check the fast charge time to see how much time is lost with these reduced fast charging windows:
% plot charging speed before and after
bar([timeCharge_baseline;timeCharge_derate]), hold on; grid on;
% format plot axis
xticklabels({"Baseline Protocol", "Derated Protocol \newlinewith Breathe Model"})
ylabel("10-80% Charge time [minutes]","FontWeight","bold")
xlabel("Charging Strategy","FontWeight","bold")
ylim([0 30])
% add legend
legend({ageingScenarios.name}, Location="southwest")
Requirement: Charging time is no slower after protecting from excessive risk of lithium plating (i.e., "Derated Protocol with Breathe Model").
Result: Fail - The updated charging strategy with Breathe Model is ~1.5 minutes (~15%) slower after derating the original step protocol.
Developing a zero-compromise fast charge protocol
From the derated protocol, we can review the anode potential profile to devise where increases to the charging rates can be made:
| # | SoC breakpoint | Charing C-rate |
| 1 | 0.10 | 2.9 |
| 2 | 0.50 | 2.7 |
| 3 | 0.55 | 1.3 |
| 4 | 0.70 | 0.7 |
Analysis of the derated anode potential charging traces results in the following changes to improve charging speed:
Step #1 is underutilised,
- indicated by the anode potential having a notable margin to the safety threshold.
- Therefore a 3.4% increase to the C-rate is applied (2.9C to 3.0C).
- This has the added benefit of heating the cell slightly faster, making the cell less susceptible to plating (higher anode potential under the same charging rate) than if it was slightly colder.
- This remains within the cell specification of charging rates up to 5C.
Step #2 is maintained as it is already on the threshold
- Same C-rate (2.7C).
Step #3 has a large gradient in anode potential, indicating that the cell is underutilised.
- Between SoC 0.55 to 0.60 there is a sharp gradient in anode potential during the step.
- As a result, this has been split up into two steps with these SoC windows.
The new Step #3 charges at a faster rate to make use of the underutilised anode potential.
- From 0.55 to 0.60, C-rate has been increased by 23% from 1.3C to 1.6C
Step #4 retains the same C-rate to leave a larger gap to the safety threshold at the higher SoCs
- Same C-rate (1.3C) of the derated Step #3 it derives from.
Step #5 is also largely underutilised, apparent in the large gap to the anode potential threshold.
- An increase in C-rate from 0.7C to 0.9C has been applied.
- Plating is likelier at higher states of charge due to the favourable kinetics of a highly-lithiated anode particle for plating. Therefore, it is sensible to leave a larger gap to the safety threshold here than at the lower states of charge.
The optimised protocol is:
| # | SoC breakpoint | Charing C-rate |
| 1 | 0.10 | 3.0 |
| 2 | 0.50 | 2.7 |
| 3 | 0.55 | 1.6 |
| 4 | 0.60 | 1.3 |
| 5 | 0.70 | 0.9 |
Note - this was done with some trial an error to find the appropriate C-rates which do not exceed the threshold. This can be done with more sophistication using closed loop control.
Simulating the updated charge protocol
Defining the look up strategy:
chargeLUT.socBps = [0.10 0.50 0.55 0.60 0.70]; % SoC steps for the fast step-charge C-rates
chargeLUT.lutData = [3.0 2.7 1.6 1.3 0.9]'; % The fast charge C-rates
Simulating over every ageing scenario:
[simOutResults, timeCharge_optimised, tempMax_optimised] = Run_Charge_Simulation(initiConds,chargeLUT,thermLoad,termCondition,ageingScenarios);
Running simulation: Calendar Ageing
Running simulation: Cycling Ageing
Running simulation: Anode Delamination
Running simulation: Frequent Fast Charge Cycling
Running simulation: High-Temperature Exposure
The new charging protocol successfully avoids encroaching on the plating limit threshold, even in the most aggressive of ageing scenarios and while using higher C-rates than the derated protocol.
Baseline and optimised charge KPI comparison
Charging speed from 10-80%
% Plot charging speed before and after
figure
bar([timeCharge_baseline;timeCharge_optimised]); grid on;
% Format plot axis
xticklabels({"Baseline Protocol", "Optimised Protocol \newlinewith Breathe Model"})
ylabel("10-80% Charge time [minutes]","FontWeight","bold")
xlabel("Charging Strategy","FontWeight","bold")
ylim([0 30])
% Add legend
legend({ageingScenarios.name}, Location="southwest")
Requirement: Charging time is no slower after protecting from excessive risk of lithium plating (i.e., "Optimised Protocol with Breathe Model").
Result: Pass - The updated charging strategy with Breathe Model has no loss in charge time from 10-80%, while avoiding encroaching on the anode potential safety margi
Maximum charging temperature
% Plot charging speed before and after
figure
bar([tempMax_baseline;tempMax_optimised]), grid on;
% Format plot axis
xticklabels({"Baseline Protocol", "Optimised Protocol \newlinewith Breathe Model"})
ylabel("Maximum charging temperature [°C]","FontWeight","bold")
xlabel("Charging Strategy","FontWeight","bold")
ylim([25 40])
% Add legend
legend({ageingScenarios.name}, Location="northeast")
Requirement: Charging time is no hotter after protecting from excessive risk of lithium plating (i.e., "Optimised Protocol with Breathe Model").
Result: Pass - The updated charging strategy with Breathe Model does not have a higher maximum temperature, meaning there is low risk to compatibility with the battery's existing thermal environment.
Utility functions
function [simDetails, cellDetails] = Sim_Cell_Details(bmjn)
%Load parameter metadata
paramJsonFile = fileread(sprintf("BM_Parameters/BMJN_%d.json", bmjn));
paramMetaData = jsondecode(paramJsonFile).Meta;
% Simulink file name and block names
simDetails.simuLinkMdlStr = "Demo_AgeingFC_LUT_Application";
% Cell characteristics (These values are specified in the Breathe Model block mask help)
cellDetails.nominalCapacity_Ah = paramMetaData.nominalCap_Ah;
cellDetails.nominalEnergy_Wh = paramMetaData.nominalEne_Wh;
cellDetails.areaCell_m2 = paramMetaData.areaCell_m2;
% Model version
simDetails.modelVersion = paramMetaData.modelVersion;
end
function [simOutResults,timeCharge,tempMax] = Run_Charge_Simulation(initiConds,chargeLUT,thermLoad,termCondition,ageingScenarios,nvp)
% Simulate step charge profile for each ageing scenario
arguments
initiConds struct
chargeLUT struct
thermLoad struct
termCondition struct
ageingScenarios
nvp.bmjn double = 1000;
end
[simDetails, cellDetails] = Sim_Cell_Details(nvp.bmjn);
simuLinkMdlStr = simDetails.simuLinkMdlStr;
modelObj = Simulink.SimulationInput(simuLinkMdlStr);
% Initialise
numScenarios = length(ageingScenarios);
simOutResults = cell(1, numScenarios);
timeCharge = zeros(1, numScenarios);
tempMax = zeros(1, numScenarios);
% Set initial condition variables
modelObj = modelObj.setVariable('z0', initiConds.z0, 'Workspace', simuLinkMdlStr);
modelObj = modelObj.setVariable('T0', initiConds.T0, 'Workspace', simuLinkMdlStr);
% Set thermal boundary condition variables
modelObj = modelObj.setVariable('Tamb', thermLoad.Tamb, 'Workspace', simuLinkMdlStr);
modelObj = modelObj.setVariable('htc', thermLoad.htc, 'Workspace', simuLinkMdlStr);
modelObj = modelObj.setVariable('areaCell', cellDetails.areaCell_m2, 'Workspace', simuLinkMdlStr);
% Set charge LUT variables
modelObj = modelObj.setVariable('socBps', chargeLUT.socBps, 'Workspace', simuLinkMdlStr);
lutData = chargeLUT.lutData*cellDetails.nominalCapacity_Ah; % The fast charge currents (written as C-rates multiplied by cell capacity)
modelObj = modelObj.setVariable('lutData', lutData, 'Workspace', simuLinkMdlStr);
% Set terminating condition variables
modelObj = modelObj.setVariable('Vmin', termCondition.Vmin, 'Workspace', simuLinkMdlStr);
modelObj = modelObj.setVariable('Vmax_safety', termCondition.Vmax_safety, 'Workspace', simuLinkMdlStr);
modelObj = modelObj.setVariable('minSoC', termCondition.minSoC, 'Workspace', simuLinkMdlStr);
modelObj = modelObj.setVariable('maxSoC', termCondition.maxSoC, 'Workspace', simuLinkMdlStr);
modelObj = modelObj.setVariable('maxTemperature', termCondition.maxTemperature, 'Workspace', simuLinkMdlStr);
figure
for ii = 1:numScenarios
% Extract ageing values
scenario = ageingScenarios(ii);
modelObj = modelObj.setVariable('LLI', scenario.LLI, 'Workspace', simuLinkMdlStr);
modelObj = modelObj.setVariable('LAMNE', scenario.LAM_anode, 'Workspace', simuLinkMdlStr);
modelObj = modelObj.setVariable('LAMPE', scenario.LAM_cathode, 'Workspace', simuLinkMdlStr);
modelObj = modelObj.setModelParameter('StopTime','inf'); % Set simuation options
% Optional: Display which scenario is running
fprintf('Running simulation: %s\n', scenario.name);
% Run simulation
simOut = sim(modelObj);
% Store results
simOutResults{ii} = simOut;
% Plot
simData = simOutResults{ii}.logsout.extractTimetable;
plot(simData.("<socModel>"),simData.("<voltAnode>"), DisplayName = scenario.name); hold on; grid on;
timeCharge(ii) = minutes(simData.Time(end)); % get charge time from initial SoC to terminating SoC
tempMax(ii) = max(simData.("<tempSurfaceModel>"));
end
formatWithSafetyMargin()
close_system(simuLinkMdlStr,1)
end
function formatWithSafetyMargin(nvp)
% Adds a visual safety margin to a lithium-ion cell plot
arguments
nvp.SafetyThreshold {mustBeNumeric, mustBeScalarOrEmpty} = 0.03
nvp.WarningColor (1,3) {mustBeNumeric} = [0.8745 0.0706 0.0706]
nvp.EdgeColor {mustBeTextScalar} = 'blend';
nvp.Alpha {mustBePositive,mustBeLessThanOrEqual(nvp.Alpha,1)} = 0.1
nvp.XLimits = [0.1, 0.8]
nvp.YLimits = [0.0, 0.15]
end
% Extract parameters
safetyThreshold = nvp.SafetyThreshold;
colorWarning = nvp.WarningColor;
alphaValue = nvp.Alpha;
xLimits = nvp.XLimits;
yLimits = nvp.YLimits;
regionName = 'Region of elevated plating risk';
thresholdName = 'Threshold of elevated plating risk';
% Calculate blended colour (mixing with white background)
backgroundColour = [1 1 1]; % Assuming white background
actualEdgeColour = colorWarning * alphaValue + backgroundColour * (1 - alphaValue);
% Hold current plot to add elements
hold on;
% Define the x range for threshold line
thresholdX = linspace(xLimits(1), xLimits(2), 100);
% Define y = safety threshold (the boundary for shading)
thresholdY = safetyThreshold * ones(size(thresholdX));
% Create a filled area (shading) from threshold downwards
fill([thresholdX, fliplr(thresholdX)], [thresholdY, zeros(size(thresholdX))], ...
colorWarning, 'FaceAlpha', alphaValue, ...
'EdgeColor', actualEdgeColour, ...
'DisplayName', regionName);
% Plot the boundary line at y=threshold
plot(thresholdX, thresholdY, '-', 'LineWidth', 1.5, ...
'Color', colorWarning, ...
'DisplayName', thresholdName);
% Format plot figure for easy viewing of anode potential traces
% Set axis limits
xlim(xLimits);
ylim(yLimits);
% Set axis labels
xlabel('State of charge [-]','FontWeight','bold');
ylabel('Anode potential [V]','FontWeight','bold');
% Set legend
legend();
% Release hold
hold off;
end