Skip to content

ECM Generation using Breathe Model

The challenge: Resource Intensive Process of ECM Generation

The majority of control-oriented algorithms for batteries, such as state estimation and fault detection, utilise equivalent circuit model (ECM) models due to their simplified linear parameter-varying model structure. However, despite this model simplicity, constructing accurate ECM models presents significant resource challenges.

At beginning-of-life: ECM parameters vary considerably with operating conditions including cell temperature, state of charge (SoC), and current rates. This variation requires extensive experimental characterisation using pulse power tests at numerous breakpoints, consuming significant cycler channel resources and time (typically a week-long experimental campaign).

At aged states: The challenge becomes even more complex when considering battery ageing. ECM parameters strongly depend on the ageing state, and including this ageing dependency can be crucial for applications such as SOC estimators that rely on accurately updating ECM parameters based on current ageing state information from SOH estimators. The experimental approach to gather characterisation data across ageing states is prohibitively costly, as it requires performing pulse tests at all necessary breakpoints across multiple cells undergoing different ageing experiments at numerous degradation levels.

The solution: Breathe Model ECMs

Breathe Model eliminates these costly experimental processes entirely, engineers can use Breathe Model to generate synthetic characterisation data instantly and at no cost. Since Breathe Model has been extensively parameterised and validated across a broad range of temperatures, state of charge levels, and current rates, the synthetic data generated delivers the same reliability as real experimental data.

For aged states, Breathe Model enables users to generate synthetic pulse test data at any combination of operating conditions and ageing states characterised by 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

ECM parameterisation involves two key steps:

  • First, the equilibrium behaviour is characterised by the OCV as a function of SoC. The generation of synthetic OCV data for specified degradation modes is described in Section Generate OCV Curves.
  • Second, dynamic parameters are determined using pulse tests under required operating conditions and ageing states. The generation of synthetic pulse test data for specified operating conditions and degradation modes is outlined in Section Generate Synthetic Pulse Data. The estimation of ECM parameters from this synthetic data is explained in Section Estimate ECM Parameters From Pulse Data.

Generate OCV Curves

In this section, we generate and plot OCV curves with hysteresis at the ageing state specified by the degradation modes.

clc, clear % Clear command window and refresh workspace

% Set the model parameter file number BMJN
bmjn = 1000;

% Specify the ageing state through degradation mode values
lamne = 0;  % Loss of active material in negative electrode (zero at BOL)
lampe = 0;  % Loss of active material in positive electrode (zero at BOL)
lli = 0;    % Loss of lithium inventory (zero at BOL)

% Generate OCV curves including hysteresis effects
ocvTable = Construct_Full_Cell_OCV(bmjn, "lamne", lamne, "lampe", lampe, "lli", lli);

% Plot the three OCV curves: discharge, mean, and charge
plot(ocvTable.refSoC, [ocvTable.refOCVDischarge, ocvTable.meanOCV, ocvTable.refOCVCharge], '.-');
grid on;
xlabel("SoC [%]");
ylabel("OCV [V]");
legend("Discharge OCV", "Mean OCV", "Charge OCV", Location="best")
figure_0.png

The figure shows the discharge, mean and charge OCV curves of the cell at the specified degradation modes.

Generate Synthetic Pulse Data

In this section, we simulate pulse tests using Breathe PBM at a set of operating conditions and aged state specified in the JSON file.

% JSON file specifying pulse test conditions (SoC, temperature, etc.)
filename = "PulseSettings.json";
jsonString = fileread(filename);

The parameters in the JSON file that describe the operating conditions, aged state, and settings at which pulse tests are simulated are provided below. Users can modify these values based on their requirements.

  1. "ambientTemps_degC": Array of ambient temperatures in degrees Celsius at which to simulate pulse tests
  2. "C_rates": Array of C-rates for the pulse tests. Positive values indicate charge pulses
  3. "SoCs:discharge": Array of SoC levels for discharge pulse tests
  4. "SoCs:charge": Array of SoC levels for charge pulse tests
  5. "t_rest_before_sec": Scalar value for rest time in seconds before applying the pulse
  6. "t_duration_sec": Scalar value for pulse duration in seconds
  7. "t_rest_after_sec": Scalar value for rest time in seconds after the pulse
  8. "lli": Scalar value for loss of lithium inventory degradation mode (0 indicates no degradation)
  9. "lamne": Scalar value for loss of active material at the negative electrode (0 indicates no degradation)
  10. "lampe": Scalar value for loss of active material at the positive electrode (0 indicates no degradation)
  11. "heatTransCoeff_SI": Scalar value for convective heat transfer coefficient in SI units
  12. "cellModel": String specifying the name of Breathe PBM block.
