PyBaMM Experiment 跑完了却是错的:五个实测陷阱
PyBaMM Experiment 跑完了却是错的:五个实测陷阱
站内搜索
直接问 AI

PyBaMM Experiment 跑完了却是错的:五个实测陷阱

PyBaMM 的 Experiment 接口几乎从不报错。用几句字符串描述一个充放电流程,它就能跑完,把解交给你。这是它的长处,也正因为这样,下面这几个错误代价很高:每一个都会让实验正常跑完,然后交回一个错误的结果。文中所有数字都是用 PyBaMM 26.5.0 在 Chen2020 和 OKane2022 参数集上实测的。

PyBaMM 电池建模系列 · 静默错误实测(第 2 / 7 篇):上一篇 PyBaMM 18 个内置参数集实测:四种静默出错的方式 · 下一篇 PyBaMM 热模型实测:产热为零、缺失的熵热与从 25 °C 开始的低温仿真 · 专题页:数据管线 4 篇 + 静默错误实测 7 篇 · RSS 订阅。

一、用安培写的步骤,到 24 小时就停

同一个慢速放电,在一颗 5 A·h 的电芯上用两种写法:

步骤字符串 实际运行 末电压 放出电量 完成步数
Discharge at C/50 until 2.5 V 51.5 h 2.500 V 5.149 A·h 1 / 1
Discharge at 0.1 A until 2.5 V 24.0 h 3.778 V 2.400 A·h 1 / 1
上面这步,接静置,再接充电 24.0 h 3.778 V 2.400 A·h 1 / 3
Discharge at 0.1 A for 60 hours or until 2.5 V 51.5 h 2.500 V 5.149 A·h 1 / 1

在这颗电芯上 0.1 A 就是 C/50。写成倍率,步骤会一直放到截止电压;写成安培,它在正好 24 小时处停下,放出的电还不到一半,而在多步流程里,后面所有步骤都被跳过。sim.solve() 正常返回。sol.termination 显示 'final time'——和一个本来就该按时长结束的步骤一模一样。

源码里写得很清楚。步骤没有指定时长时,PyBaMM 会给一个默认值:

  • 倍率步骤:2 / C 小时——按标称容量走完一遍所需时间的两倍。
  • 其余一切——安培电流、功率、恒压保持:24 小时。

这个规则来自 PR #4239,它修的是 issue #4224(一个 0.01C 放电在 24 小时处停下)。那次修复只把倍率步骤改成了 2/C 小时;随附的单元测试至今仍断言电流步骤的默认时长是 24 * 3600 秒。所以这是有意为之。而任何从倍率改用安培的人——比如为了绕开参数集那篇文章里说的标称容量误差——一到小电流就会直接撞上。

倍率的默认值也不是万无一失,因为它按标称容量算。Ramadass2004 的实际容量是标称的 1.775 倍,一个 C/20 放电要用掉给它的 40 小时里的约 35.5 小时:还没出别的问题,89% 的安全余量就没了。

警告,以及它是怎么消失的

PyBaMM 其实察觉到了。它会写一句 “Experiment is infeasible: default duration (86400 seconds) was reached…”——通过它自己的 logger,级别是 WARNING。它不是 Python 的 warning,也不是异常。而批量跑仿真的脚本,常常会用这一行压掉 PyBaMM 的日志噪音:

pybamm.set_logging_level("ERROR")

加了这一行,截断照样发生,提示却彻底消失。我两个级别都测了:WARNING 下能看到那句话,ERROR 下什么都没有。

修法:给每个慢速或者没有明确终点的步骤写上时长上限——"Discharge at 0.1 A for 60 hours or until 2.5 V"——并且显式检查实验是否跑完(见第六节),而不是默认”没抛异常就没问题”。

二、平铺列表不是一个循环

下面两个实验包含的步骤完全相同:

steps = ("Discharge at 1C until 2.5 V", "Rest for 30 minutes",
         "Charge at C/2 until 4.2 V", "Hold at 4.2 V until C/50")

pybamm.Experiment([steps] * 12)        # 元组组成的列表:12 个循环
pybamm.Experiment(list(steps) * 12)    # 平铺列表:48 个"循环"

元组会把其中的步骤归为一个循环;平铺列表则让每个字符串自成一个循环。我在带 SEI 模型的 OKane2022 上把两种写法都跑了 12 个真实循环,再拟合容量对循环号的斜率:

写法 summary 行数 锂库存损失 拟合出的”每循环衰减”
元组 12 0.0234% −0.1357 mAh / 循环
平铺列表 48 0.0234% −0.0343 mAh / 循环

物理过程一模一样——损失的锂库存完全相同。变的只是 x 轴,差了 4 倍。拿平铺写法的拟合去外推”衰减到 80% 要多少圈”,循环寿命会被高估约 4 倍。不会报错,容量数值本身看起来也完全正常,因为 PyBaMM 的 Capacity [A.h] 是从电池状态算出来的,而不是按循环计数的。

写这篇文章测第五节时,我自己就掉进去了:一个四步的流程每个”循环”只打印出一步,因为我写成了列表。

修法:每个循环的步骤用元组包起来,并核对 len(sol.cycles) 是否等于你想要的循环数。

三、save_at_cycles 会留下空洞

长时间运行时,sim.solve(save_at_cycles=5) 只保存部分循环的完整解。跑 12 个循环时,保存下来的是第 1、5、10、12 个——首尾总会保存。其余的是 None,但仍然占着列表里的位置:

len(sol.cycles)                    -> 12
sol.cycles[3]["Voltage [V]"]       -> TypeError: 'NoneType' object is not subscriptable

任何”逐个循环画图”的循环都会在这里崩。按循环的 summary 变量不受影响——12 行都在——所以按循环的指标用 sol.summary_variables 取,完整解则用 [c for c in sol.cycles if c is not None] 过滤。

四、period 不会让 PyBaMM 更准,但可能让你更不准

曲线看起来毛糙时,常见的反应是把步骤的 period 调小。对 PyBaMM 自己算出的结果,这一点用都没有。Chen2020 上的 1C 放电:

period 输出点数 放出电量 终止时刻 求解耗时
1 秒 3596 4.991870 A·h 3594.146 s 0.36 s
1 分钟(默认) 61 4.991870 A·h 3594.146 s 0.25 s
1 小时 2 4.991870 A·h 3594.146 s 0.24 s

到小数点后 6 位都一样。求解器自己决定内部步长,并在电压事件上精确停下;period 只决定事后给你多少个点。

陷阱在你自己这边。如果你从输出里自己算东西——比如对电流积分求充入电量——分辨率就要紧了。以恒流再恒压的充电为例,恒压阶段电流一直在衰减:

period 点数 PyBaMM 算的充入电量 对输出做 np.trapezoid 误差
10 秒 987 5.1217 A·h 5.1219 A·h +0.00%
1 分钟 167 5.1217 A·h 5.1221 A·h +0.01%
10 分钟 19 5.1217 A·h 5.1377 A·h +0.31%
30 分钟 8 5.1217 A·h 5.2798 A·h +3.09%

修法:累计量直接取 PyBaMM 的变量(Discharge capacity [A.h]、Throughput capacity [A.h]),不要自己对输出积分。period 按图想画多细来选,而不是按精度选。

五、initial_soc=1.0 不等于 4.2 V

initial_soc 是相对参数集自己的电压窗口定义的。我在 initial_soc=1.0 下静置 1 秒读开路电压:

参数集 上限电压 initial_soc=1.0 时的电压
Chen2020、OKane2022、Mohtat2020、Ai2020、Ecker2015 4.2 V 4.2000 V
Marquis2019 4.1 V 4.1000 V
Prada2013(磷酸铁锂) 3.6 V 3.6000 V
ORegan2022 4.4 V 4.4000 V

把一套”充到 4.2 V”的流程从 initial_soc=1.0 开始套在几个参数集上,ORegan2022 的起点就比别人高 0.2 V;它的第一次放电在负载下从 4.2466 V 开始。这不是过冲——步骤终止是精确的,这些测试里每个充电步骤都正好停在 4.2000 V——它就是起点本身。再加上实验字符串会静默覆盖参数集的截止电压(见参数集那篇),跨参数集比较时,起始状态要用伏特写明,而不是用 SOC。

六、确认实验真的跑完了

第一节的失败是以一个正常的解返回的,所以要显式检查。被截断的实验,循环数会少于定义,或者最后一个循环的步数不足:

def experiment_completed(sol, experiment):
    """(ok, message): False when PyBaMM stopped the experiment early."""
    n_run = len(sol.cycles)
    n_expected = len(experiment.cycles)
    steps_done = len(sol.cycles[-1].steps)          # the last cycle is always saved
    steps_expected = experiment.cycle_lengths[n_run - 1]
    if steps_done < steps_expected:
        return False, f"cycle {n_run} stopped after step {steps_done} of {steps_expected}"
    if n_run < n_expected and not experiment.termination:
        return False, f"only {n_run} of {n_expected} cycles ran"
    return True, f"{n_run} of {n_expected} cycles ran, last cycle complete"
正常,3 个循环                     (True,  '3 of 3 cycles ran, last cycle complete')
0.1 A 步骤被截断                   (False, 'cycle 1 stopped after step 1 of 3')
12 个循环 + save_at_cycles=5      (True,  '12 of 12 cycles ran, last cycle complete')
在 3 个循环的第 2 个里被截断         (False, 'cycle 2 stopped after step 1 of 3')
设了容量终止条件                    (True,  '3 of 3 cycles ran, last cycle complete')

设了 termination=(比如 "80% capacity")的实验允许提前结束,这个检查只要求它的最后一个循环是完整的。它抓不到第二节的平铺列表问题——那个实验确实跑完了,只是意思和你想的不一样。

七、测量方式

  • PyBaMM 26.5.0,Python 3.13,Apple M3,默认 IDAKLUSolver。
  • 第一、三、四、六节用 Chen2020;第二节用带 {"SEI": "solvent-diffusion limited"} 的 OKane2022;第五节用表中所列参数集。
  • 默认时长取自这个版本的 pybamm/experiment/step/base_step.py 和 steps.py,以及 PR #4239 的改动。
  • 日志行为的检查方式:给 pybamm.logger 挂一个 handler,分别在 WARNING 和 ERROR 级别下跑被截断的那个例子。

References

相关阅读:PyBaMM 18 个内置参数集实测——标称容量与电压窗口;PyBaMM 求解器收敛失败排查——实验真的报错时;老化数据集管线——一个被截断的循环在那里会变成一个标错的训练样本。在指定温度下跑实验的话,PyBaMM 热模型实测解释了为什么集总模型里的电池可能从温热状态开始。如果用 start_time 排循环,请看 PyBaMM SEI 与析锂实测里关于 start_time 的补充说明:重复出现的相同步骤会静默丢掉安排好的静置。

发表回复

向下探索