用 PyBaMM 生成电池老化与阻抗 AI 数据集:标签、切分和质量控制
用 PyBaMM 生成电池老化与阻抗 AI 数据集:标签、切分和质量控制
站内搜索
直接问 AI

用 PyBaMM 生成电池老化与阻抗 AI 数据集:标签、切分和质量控制

用 PyBaMM 生成”海量、带物理标签”的电池数据集,难点从来不在于写一个 for 循环。它本质上是一个数值实验设计问题:你要控制物理边值条件、参数空间的拓扑、温度梯度、电化学状态向量的瞬态快照、频域阻抗网格,以及代数求解器的收敛容差。任何一处失守,神经网络学到的都不是电化学,而是仿真器自身的数值偏差。

这篇文章给出一条能实际跑通的管线,以及四类会让你悄无声息拿到垃圾数据的失败模式。所有输出都是偏微分方程组的确定性数值解——属于物理驱动的合成数据,必须经过参数辨识与真实电池对齐,否则它只是一个自洽的幻觉。

样例 SOH 与局部 ECM resistance 标签随 cycle 的变化
SOH 衰减轨迹与内部阻抗演变。这些底层劣化动力学必须与真实循环实验数据校准后才有意义。

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() 后再改,否则各次迭代互相污染;求解失败要返回大惩罚值而不是抛异常,否则优化器会在第一个不收敛的点上直接死掉。

如果不做这一步,模型学到的只是拉丁超立方采样带来的人造分布,与电化学无关。成熟的工具链可以直接用 PyBOPpybamm-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_idprotocol_id、参数簇是否跨 split 泄漏 测试集来自物理隔离的设计或协议组合 重建 split,不允许同一轨迹被随机拆散

九、连接合成数据分布与现实物理世界

合成数据在预训练、验证主动学习采集函数、以及架构消融实验上有不可替代的价值。但要跨越 sim-to-real 的鸿沟,还需要:

  • 持续把 PyBaMM 模型与真实循环机的微分电压分析(DVA)和增量容量分析(ICA)做基准校准。这两种分析对电极级退化尤其敏感,是检验模型是否抓住了正确机理的好探针。手上有循环数据想先看一眼曲线,可以用站内的 dQ/dV 增量容量分析器,在浏览器里直接出图。
  • 用全局敏感性分析(如 Sobol 指数)量化究竟是哪些不可观测参数主导了生成的电压特征。如果主导项和你的物理直觉不符,通常是参数边界设错了。
  • 严格记录求解器约束。如果 SUNDIALS 在剧烈老化步骤上无法满足 $10^{-6}$ 的绝对误差容限,那些点必须标记为非物理结果并丢弃,绝不能闭眼喂给模型。

最后一句提醒:合成数据集最大的风险不是不准,而是它太自洽了。真实电池有制造离散性、有温度不均、有传感器噪声,而 PDE 解没有。在把合成数据喂给模型之前,认真考虑注入符合实测分布的噪声——否则模型在仿真上的漂亮指标,到真实电池上会掉得让你怀疑人生。

References

批量生成时最常撞到的是求解器不收敛。判断该调参数还是该改协议,见PyBaMM 求解器收敛失败排查

发表回复

向下探索