%Print the validated range of C-rates and ambient temperatures
PulseTestEmulator.showModelOperatingRanges(bmjn)
Cell name: Moli_P45B
Temperature Validation Range: 0.0°C to 50.0°C
C-Rate Validation Range: -10.0 to 5.0

Let's take a look at the settings chosen in the current JSON file

pulseSettings = jsondecode(jsonString);
disp(pulseSettings)
    ambientTemps_degC: [4x1 double]
              C_rates: [2x1 double]
                 SoCs: [1x1 struct]
    t_rest_before_sec: 1
       t_duration_sec: 15
     t_rest_after_sec: 3600
                  lli: 0
                lamne: 0
                lampe: 0
    heatTransCoeff_SI: 35
            cellModel: 'Breathe_Model'
%Ambient temperatures chosen
ambientTemeperatures = pulseSettings.ambientTemps_degC
ambientTemeperatures = 4x1    
     0
    15
    30
    50
%SOC points to do charge pulses
pulseSoCs_charge = pulseSettings.SoCs.charge
pulseSoCs_charge = 6x1    
         0
    0.1000
    0.2000
    0.4000
    0.6000
    0.8000
%SOC points to do discharge pulses
pulseSoCs_discharge = pulseSettings.SoCs.discharge
pulseSoCs_discharge = 6x1    
1.0000
    0.8000
    0.6000
    0.4000
    0.2000
    0.1000

We now proceed to simulate the pulse test using the operating conditions defined in the JSON configuration.

% Build pulse test emulator object
pulseTestEmulatorObj = PulseTestEmulator(filename,bmjn);
% Generate the simulink model
pulseTestEmulatorObj.Build_Simulink_Model(); 
Model saved to: D:\GIT\Breathe_Simulate_Demo\Breathe_Model_Demo\Breathe_Model_Subpackages\BM_Functions\slx_models\PBM2ECM.slx
% Execute the simulations
pulseTestEmulatorObj.Initialise_Simulink_Model();
pulseTestEmulatorObj.Print_Number_Of_Experiments()
Number of simulations to run: 48
pulseTestEmulatorObj.Run_Simulation();
[25-Aug-2025 18:44:07] Checking for availability of parallel pool...
[25-Aug-2025 18:44:07] Starting Simulink on parallel workers...
[25-Aug-2025 18:44:08] Configuring simulation cache folder on parallel workers...
[25-Aug-2025 18:44:08] Loading model on parallel workers...
[25-Aug-2025 18:44:09] Running simulations...
[25-Aug-2025 18:44:15] Completed 1 of 48 simulation runs
[25-Aug-2025 18:44:15] Completed 2 of 48 simulation runs
[25-Aug-2025 18:44:16] Completed 3 of 48 simulation runs
[25-Aug-2025 18:44:16] Completed 4 of 48 simulation runs
[25-Aug-2025 18:44:16] Completed 5 of 48 simulation runs
[25-Aug-2025 18:44:16] Completed 6 of 48 simulation runs
[25-Aug-2025 18:44:16] Completed 7 of 48 simulation runs
[25-Aug-2025 18:44:16] Completed 8 of 48 simulation runs
[25-Aug-2025 18:44:16] Completed 9 of 48 simulation runs
[25-Aug-2025 18:44:16] Completed 10 of 48 simulation runs
[25-Aug-2025 18:44:16] Completed 11 of 48 simulation runs
[25-Aug-2025 18:44:16] Completed 12 of 48 simulation runs
[25-Aug-2025 18:44:16] Completed 13 of 48 simulation runs
[25-Aug-2025 18:44:16] Completed 14 of 48 simulation runs
[25-Aug-2025 18:44:20] Completed 15 of 48 simulation runs
[25-Aug-2025 18:44:20] Completed 16 of 48 simulation runs
[25-Aug-2025 18:44:20] Completed 17 of 48 simulation runs
[25-Aug-2025 18:44:20] Completed 18 of 48 simulation runs
[25-Aug-2025 18:44:20] Completed 19 of 48 simulation runs
[25-Aug-2025 18:44:20] Completed 20 of 48 simulation runs
[25-Aug-2025 18:44:20] Completed 21 of 48 simulation runs
[25-Aug-2025 18:44:20] Completed 22 of 48 simulation runs
[25-Aug-2025 18:44:21] Completed 23 of 48 simulation runs
[25-Aug-2025 18:44:21] Completed 24 of 48 simulation runs
[25-Aug-2025 18:44:21] Completed 25 of 48 simulation runs
[25-Aug-2025 18:44:21] Completed 26 of 48 simulation runs
[25-Aug-2025 18:44:21] Completed 27 of 48 simulation runs
[25-Aug-2025 18:44:21] Completed 28 of 48 simulation runs
[25-Aug-2025 18:44:24] Completed 29 of 48 simulation runs
[25-Aug-2025 18:44:25] Completed 30 of 48 simulation runs
[25-Aug-2025 18:44:25] Completed 31 of 48 simulation runs
[25-Aug-2025 18:44:25] Completed 32 of 48 simulation runs
[25-Aug-2025 18:44:25] Completed 33 of 48 simulation runs
[25-Aug-2025 18:44:25] Completed 34 of 48 simulation runs
[25-Aug-2025 18:44:25] Completed 35 of 48 simulation runs
[25-Aug-2025 18:44:26] Completed 36 of 48 simulation runs
[25-Aug-2025 18:44:26] Completed 37 of 48 simulation runs
[25-Aug-2025 18:44:26] Completed 38 of 48 simulation runs
[25-Aug-2025 18:44:26] Completed 39 of 48 simulation runs
[25-Aug-2025 18:44:26] Completed 40 of 48 simulation runs
[25-Aug-2025 18:44:26] Completed 41 of 48 simulation runs
[25-Aug-2025 18:44:26] Completed 42 of 48 simulation runs
[25-Aug-2025 18:44:28] Completed 43 of 48 simulation runs
[25-Aug-2025 18:44:28] Completed 44 of 48 simulation runs
[25-Aug-2025 18:44:28] Completed 45 of 48 simulation runs
[25-Aug-2025 18:44:28] Completed 46 of 48 simulation runs
[25-Aug-2025 18:44:28] Completed 47 of 48 simulation runs
[25-Aug-2025 18:44:28] Completed 48 of 48 simulation runs
[25-Aug-2025 18:44:28] Cleaning up parallel workers...
% Retrieve the results from the pulseTestEmulatorObjulator as a structured variable
pulseTestResult = pulseTestEmulatorObj.Get_Results_Struct();

