用 PyBaMM 生成”海量、带物理标签”的电池数据集,难点从来不在于写一个 for 循环。它本质上是一个数值实验设计问题:你要控制物理边值条件、参数空间的拓扑、温度梯度、电化学状态向量的瞬态快照、频域阻抗网格,以及代数求解器的收敛容差。任何一处失守,神经网络学到的都不是电化学,而是仿真器自身的数值偏差。
这篇文章给出一条能实际跑通的管线,以及四类会让你悄无声息拿到垃圾数据的失败模式。所有输出都是偏微分方程组的确定性数值解——属于物理驱动的合成数据,必须经过参数辨识与真实电池对齐,否则它只是一个自洽的幻觉。

PyBaMM 电池建模系列(共 4 篇):① 架构与求解器 → ② EIS 标签 → ③ 数据集管线 → ④ 训练 SOH/RUL。本文是第 ③ 篇。
一、形式化的数据集生成目标
一个定义明确的电池 AI 数据集是这样一个集合:
$$ \mathcal{D} = \{(\mathbf{x}_i, \mathbf{y}_i, \mathbf{m}_i)\}_{i=1}^N $$
其中 $\mathbf{x}_i \in \mathbb{R}^p$ 是可观测特征向量(时序电压响应、复数阻抗 $Z(\omega)$、操作协议特征);$\mathbf{y}_i \in \mathbb{R}^q$ 是内部不可观测的退化量(SOH、可用锂库存损失 LLI、活性物质损失 LAM)。
真正被低估的是第三项。$\mathbf{m}_i$ 是封装了模型结构假设、热力学参数和求解器配置的完整元数据。缺了它,$f: \mathbf{x} \rightarrow \mathbf{y}$ 的物理因果性无从谈起——半年后你拿到一批数据,如果不知道它是 SPMe 还是 DFN 生成的、求解容差是多少、用的哪套参数集,这批数据就只能丢掉重来。元数据不是可选的记录习惯,它是数据集的一部分。
二、先选模型:这一步决定了你能生成多少数据
大多数教程直接从 DFN 开始,然后在样本量上卡死。PyBaMM 的三个主力模型在保真度和计算成本上差了不止一个量级,而这个选择直接决定了你的数据集规模上限。
| 模型 | 物理内容 | 相对成本 | 什么时候它是错的 |
|---|---|---|---|
SPM |
单颗粒,忽略电解液浓度梯度 | 最低 | 高倍率(约 >1C)下电解液极化不可忽略,电压曲线会系统性偏乐观 |
SPMe |
单颗粒 + 电解液动力学 | 中等 | 需要分辨电极内部反应分布不均时不够用 |
DFN |
完整 Doyle-Fuller-Newman,电极内空间离散 | 最高 | 基本不会”错”,但退化模拟下求解失败率显著上升 |
一条实用的路径是:用 SPMe 扫参数空间,用 DFN 在筛出的子集上做高保真复算。SPMe 保留了电解液极化这个最主要的一阶效应,成本却低得多,适合覆盖大范围采样;DFN 只用在你真正关心的区域。这样做的前提是记录清楚每个样本用的是哪个模型——回到第一节的 $\mathbf{m}_i$。
注意成本比例强烈依赖于电极离散点数、老化周期数和求解器。不要照抄任何博客给出的秒数,包括这一篇;下面的脚本会把耗时打印出来,在你自己的机器上测一次才作数。
三、劣化动力学与状态标签推导
提取”标签”时,你实际上是在查询特定退化偏微分方程在时间上的积分状态。例如 SEI 膜厚 $L_{SEI}$ 的生长速率(LLI 的主要来源)通常这样建模:
$$ \frac{\partial L_{SEI}}{\partial t} = \frac{M_{SEI}}{\rho_{SEI} z F} j_{SEI} \exp\left( -\frac{E_a}{R T} \right) \exp\left( -\frac{\alpha F (\phi_s – \phi_e – U_{SEI})}{R T} \right) $$
- SOH:当前循环放电容量与标称容量之比 $Q_k / Q_0$。
- RUL-to-80:当前状态流形与 $SOH = 0.8$ 边界相交所需的剩余循环数。
- LLI:寄生副反应电流的时间积分累积量,直接压低库仑效率。
- LAM:受颗粒破裂力学控制,局部应力张量 $\sigma_{t,max}$ 超过屈服强度时触发。正负极必须分别记录——两者的退化速率和机理完全不同,合并成单一标量会抹掉最有判别力的信息。
- EIS 特征向量:在频域内对 DFN 雅可比矩阵施加微扰并线性化生成,捕获电荷转移半圆和 Warburg 扩散尾。
四、一个能跑通的完整管线
下面这段是自包含的——复制出去就能跑,不依赖任何未定义的变量。它做四件事:装配带退化子模型的电池、跑循环实验、抽取标签、把求解失败当成一等公民处理。
import time
import pybamm
import pandas as pd
# 退化机理必须在建模型时声明。事后往 ParameterValues 里塞参数不会
# 让这些机理生效 —— 这是最常见的一类"跑通了但没有退化"的错误。
OPTIONS = {
"SEI": "solvent-diffusion limited",
"SEI porosity change": "true",
"lithium plating": "partially reversible",
"lithium plating porosity change": "true",
"particle mechanics": ("swelling and cracking", "swelling only"),
"SEI on cracks": "true",
"loss of active material": "stress-driven",
"calculate discharge energy": "true",
}
def run_one(cycles=100, model_cls=pybamm.lithium_ion.SPMe, seed_params=None):
model = model_cls(options=OPTIONS)
# OKane2022 是为退化研究标定的参数集。用 Chen2020 之类没有
# 退化参数的集合,SEI/plating 子模型会拿到缺省值,结果没有物理意义。
param = pybamm.ParameterValues("OKane2022")
if seed_params:
param.update(seed_params, check_already_exists=False)
exp = pybamm.Experiment(
[("Discharge at 1C until 2.5V", "Charge at 0.3C until 4.2V", "Hold at 4.2V until C/100")]
* cycles
)
sim = pybamm.Simulation(
model, parameter_values=param, experiment=exp,
solver=pybamm.IDAKLUSolver(), # DAE 上比默认 CasADi 快得多
)
t0 = time.perf_counter()
try:
sol = sim.solve(calc_esoh=False)
except pybamm.SolverError as exc:
# 关键:失败样本要留档,不能静默跳过。参数空间边界
# 就是靠这些失败点画出来的。
return {"ok": False, "reason": str(exc)[:200], "seconds": time.perf_counter() - t0}
elapsed = time.perf_counter() - t0
sv = sol.summary_variables
# summary_variables 的键名在 PyBaMM 各版本间改过。硬编码键名的脚本
# 会在升级后拿到 KeyError,或者更糟 —— 静默拿到 None。
def pick(*names):
for n in names:
if n in sv:
return sv[n]
raise KeyError(f"没有找到这些键中的任何一个: {names};实际可用: {list(sv.keys())[:12]}")
cap = pick("Capacity [A.h]")
return {
"ok": True,
"seconds": elapsed,
"cycles_completed": len(cap),
"capacity": cap,
"soh": [c / cap[0] for c in cap],
"lli": pick("Loss of lithium inventory [%]"),
"lam_neg": pick("Loss of active material in negative electrode [%]"),
"lam_pos": pick("Loss of active material in positive electrode [%]"),
# 元数据跟着标签一起走,不要另存一个文件
"meta": {
"model": model_cls.__name__,
"param_set": "OKane2022",
"pybamm_version": pybamm.__version__,
"options": OPTIONS,
},
}
if __name__ == "__main__":
r = run_one(cycles=50)
if r["ok"]:
print(f"完成 {r['cycles_completed']} 圈,耗时 {r['seconds']:.1f}s")
print(f"末端 SOH={r['soh'][-1]:.4f} LLI={r['lli'][-1]:.2f}% "
f"LAM(neg)={r['lam_neg'][-1]:.2f}% LAM(pos)={r['lam_pos'][-1]:.2f}%")
else:
print(f"求解失败(这也是数据): {r['reason']}")
批量生成时用仓库里的脚本,它把上面的逻辑包了多进程和采样:
cd pybamm-ai-data-lab
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python src/run_all.py --samples 200 --workers 4 --seed 7 \
--backend pybamm --output /tmp/pybamm-ai-dataset
--workers 不要盲目设成 CPU 核数。每个 PyBaMM 进程会独立编译 CasADi/IDAKLU 表达式,内存占用可观;核数拉满经常换来的是换页而不是加速。从 4 开始,看着内存往上加。
五、四类会让你悄悄拿到垃圾数据的失败模式
这一节是这条管线里最值钱的部分。以下四种情况都不会报错,脚本会正常退出、CSV 会正常生成,而数据是废的。
1. 退化机理没有真正启用
症状:跑了 500 圈,SOH 曲线几乎是一条水平线,末端还在 0.999。
原因:退化子模型必须在构造模型时通过 options 声明。很多人先建了 pybamm.lithium_ion.DFN(),再往 ParameterValues 里更新 SEI 相关参数,以为这样就打开了 SEI 生长——不会。那些参数会被静静地忽略。检查办法很直接:print(model.options["SEI"])。
2. 参数集与机理不匹配
症状:SOH 掉得离谱,或者 LLI 一路飙到物理上不可能的数值。
原因:用了 Chen2020 这类没有为退化标定的参数集。缺失的退化参数会取缺省值,而缺省值不是”合理值”,只是”能让求解器跑下去的值”。做退化研究就用 OKane2022,或者你自己拟合过的参数集。
3. 实验协议提前终止
症状:cycles_completed 远小于你要求的圈数,但没有异常抛出。
原因:PyBaMM 的 Experiment 在电池衰减到无法满足终止条件时会提前停止(比如容量已经掉到放电根本到不了 2.5V 的截止条件)。这是正确行为,但如果你不检查实际完成圈数,就会拿到一批长度参差不齐、且末端分布被系统性截断的轨迹。永远比对 len(capacity) 和你请求的圈数。
4. EIS 谱是数值伪像
症状:Nyquist 图上出现负实部,或者高频段出现物理上不可能的回勾。
原因:在退化到深处的工作点上线性化,雅可比矩阵条件数极差;频率网格太稀也会让插值出鬼影。用 Kramers-Kronig 关系做一致性检验——它对线性、因果、稳定、时不变的系统必然成立,不成立就说明你的谱不是一个合法的阻抗响应。这类样本要标记出来,不能进监督训练。
六、参数辨识:让合成数据对齐真实电池
在信任合成数据之前,底层参数分布必须拟合到真实实验数据上。做法是最小化实验电压 $V_{exp}$ 与仿真电压 $V_{sim}(\mathbf{p})$ 的残差:
$$ J(\mathbf{p}) = \sum_{k} w_k \left( V_{exp}(t_k) – V_{sim}(t_k; \mathbf{p}) \right)^2 $$
import numpy as np
import pybamm
import scipy.optimize as opt
model = pybamm.lithium_ion.SPMe()
base_param = pybamm.ParameterValues("Chen2020")
# V_exp / t_eval 来自你的循环机导出,这里假定已经对齐到同一时间栅格
# t_eval: np.ndarray 形状 (T,)
# V_exp: np.ndarray 形状 (T,)
KEYS = [
"Negative electrode active material volume fraction",
"Positive electrode active material volume fraction",
]
BOUNDS = [(0.4, 0.8), (0.4, 0.8)]
def objective(x):
param = base_param.copy() # 必须拷贝,否则会污染下一次迭代
param.update(dict(zip(KEYS, x)))
try:
sim = pybamm.Simulation(model, parameter_values=param)
sol = sim.solve(t_eval)
except pybamm.SolverError:
return 1e6 # 不收敛的参数组合给一个大惩罚,
# 直接 raise 会让整个优化中断
V_sim = sol["Terminal voltage [V]"](t_eval)
return float(np.sum((V_exp - V_sim) ** 2))
res = opt.minimize(
objective,
x0=[base_param[k] for k in KEYS],
method="L-BFGS-B",
bounds=BOUNDS,
)
print(res.x, res.fun)
两个容易踩的点已经写在注释里:ParameterValues 必须 copy() 后再改,否则各次迭代互相污染;求解失败要返回大惩罚值而不是抛异常,否则优化器会在第一个不收敛的点上直接死掉。
如果不做这一步,模型学到的只是拉丁超立方采样带来的人造分布,与电化学无关。成熟的工具链可以直接用 PyBOP 或 pybamm-param,它们把代价函数和优化器的组合都封装好了。
七、正交切分与信息泄漏防范
如果同一条模拟老化轨迹上的不同快照(同一套参数下的 cycle 10、50、100)被随机划进训练集和测试集,模型会过拟合到 ODE 系统的确定性演化上。它是在背诵这条轨迹,而不是学习可泛化的劣化函数。
- 切分边界必须保持物理正交:按
cell_design_id切,或按动力学参数空间的随机子集隔离。 - 频域 EIS 数据点受 Kramers-Kronig 关系结构性约束;同一条阻抗谱内的相邻频率点绝对不能跨越训练/测试边界。
- 做完切分后反查一次:测试集里任何一个
cell_design_id都不应出现在训练集中。这条断言写进流水线,比事后发现指标好得离谱要便宜得多。
八、数据集生成质量门禁表
大规模生成时,最重要的不是样本数量,而是每一批是否通过物理、数值和机器学习三个层面的门禁。
| 门禁项 | 检查内容 | 通过条件 | 失败后的处理 |
|---|---|---|---|
| 参数采样 | 参数范围、相关性、LHS 覆盖度、异常组合比例 | 覆盖主要物理维度,且不产生非物理电池状态 | 收窄边界,或把异常区域单独标注为 OOD |
| 退化生效 | 末端 SOH、LLI、LAM 是否偏离初值 | SOH 有可测量的下降,量级与协议相符 | 检查 model.options 与参数集是否匹配 |
| 求解收敛 | SUNDIALS/CasADi 状态、最大步数、容差、失败异常 | 保留样本都有成功标志和稳定步长统计 | 丢弃失败样本,但保留失败参数用于刻画边界 |
| 轨迹完整性 | cycles_completed 与请求圈数之比 |
提前终止的样本被显式标记 | 单独成组,不与完整轨迹混合训练 |
| EIS 合理性 | Nyquist 形状、高频截距、Warburg 尾、KK 一致性 | 阻抗谱与模型假设和频率网格自洽 | 标记为数值伪像,不参与监督训练 |
| 切分策略 | cell_design_id、protocol_id、参数簇是否跨 split 泄漏 |
测试集来自物理隔离的设计或协议组合 | 重建 split,不允许同一轨迹被随机拆散 |
九、连接合成数据分布与现实物理世界
合成数据在预训练、验证主动学习采集函数、以及架构消融实验上有不可替代的价值。但要跨越 sim-to-real 的鸿沟,还需要:
- 持续把 PyBaMM 模型与真实循环机的微分电压分析(DVA)和增量容量分析(ICA)做基准校准。这两种分析对电极级退化尤其敏感,是检验模型是否抓住了正确机理的好探针。手上有循环数据想先看一眼曲线,可以用站内的 dQ/dV 增量容量分析器,在浏览器里直接出图。
- 用全局敏感性分析(如 Sobol 指数)量化究竟是哪些不可观测参数主导了生成的电压特征。如果主导项和你的物理直觉不符,通常是参数边界设错了。
- 严格记录求解器约束。如果 SUNDIALS 在剧烈老化步骤上无法满足 $10^{-6}$ 的绝对误差容限,那些点必须标记为非物理结果并丢弃,绝不能闭眼喂给模型。
最后一句提醒:合成数据集最大的风险不是不准,而是它太自洽了。真实电池有制造离散性、有温度不均、有传感器噪声,而 PDE 解没有。在把合成数据喂给模型之前,认真考虑注入符合实测分布的噪声——否则模型在仿真上的漂亮指标,到真实电池上会掉得让你怀疑人生。
References
- PyBaMM long experiments and summary variables
- PyBaMM coupled degradation 机制
- Python Battery Mathematical Modelling (PyBaMM)
- Physics-based battery model parametrisation from impedance data
- PyBOP: A Python package for battery model optimisation and parameterisation
- Synthetic dataset of LG M50 batteries with different degradation pathways
批量生成时最常撞到的是求解器不收敛。判断该调参数还是该改协议,见PyBaMM 求解器收敛失败排查。