Smart Cargo E-Bike, Part 3 — Physics-Based Batteries: from Electrochemistry to a Real-Time ECM¶
Reading time ~30 min · runtime ~2 min on CPU (PyBaMM DFN solves in ~1 s).
In Part 1 the e-bike carried a 2-RC
equivalent-circuit battery: three ODEs (SOC, two RC overpotentials) and an
open-circuit-voltage curve. It is fast, it closed the energy audit, and it is
the right model to put inside a real-time vehicle simulation. But its
parameters (R0, R1, C1, …) are empirical knobs — where do their values
come from, and when does a lumped circuit stop being trustworthy?
This notebook answers that by climbing the battery-modelling hierarchy with PyBaMM: a first-principles Doyle–Fuller–Newman (DFN) electrochemical cell as the physical truth, a Single-Particle model (SPMe) as a physics-based reduced model, and the 2-RC ECM calibrated from the DFN. We will see the lithium concentration fields inside the electrodes, watch the cell age, and end knowing exactly when each fidelity level is the right tool.
Prerequisites. Part 1 (the calibrated e-bike model and its ECM battery). Requires
pybamm(pip install pybamm). We reuse helpers from the companion scriptebike_pybamm_battery.py.
1. The modelling question: what is a battery model for?¶
A cell is a device that trades chemical energy for electrical work by shuttling lithium between two porous electrodes through an electrolyte. Which of that physics you resolve depends on the question:
| Question | Model you need |
|---|---|
| Range / energy over a drive cycle, BMS state estimation, real-time control | ECM (0-D circuit) — µs/step, empirical |
| Terminal voltage under a hard transient (fast charge, cold start, high C-rate) | SPMe or DFN — resolves transport limits |
| Aging, lithium plating, safety margins, cell/pack design | DFN — resolves the internal fields that drive degradation |
The key relationship, which this notebook makes concrete, is that the high-fidelity model is how you earn the right to use the cheap one: you fit (or reduce) the DFN to get ECM parameters you can defend, then run the ECM in the vehicle model. This is the same "high-fidelity → ROM → system model" pattern as the CFD (Part 5) and multibody (Part 4) couplings — here the ladder just happens to be within one physical domain.
2. The governing equations, briefly (and why each matters)¶
Symbols — $c_s(r,x,t)$ solid Li concentration [mol m⁻³] (radius $r$ inside a particle at through-cell position $x$); $c_e(x,t)$ electrolyte concentration; $\phi_s,\phi_e$ solid/electrolyte potentials [V]; $j$ interfacial current density [A m⁻²]; $D_s,D_e$ diffusivities [m² s⁻¹]; $i_0$ exchange current; $\eta$ overpotential [V]; $F$ Faraday's constant.
(1) Solid diffusion — lithium diffuses in/out of spherical particles (Fick): $$ \frac{\partial c_s}{\partial t} = \frac{1}{r^2}\frac{\partial}{\partial r}\!\left(D_s\, r^2 \frac{\partial c_s}{\partial r}\right). \tag{1}$$ The particle surface concentration sets the local voltage; when you pull current faster than $D_s$ can replenish the surface, voltage sags even though the particle core is still full — that is a transport limit an ECM only mimics through its RC time constants.
(2) Butler–Volmer kinetics — the current across the electrode/electrolyte interface depends exponentially on overpotential: $$ j = i_0\left[\exp\!\Big(\tfrac{\alpha_a F}{RT}\eta\Big) - \exp\!\Big(-\tfrac{\alpha_c F}{RT}\eta\Big)\right]. \tag{2}$$ Dimensional check: $[\eta]=$ V and $F/RT \approx 39\ \mathrm{V^{-1}}$ at 298 K, so a ~36 mV overpotential doubles the forward rate (for $\alpha_a = 0.5$; $\ln 2/(\alpha_a F/RT) \approx 0.693/19.5\ \mathrm{V^{-1}}$) — kinetics are stiff and temperature-sensitive.
(3) Electrolyte transport + charge conservation close the system across the cell thickness. The DFN (a.k.a. P2D — pseudo-2-D) solves (1)–(3) everywhere: a radial dimension inside every particle and the through-cell dimension. The SPMe collapses each electrode to one representative particle plus an electrolyte correction — cheap, accurate at low–moderate C-rate. The ECM throws away all spatial structure and fits the terminal response with a resistor + RC branches.
3. Setup¶
import os, sys, time
import numpy as np
import matplotlib.pyplot as plt
import pybamm
sys.path.append(os.path.abspath("."))
from ebike_pybamm_battery import (
run_pybamm, extract_pseudo_ocv, ecm_voltage, calibrate_ecm,
ebike_current, ebike_current_holdout, rmse_mV, T_END,
)
np.random.seed(0)
print("pybamm", pybamm.__version__)
pybamm 26.7.1.0
4. An e-bike-representative load, solved three ways¶
We drive one representative cell (Chen 2020, a 5 Ah 21700 NMC/graphite cell) with a bench profile scaled to the Class-1 vehicle and designed for identifiability, then solve it with the DFN (truth), the SPMe (physics ROM), and the calibrated ECM.
Pack ↔ cell bookkeeping, stated once. The vehicle pack is 13S3P of these cells; Part 1 lumps each 3P group into one 15 Ah "cell", so cell current here = pack current / 3 (cruise ≈ 2 A ≈ 0.4C, climb peaks ≈ 4.8 A ≈ 1C), and group parameters = cell R/3, cell C×3. Every mV figure in this notebook is per cell — multiply by 13 for the pack (a 10 mV cell error is a 130 mV pack error, ~0.25% of the 53 V bus).
Why this profile and not the vehicle's own drive cycle? The vehicle cycle is a terrible identification signal: one pulse scale, no rest. This bench profile adds short pulses (probe the fast RC branch), long pulses (the slow one), a charge segment (breaks discharge-only degeneracy — note the Part-1 drivetrain has no regen path; this is a bench excitation, not vehicle telemetry), and a full-rest relaxation tail. Section 10's identifiability check shows what happens without this care.
# 0.25 s comparison grid: coarser (~1 s) sampling cannot see the fast
# charge-transfer branch (tau ~ 0.5 s) and the 2-RC fit silently degenerates
# to 1-RC -- try it in Exercise 1.
tg = np.linspace(0.0, T_END, 1520)
t_dfn, V_dfn, T_dfn, wall_dfn = run_pybamm(pybamm.lithium_ion.DFN, "DFN")
t_spm, V_spm, T_spm, wall_spm = run_pybamm(pybamm.lithium_ion.SPMe, "SPMe")
from scipy.interpolate import interp1d
Vd = interp1d(t_dfn, V_dfn, fill_value="extrapolate")(tg)
Vs = interp1d(t_spm, V_spm, fill_value="extrapolate")(tg)
# The OCV curve -- the ECM's dominant term -- is EXTRACTED from a slow DFN-family
# discharge, not hand-written. (A cubic guess used previously was +221 mV wrong
# at SOC 0.1; invisible in a high-SOC window, wrong everywhere else.)
ocv_fn = extract_pseudo_ocv()
print(f"pseudo-OCV: {float(ocv_fn(1.0)):.3f} V @ SOC 1.0, {float(ocv_fn(0.5)):.3f} V @ 0.5, "
f"{float(ocv_fn(0.1)):.3f} V @ 0.1")
p_cal, ident = calibrate_ecm(tg, Vd, ebike_current, ocv_fn)
V_ecm = ecm_voltage(p_cal, tg, ebike_current, ocv_fn)
print(f"fitted: R0={p_cal[0]*1e3:.2f} mΩ | R1={p_cal[1]*1e3:.2f} mΩ, C1={p_cal[2]:.0f} F "
f"(τ₁={p_cal[1]*p_cal[2]:.1f} s) | R2={p_cal[3]*1e3:.2f} mΩ, C2={p_cal[4]:.0f} F "
f"(τ₂={p_cal[3]*p_cal[4]:.2f} s) | soc₀={p_cal[5]:.4f} (fit, not eyeballed)")
print(f"identifiability: {ident}")
fig, ax = plt.subplots(1, 2, figsize=(13, 4.2))
ax[0].plot(tg, ebike_current(tg), color="0.4"); ax[0].set(xlabel="time (s)", ylabel="cell current (A, + = discharge)", title="Identification profile (one cell): multi-scale pulses, charge dip, rest tail")
ax[1].plot(tg, Vd, "k", lw=2, label="DFN (truth)")
ax[1].plot(tg, Vs, "--", color="tab:blue", lw=1.3, label=f"SPMe ROM ({rmse_mV(Vs,Vd):.1f} mV)")
ax[1].plot(tg, V_ecm, color="tab:green", lw=1.3, label=f"2-RC ECM fit-to-DFN ({rmse_mV(V_ecm,Vd):.1f} mV, in-sample)")
ax[1].set(xlabel="time (s)", ylabel="terminal voltage (V, per cell)", title="Three fidelities under the same load"); ax[1].legend(fontsize=8)
fig.tight_layout(); plt.show()
DFN : solved in 3.05 s V 4.104->4.095 V T 25.00->27.53 C
SPMe : solved in 0.19 s V 4.104->4.095 V T 25.00->27.58 C pseudo-OCV: 4.171 V @ SOC 1.0, 3.743 V @ 0.5, 3.142 V @ 0.1
fitted: R0=15.18 mΩ | R1=9.01 mΩ, C1=2854 F (τ₁=25.7 s) | R2=7.68 mΩ, C2=61 F (τ₂=0.47 s) | soc₀=0.9720 (fit, not eyeballed) identifiability: both RC branches identified (distinct taus, no bound hits)
Held-out validation, and closing the loop with Part 1¶
The RMSE above is in-sample — the fit is judged on the profile it was fit to, which any overfit model passes. Two further checks make the calibration mean something. First, a held-out profile (different pulse periods, phases and amplitudes, same regime) the fit never saw. Second — the step most calibration exercises skip — the fitted values are compared against the group-level defaults the Part-1 vehicle model actually ships: those defaults were written from this fit, and this check fails loudly if either side drifts. A calibration that never reaches the model it calibrates is decoration.
# held-out profile
t_h, V_h, _, _ = run_pybamm(pybamm.lithium_ion.DFN, "DFN-h", current_fn=ebike_current_holdout)
Vh = interp1d(t_h, V_h, fill_value="extrapolate")(tg)
V_cal_h = ecm_voltage(p_cal, tg, ebike_current_holdout, ocv_fn)
print(f"2-RC ECM (fit to DFN): {rmse_mV(V_ecm, Vd):5.2f} mV in-sample, "
f"{rmse_mV(V_cal_h, Vh):5.2f} mV held-out")
# a hand-written baseline: datasheet-style parameters + cubic OCV
def _ocv_hand(s):
s = np.clip(s, 0.01, 0.99)
return 3.3 + 1.2*s - 0.5*s**2 + 0.15*s**3
p_guess = np.array([0.015, 0.010, 2000.0, 0.012, 10000.0, 0.95])
print(f"hand-written guess : {rmse_mV(ecm_voltage(p_guess, tg, ebike_current, _ocv_hand), Vd):5.2f} mV in-sample, "
f"{rmse_mV(ecm_voltage(p_guess, tg, ebike_current_holdout, _ocv_hand), Vh):5.2f} mV held-out")
# loop closure vs the shipped Part-1 defaults (group = cell/3 for R, x3 for C)
from ebike_hybrid_simulation import HighFidelityBatteryCellECM as _Cell
import inspect
sig = inspect.signature(_Cell.__init__).parameters
ship = {k: sig[k].default for k in ("R00", "R10", "C10", "R20", "C20")}
pairs = [("R00", ship["R00"], p_cal[0]/3), ("R10", ship["R10"], p_cal[1]/3),
("C10", ship["C10"], 3*p_cal[2]), ("R20", ship["R20"], p_cal[3]/3),
("C20", ship["C20"], 3*p_cal[4])]
print("\nPart-1 shipped defaults vs this fit (25% tolerance for fit-to-fit variation):")
for nm, a, b in pairs:
rel = abs(a-b)/abs(b)
print(f" {nm}: shipped {a:.4g} vs fit {b:.4g} ({rel*100:.0f}% off)")
assert rel < 0.25, f"{nm} drifted -- update Part 1's defaults from this fit"
# validity window, stated
dq = np.trapezoid(ebike_current(tg), tg) / 3600.0
print(f"\nValidity window: SOC {p_cal[5]:.2f} → {p_cal[5]-dq/5.0:.2f}, 25 °C, ≤ ~1C.")
print("Outside it (low SOC, cold, high C-rate) the parameters change and the")
print("fit must be redone -- see the regime table below.")
DFN-h : solved in 2.71 s V 4.045->4.089 V T 25.00->27.64 C 2-RC ECM (fit to DFN): 10.69 mV in-sample, 8.21 mV held-out hand-written guess : 28.23 mV in-sample, 23.01 mV held-out
Part-1 shipped defaults vs this fit (25% tolerance for fit-to-fit variation): R00: shipped 0.00506 vs fit 0.005062 (0% off) R10: shipped 0.003 vs fit 0.003004 (0% off) C10: shipped 8562 vs fit 8562 (0% off) R20: shipped 0.00256 vs fit 0.002558 (0% off) C20: shipped 183 vs fit 182.8 (0% off) Validity window: SOC 0.97 → 0.91, 25 °C, ≤ ~1C. Outside it (low SOC, cold, high C-rate) the parameters change and the fit must be redone -- see the regime table below.
Figure 1: (left) the single-cell identification current; (right) terminal voltage from the three fidelities. The DFN resolves transport and kinetics; the SPMe tracks it to a few mV at a fraction of the cost; the 2-RC ECM — with a DFN-extracted OCV and its initial SOC fitted rather than eyeballed — follows to ~10 mV per cell (~130 mV at pack level) inside its validity window. Note the rest tail after t = 300 s: with no forcing, the relaxation is pure RC-and-diffusion signature, which is what lets the fit separate its two time constants.
5. Look inside: the fields an ECM cannot see¶
The whole point of the DFN is the internal state. Here are two fields the lumped ECM has no representation of at all: the electrolyte concentration across the cell thickness and the lithium concentration inside a negative particle (radius × time). Gradients here are what limit fast charge and seed degradation.
_p = pybamm.ParameterValues("Chen2020")
_p["Current function [A]"] = pybamm.Interpolant(tg, ebike_current(tg), pybamm.t)
sol = pybamm.Simulation(pybamm.lithium_ion.DFN(options={"thermal": "lumped"}),
parameter_values=_p).solve([0, T_END])
ce = sol["Electrolyte concentration [mol.m-3]"].entries # (x, t)
cs = sol["X-averaged negative particle concentration [mol.m-3]"].entries # (r, t)
tt = sol["Time [s]"].entries
fig, ax = plt.subplots(1, 2, figsize=(13, 4.4))
im0 = ax[0].pcolormesh(tt, np.linspace(0, 1, ce.shape[0]), ce, shading="auto", cmap="viridis")
fig.colorbar(im0, ax=ax[0], label="cₑ [mol m⁻³]")
ax[0].set(xlabel="time (s)", ylabel="through-cell position (0=anode → 1=cathode)", title="Electrolyte Li⁺ concentration field")
im1 = ax[1].pcolormesh(tt, np.linspace(0, 1, cs.shape[0]), cs, shading="auto", cmap="magma")
fig.colorbar(im1, ax=ax[1], label="cₛ [mol m⁻³]")
ax[1].set(xlabel="time (s)", ylabel="particle radius (0=centre → 1=surface)", title="Negative-particle Li concentration")
fig.tight_layout(); plt.show()
Figure 2: the DFN's internal fields. Left — electrolyte concentration develops a gradient across the cell whenever current flows (depletion at one electrode, accumulation at the other); it relaxes during the regen dip. Right — lithium fills the negative particle from the surface inward during charge and empties surface-first during discharge, so the surface can be depleted while the core is still full. These gradients are exactly the physics the ECM's RC branches only approximate with a lumped time constant — and they are what a BMS gets wrong at high C-rate if it trusts an ECM outside its fitted range.
6. When do you actually need the DFN?¶
| Regime | ECM ok? | Why |
|---|---|---|
| Cruise / gentle drive cycle (our e-bike) | ✅ | Low C-rate, small gradients — RC branches capture the response |
| Fast charge, high-power pulses | ⚠️/❌ | Surface depletion & electrolyte gradients dominate; ECM extrapolates badly |
| Cold temperature | ❌ | Kinetics (2) and $D_s$ drop sharply; strongly nonlinear |
| Aging / lifetime prediction | ❌ | Degradation is driven by internal potentials/concentrations |
| Cell or pack design | ❌ | You are choosing the geometry/chemistry the ECM would only fit after the fact |
The e-bike sits firmly in the green row — which is why Part 1's ECM was a defensible choice. This notebook is how we'd defend it, and how we'd know to switch if the duty cycle got aggressive.
7. Aging: why lifetime needs physics¶
Capacity fade comes from side reactions — chiefly SEI (solid-electrolyte interphase) growth consuming cyclable lithium, plus plating and particle cracking. These depend on the internal overpotentials, so an ECM can only replay a measured fade curve, never predict one from a new duty cycle. We run the DFN with an SEI submodel over a handful of 1C cycles and watch capacity fall.
try:
aging_model = pybamm.lithium_ion.SPM({"SEI": "solvent-diffusion limited"})
exp = pybamm.Experiment([("Discharge at 1C until 3.0 V", "Charge at 1C until 4.1 V", "Hold at 4.1 V until 50 mA")] * 8)
t0 = time.time()
sim_age = pybamm.Simulation(aging_model, experiment=exp, parameter_values=pybamm.ParameterValues("Chen2020"))
sol_age = sim_age.solve()
cyc = sol_age.summary_variables["Cycle number"]
cap = sol_age.summary_variables["Capacity [A.h]"]
print(f"aging: {len(cyc)} cycles in {time.time()-t0:.1f}s; capacity {cap[0]:.3f} -> {cap[-1]:.3f} Ah "
f"({(1-cap[-1]/cap[0])*100:.2f}% fade)")
fig, ax = plt.subplots(figsize=(6.5, 4))
ax.plot(cyc, cap / cap[0] * 100, "o-", color="tab:red")
ax.set(xlabel="cycle number", ylabel="capacity retention (%)", title="SEI-driven capacity fade (SPM + SEI submodel)")
plt.show()
AGING_OK = True
except Exception as e:
print("aging demo skipped:", type(e).__name__, str(e)[:80]); AGING_OK = False
aging: 8 cycles in 0.3s; capacity 5.153 -> 5.152 Ah (0.02% fade)
Figure 3: capacity retention vs cycle from an SEI-growth submodel (on the SPM — cheap enough to cycle; the mechanism, not the host model, is the point). The fade is emergent from internal chemistry rather than fit to a curve — change the C-rate or temperature and the slope changes for physical reasons. Two honest caveats: the rate here is illustrative, not calibrated — at this setting the cell would take ~8000 cycles to lose 20%, several times longer than real NMC/graphite cells manage (~500–1500 cycles), because the only mechanism enabled is solvent-diffusion-limited SEI; and eight cycles is a mechanism demo, not a lifetime study. The claim is about model form — aging needs internal states an ECM does not have — not about this particular trajectory.
8. Temperature: performance is not isothermal¶
Diffusion and kinetics are strongly Arrhenius. The same current at 0 °C vs 35 °C gives very different usable voltage and heat. A single DFN sweep shows why cold range is worse and why thermal management matters.
fig, ax = plt.subplots(1, 2, figsize=(13, 4.2))
for Tamb_C, col in [(0.0, "tab:blue"), (25.0, "tab:green"), (40.0, "tab:red")]:
pT = pybamm.ParameterValues("Chen2020")
pT["Ambient temperature [K]"] = 273.15 + Tamb_C
pT["Initial temperature [K]"] = 273.15 + Tamb_C
pT["Current function [A]"] = pybamm.Interpolant(tg, ebike_current(tg), pybamm.t)
s = pybamm.Simulation(pybamm.lithium_ion.DFN(options={"thermal": "lumped"}), parameter_values=pT).solve([0, T_END])
ax[0].plot(s["Time [s]"].entries, s["Terminal voltage [V]"].entries, color=col, label=f"{Tamb_C:.0f} °C")
ax[1].plot(s["Time [s]"].entries, s["Volume-averaged cell temperature [K]"].entries - 273.15, color=col, label=f"{Tamb_C:.0f} °C")
ax[0].set(xlabel="time (s)", ylabel="terminal voltage (V)", title="Voltage vs ambient temperature"); ax[0].legend(fontsize=8)
ax[1].set(xlabel="time (s)", ylabel="cell temperature (°C)", title="Self-heating vs ambient"); ax[1].legend(fontsize=8)
fig.tight_layout(); plt.show()
Figure 4: the identical current profile at three ambient temperatures. Cold cells sag lower (sluggish transport/kinetics) and would hit the cut-off sooner — the physical origin of reduced winter range — while all self-heat by a few °C.
9. Parameterisation & co-simulation — how the ladder connects to the vehicle¶
Getting ECM parameters. In practice you identify an ECM from GITT/pulse
tests (current pulses + relaxation isolate R0 from the RC dynamics) on a real
cell, or — as we did in §4 — by least-squares fitting the ECM to a DFN you
trust. Either way the ECM's numbers become traceable.
Coupling the battery to the vehicle sim. Three patterns, cheapest first:
- Embedded reduced model (what Part 1 does). The ECM lives inside the acausal circuit; current and voltage are solved simultaneously with the motor and vehicle. µs-cheap, fully coupled, real-time-capable.
- Offline one-way playback. Run the vehicle sim → export the pack current $I(t)$ → drive a DFN offline for a detailed voltage/temperature/aging report. No feedback, but full fidelity for post-hoc analysis. (§4's profile is a purpose-built bench excitation, not exported vehicle current — an identification signal and a duty-cycle replay are different tools; wiring the actual export is Exercise 6.)
- Online co-stepping. Advance the vehicle DAE and the DFN DAE in lockstep, exchanging (current ↔ voltage) each macro-step via operator splitting. Needed only when the battery's fast dynamics feed back into control within a step — rare for an e-bike, common for grid inverters.
Data-driven ROMs. You can also learn a reduced battery model from DFN (or
measured) data — DMDc, eDMDc, SINDyc — which is exactly what Jaxonomy's
battery_part_4/5/6 tutorials do. That gives a fast block, like our ECM, but
fit by system-identification rather than circuit intuition.
10. Validation & failure modes¶
Validation, as actually performed above: SPMe reproduces the DFN to a few mV; the calibrated ECM reaches ~10 mV in-sample and ~8 mV on a held-out profile (the number that counts); the fitted parameters match the defaults shipped in Part 1's vehicle model by construction, and the assertion in §4 fails if they drift; the temperature trend has the right sign. The aging curve is monotonic with an emergent, uncalibrated rate — a mechanism demo, not a lifetime claim. Against a real cell you would additionally validate the DFN parameters themselves against pulse and rate-test data.
Failure modes — be specific:
- ECM extrapolation. The fit is valid in its stated window (SOC ~0.97–0.91, 25 °C, ≤ ~1C). An ECM fit there will mispredict a 4C cold pulse by tens–hundreds of mV; its RC branches have no transport physics to fall back on — and the OCV steepens sharply below SOC ~0.2, where a fit that never saw low SOC is silently wrong.
- Identifiability is a property of the experiment, not the model. On a ~1 s grid, or without multi-scale pulses and a rest tail, this same 2-RC fit collapses to 1-RC with a parameter pinned at its bound — and without the printed identifiability report you would never know (the fit "succeeds" and the RMSE looks fine). This is why GITT/pulse protocols exist.
- DFN parameter sensitivity. DFN outputs are only as good as $D_s$, $i_0$, porosities, particle radii — often the least-known numbers in the model.
- Aging model form. SEI-only misses plating/cracking; the mechanism must match the regime (fast charge → plating; calendar → SEI).
- Thermal coupling. A lumped thermal model hides internal hot-spots (see the
multi-node network in Part 1 /
ebike_thermal_rom.py).
11. Exercises¶
- (code — the degeneracy trap, replayed) Re-run the §4 fit on a coarse
400-point grid (
tg = np.linspace(0, T_END, 400)). Watch the identifiability report flip to "branches degenerate": the fast charge-transfer branch (τ ≈ 0.5 s) is invisible at ~1 s sampling and the optimizer quietly merges the two RC pairs. Then restore the fine grid but delete the rest tail from the profile and see which diagnostic catches it. - (code) Re-fit the ECM with a 3C peak current instead of ~1C. How much does the held-out RMSE degrade, and which parameter moves most?
- (code) Add the electrolyte-concentration gradient magnitude $\max_x c_e - \min_x c_e$ as a time series. When is it largest, and how does it correlate with the terminal-voltage sag?
- (concept) The e-bike sits in the "ECM ok" regime. Name a realistic e-bike scenario that would push it out, and say which fidelity you'd switch to and why.
- (concept) Sketch the data flow for online co-stepping the DFN with the Part-1 vehicle DAE. What is exchanged each step, and what could make the coupling unstable?
- (code) Wire coupling pattern 2 for real: run the Part-1 reference cycle, export its pack current, divide by 3, and replay it through the DFN here. Compare the DFN's voltage against the vehicle model's own ECM trace — where do they part company, and is that inside or outside the fit's validity window?
- (open-ended) Fit a data-driven ROM (à la
battery_part_4DMDc) to the DFN §4 data and compare it to the 2-RC ECM on accuracy, speed, and extrapolation to the held-out profile.
Key takeaways¶
- Battery models form a fidelity ladder — ECM (0-D) ↔ SPMe ↔ DFN (P2D) — trading spatial physics for speed.
- The DFN resolves internal fields (electrolyte + particle concentration) that drive fast-charge limits, temperature dependence, and aging; the ECM has no representation of them.
- You earn the cheap ECM by fitting/reducing it from the DFN (or GITT data), making its parameters defensible — then run it inside the vehicle model.
- Choose fidelity by the question: real-time vehicle/BMS → ECM; transient, thermal, aging, or design → DFN/SPMe.
Next: Part 4 — 3-D multibody dynamics and co-simulation with MuJoCo.
References¶
- M. Doyle, T. F. Fuller, J. Newman, J. Electrochem. Soc. 140(6), 1993 (the DFN/P2D model).
- C.-H. Chen et al., J. Electrochem. Soc. 167, 2020 (the "Chen2020" parameter set).
- V. Sulzer et al., PyBaMM, J. Open Res. Software 9, 2021.