Save and Visualise Pulse Data

We will now save the simulation results as a structured variable and plot the result at one of the setting.

% Save the results a .mat file
result.pulseTest = pulseTestResult;
result.ocv = ocvTable;
save('SyntheticData.mat', 'result');

% Plot the result of pulse test using the first setting from the JSON file.
% Users can specify different conditions to plot any simulated test case.
% Ensure that the specified test conditions exist in the JSON file.
% If the specified condition was not simulated, an error will be raised.

firstCrate = pulseSettings.C_rates(1);
firstTamb = pulseSettings.ambientTemps_degC(1);

if firstCrate>0
    firstSoC = pulseSettings.SoCs.charge(1);
else
    firstSoC = pulseSettings.SoCs.discharge(1);
end

pulseTestEmulatorObj.plotBatteryCondition('c_rate', firstCrate,'T_amb', firstTamb,'soc', firstSoC)
figure_1.png

Estimate ECM Parameters From Pulse Data

We estimate ECM parameters from synthetic pulse data using an optimisation routine. The model order is set by the initial guess thetaInit, which must follow the format [R0; R1; ...; Rn; τ1; ...; τn] for an n-order model. The estimator is created with EcmParamEstimator and run via Run_Parameterisation(). Results are stored as charge and discharge lookup tables over SoC, temperature, and C-rate.

% Define initial guess for ECM parameters
% Follow this format: [R0; R1; R2; ...; Rn; tau1; tau2; ...; taun]
% For a 1st-order ECM (1 RC pair): [R0; Rp1; tau1]
thetaInit = [0.01;  0.005;  50];   

modelOrder = (numel(thetaInit)-1)/2;
fprintf('ECM model order = %d\n', modelOrder);
ECM model order = 1
% Define lower and upper bounds matching the format and length of thetaInit.
% Ensure all elements are non-negative and each upper bound is greater than 
% its corresponding lower bound.

thetaLB   = [0.001; 0;    0.1;];
thetaUB   = [0.3;   0.3;  1000 ];

% Create ECM parameter estimator object using synthetic pulse data and parameter bounds
ecmParamEstObj = EcmParamEstimator(pulseTestEmulatorObj, thetaInit, thetaLB, thetaUB);

% Run the parameterisation to optimise R0, Rp1, and tau1 over all simulated cases
ecmParamEstObj.Run_Parameterisation();
Optimising: charge | T0 | CRate_1 | soc_0 : Success
Optimising: charge | T0 | CRate_1 | soc_0p1 : Success
Optimising: charge | T0 | CRate_1 | soc_0p2 : Success
Optimising: charge | T0 | CRate_1 | soc_0p4 : Success
Optimising: charge | T0 | CRate_1 | soc_0p6 : Success
Optimising: charge | T0 | CRate_1 | soc_0p8 : Success
Optimising: charge | T15 | CRate_1 | soc_0 : Success
Optimising: charge | T15 | CRate_1 | soc_0p1 : Success
Optimising: charge | T15 | CRate_1 | soc_0p2 : Success
Optimising: charge | T15 | CRate_1 | soc_0p4 : Success
Optimising: charge | T15 | CRate_1 | soc_0p6 : Success
Optimising: charge | T15 | CRate_1 | soc_0p8 : Success
Optimising: charge | T30 | CRate_1 | soc_0 : Success
Optimising: charge | T30 | CRate_1 | soc_0p1 : Success
Optimising: charge | T30 | CRate_1 | soc_0p2 : Success
Optimising: charge | T30 | CRate_1 | soc_0p4 : Success
Optimising: charge | T30 | CRate_1 | soc_0p6 : Success
Optimising: charge | T30 | CRate_1 | soc_0p8 : Success
Optimising: charge | T50 | CRate_1 | soc_0 : Success
Optimising: charge | T50 | CRate_1 | soc_0p1 : Success
Optimising: charge | T50 | CRate_1 | soc_0p2 : Success
Optimising: charge | T50 | CRate_1 | soc_0p4 : Success
Optimising: charge | T50 | CRate_1 | soc_0p6 : Success
Optimising: charge | T50 | CRate_1 | soc_0p8 : Success
Optimising: discharge | T0 | CRate_1 | soc_0p1 : Success
Optimising: discharge | T0 | CRate_1 | soc_0p2 : Success
Optimising: discharge | T0 | CRate_1 | soc_0p4 : Success
Optimising: discharge | T0 | CRate_1 | soc_0p6 : Success
Optimising: discharge | T0 | CRate_1 | soc_0p8 : Success
Optimising: discharge | T0 | CRate_1 | soc_1 : Success
Optimising: discharge | T15 | CRate_1 | soc_0p1 : Success
Optimising: discharge | T15 | CRate_1 | soc_0p2 : Success
Optimising: discharge | T15 | CRate_1 | soc_0p4 : Success
Optimising: discharge | T15 | CRate_1 | soc_0p6 : Success
Optimising: discharge | T15 | CRate_1 | soc_0p8 : Success
Optimising: discharge | T15 | CRate_1 | soc_1 : Success
Optimising: discharge | T30 | CRate_1 | soc_0p1 : Success
Optimising: discharge | T30 | CRate_1 | soc_0p2 : Success
Optimising: discharge | T30 | CRate_1 | soc_0p4 : Success
Optimising: discharge | T30 | CRate_1 | soc_0p6 : Success
Optimising: discharge | T30 | CRate_1 | soc_0p8 : Success
Optimising: discharge | T30 | CRate_1 | soc_1 : Success
Optimising: discharge | T50 | CRate_1 | soc_0p1 : Success
Optimising: discharge | T50 | CRate_1 | soc_0p2 : Success
Optimising: discharge | T50 | CRate_1 | soc_0p4 : Success
Optimising: discharge | T50 | CRate_1 | soc_0p6 : Success
Optimising: discharge | T50 | CRate_1 | soc_0p8 : Success
Optimising: discharge | T50 | CRate_1 | soc_1 : Success

After the optimisation, the 3D lookup tables for the estimated ECM parameters, along with the corresponding SoC, C-rate, and temperature breakpoints and the fitted voltage traces, are available in the ecmOptParameters property of the estimator object. These are stored separately for charge and discharge conditions as the charge and discharge fields, respectively.

% MATLAB struct containing lookup tables, breakpoints, and voltage fits 
% for both charge and discharge directions.
chargeECMParamLUT = ecmParamEstObj.ecmOptParameters.charge
chargeECMParamLUT = 
      dim1_socBPs: [6x1 double]
     dim2_tempBPs: [4x1 double]
    dim3_crateBPs: 1
     lookUpTables: [1x1 struct]
dischargeECMParamLUT = ecmParamEstObj.ecmOptParameters.discharge
dischargeECMParamLUT = 
      dim1_socBPs: [6x1 double]
     dim2_tempBPs: [4x1 double]
    dim3_crateBPs: -1
     lookUpTables: [1x1 struct]

Save and Visualise ECM Tables

We now save the results stored in ecmParamEstObj.ecmOptParameters into a MATLAB struct named ecmOptimalParameters, which contains the estimated ECM parameter lookup tables, breakpoints, and voltage fits for both charge and discharge directions.

% Save ECM parameter LUTs as a .mat file
ecmOptimalParameters.charge = chargeECMParamLUT;
ecmOptimalParameters.discharge = dischargeECMParamLUT;
save('EstimatedECM_LUTs.mat', 'ecmOptimalParameters');

Next we plot the lookup tables for all estimated parameters. Each plot corresponds to a specific C-rate, with SoC on the x-axis and parameter values shown for each temperature on the y-axis.

%% Plot ECM parameters estimated from charge pulse (first charge C-rate)
parameterNames = ecmParamEstObj.paramNames;
enumCrate = 1
enumCrate = 1
for pp = parameterNames
    figure()

        para_LUT = chargeECMParamLUT.lookUpTables.(pp)(:,:,enumCrate);
        socBPLUT = chargeECMParamLUT.dim1_socBPs;
        tempBPLUT = chargeECMParamLUT.dim2_tempBPs;
        cRate = chargeECMParamLUT.dim3_crateBPs(enumCrate);

        plot(socBPLUT,para_LUT,'-o'); grid on
        xlabel("SoC [-]"); ylabel("Charge "+ pp); legend(string(tempBPLUT)+"degC")
        title(sprintf("Charge C-rate: %1.1fC",cRate))

end
figure_2.png
figure_3.png
figure_4.png
%% Plot ECM parameters estimated from discharge pulse (first discharge C-rate)
parameterNames = ecmParamEstObj.paramNames;
enumCrate = 1;
for pp = parameterNames
    figure()  
        para_LUT = dischargeECMParamLUT.lookUpTables.(pp)(:,:,enumCrate);
        socBPLUT = dischargeECMParamLUT.dim1_socBPs;
        tempBPLUT = dischargeECMParamLUT.dim2_tempBPs;
        cRate = dischargeECMParamLUT.dim3_crateBPs(enumCrate);

        plot(socBPLUT,para_LUT,'-o'); grid on
        xlabel("SoC [-]"); ylabel("Discharge "+ pp); legend(string(tempBPLUT)+"degC")
        title(sprintf("Discharge C-rate: %1.1fC",cRate))
end
figure_5.png
figure_6.png
figure_7.png

Next, we plot the voltage profile for one selected pulse condition, showing both the PBM synthetic data and the corresponding ECM model fit.

% Select which pulse to plot by setting:

direction = "charge";  % "charge" for charge pulses, "discharge" for discharge pulses
socIdx   = 1;          % index of desired SoC breakpoint
tempIdx  = 1;          % index of desired temperature breakpoint
crateIdx = 1;          % index of desired C-rate breakpoint

%Ploting the ECM fit
lut = chargeECMParamLUT; 
if direction == "discharge", lut = dischargeECMParamLUT; end

if ~isempty(lut.lookUpTables.ecmModelVoltFits)
    socVal = lut.dim1_socBPs(socIdx);
    tempVal = lut.dim2_tempBPs(tempIdx);
    crateVal = lut.dim3_crateBPs(crateIdx);

    ft = lut.lookUpTables.ecmModelVoltFits{socIdx, tempIdx, crateIdx};
    figure;
    plot(ft.time, ft.voltagePhyModel, 'k-', 'LineWidth', 1.2); hold on;
    plot(ft.time, ft.voltageEcnFit, 'r--', 'LineWidth', 1.2);
    grid on; xlabel('Time [s]'); ylabel('Voltage [V]');
    legend('PBM synthetic', 'ECM fit', 'Location', 'best');
    title(sprintf('%s pulse: SoC=%.2f, Temp=%.1f°C, C-rate=%.2fC', ...
        direction, socVal, tempVal, crateVal));
end
figure_8.png