去年冬天一次内部方案评审,有人指着大屏问了句:“这套系统既然叫‘联动’,那我把热源侧出口温度往下压20度,制氢侧的氢气流量多久能跟上、纯度掉不掉、电解堆会不会超温?”会议室安静了十几秒。这个问题一点都不刁钻,它只是把“仿真联动”四个字翻译成了工程语言。而当时我们手上那套东西,热工模型在一个人电脑上跑,电化学模型在另一个人电脑上跑,两边靠导表格对齐数据,所谓“平台”只是个共享文件夹。后来一年多时间,我们把核能耦合制氢这条链路上的热工、电化学、热化学、储运几段模型重新梳理,用大模型做交互与编排层,搭出了一套能跑通全流程的仿真联动系统平台。这篇就把这套东西拆开讲:难在哪、架构怎么分层、机理模型怎么降阶、联动的地基怎么打、大模型到底该干哪些活、联调时踩了什么坑。适合正在做能源系统仿真、数字孪生、多物理场耦合平台的朋友参考,也适合刚接触核能制氢工艺、想知道工程落地长什么样的读者。
1. 热-电-氢三股流挤在一条链路上:联动到底难在哪
核能耦合制氢听起来像一句话,拆开是一串能量转手。热源侧给出高温热,中间换热器把热量交给蒸汽,蒸汽进入高温蒸汽电解堆或者热化学循环,产出的氢气再经过压缩、储存、外送。每一段都有自己的物理规律、自己的时间常数、自己的边界条件,而“联动”要求它们在同一时刻互相看得见对方的状态。难的不是单段建模,难的是让这几段在同一个时间轴上对话。
1.1 从热源出口到氢气纯度,中间隔着七次能量转手
我习惯把这条链路画成七次转手:热源载热剂的对流换热、中间换热器的跨壁导热、蒸汽发生与过热、电解堆内的电化学与热耦合、产气侧的组分分离与干燥、压缩机的机械功输入、储罐的压力与温度平衡。每一次转手都会引入一个效率项和一个延迟项,最终氢气出口的流量和纯度是这七项叠出来的结果。
举个例子,假设制氢侧要求1000 Nm³/h的氢气产能,用高温蒸汽电解,电耗按3.3 kWh/Nm³算,电功率就是3300 kW。对应的水蒸气需求量按化学计量大约是804 kg/h,考虑70%的单程利用率,实际要送进去约1150 kg/h蒸汽。这些蒸汽从150℃预热的饱和态升到800℃,显热项大约是1150×2.2×650/3600≈457 kW,如果从液态水算起再加汽化潜热2257 kJ/kg,约721 kW,合计接近1.2 MW的热量必须由热源侧稳定供给。这个数字看着不大,但它对温度品位的要求很苛刻——蒸汽必须到800℃量级,热源侧出口温度就得在850℃以上,中间换热器夹点温差还得留出30℃左右的余量。
算这笔账的意义在于:任何一段模型的边界条件写错,误差会沿着链路放大。我见过最典型的一次,是换热器模型里把氦气的定压比热当成常数处理,在850℃附近偏差接近8%,结果热源侧热量够、制氢侧却一直报“蒸汽温度不足”,排查了两天才发现是物性表的锅。这类问题在单段仿真里几乎不会暴露,一旦联动就原形毕露。
1.2 多时间尺度才是真正的拦路虎:秒、分钟、小时挤在一个平台里
电化学过程的响应是秒级的,电流密度变化后电压几乎立刻跟随;换热器的热惯性是分钟级的,管壁和流体都要慢慢把温度传下去;热化学循环里碘硫路线的三个反应器串起来,整个循环建立稳态要几个小时;储氢罐的压力平衡更是以小时甚至天计。这四个尺度的东西塞进同一个仿真时钟里,怎么办?
我们的做法是分三层时钟。快层走1秒步长,负责电解堆的电化学与堆内热;中层走10秒到60秒,负责换热器、汽包、管道;慢层走10分钟到1小时,负责循环回路、储罐、调度逻辑。层与层之间用零阶保持加一阶外推的方式传递边界值。这个方案不新鲜,但真正做的时候有个细节特别容易被忽略:慢层传给快层的边界值必须带“斜率”,否则快层在两次慢层更新之间会看到一条阶梯状的输入曲线,数值上会产生虚假的振荡,表现出来就是氢气流量曲线上一排小毛刺。
另外,快层和中层的时间步比最好取整数倍关系,比如1秒对60秒,不要取1秒对50秒。看着无所谓,跑几千秒之后累积的相位偏差足够让两条曲线错开半个周期,联调时你会误以为是模型参数出了问题。
1.3 数据孤岛比模型精度更致命
我们做过一次统计,在平台上线前的三个月里,团队花在“对数据”上的时间占到了总工作量的四成以上。这里的“对数据”不是简单的格式转换,而是口径统一:热源侧给的流量是kg/s还是kg/h,压力是表压还是绝压,氢气产量是Nm³/h还是kg/h还是mol/s,温度是摄氏还是开尔文。每一个口径分歧都会在联动时变成一个“莫名其妙”的偏差。
提示:所有跨模块变量在进入平台时必须经过一次强制单位归一,归一后的标准单位建议固定为:温度K、压力Pa(绝压)、质量流量kg/s、摩尔流量mol/s、能量W、体积流量按标准状态换算为Nm³/s并单独标注参考温压条件。归一在接口层做一次,不要在模型内部各写各的。
比单位更麻烦的是语义分歧。热源侧说的“出口温度”和制氢侧理解的“入口温度”可能根本不是同一个测点,中间还隔着一段管道和一台换热器。这种分歧在文档里看不出来,只有把两条曲线画在同一张图上才会发现它们永远差一个固定的温差。后来我们强制要求每个变量都带一份元数据:测点位置、物理含义、正常范围、异常阈值、数据来源模块。这份元数据表后来成了平台最有价值的资产之一,比任何单个模型都重要。
2. 大模型站在平台的哪一层:四层架构里做的一次减法
最初立项的时候,团队里有一种很诱人的想法:既然上了大模型,干脆端到端,输入工况描述,直接输出优化后的运行方案。这个想法被我们自己否掉了。原因很简单,核能耦合制氢这条链路上有硬安全边界,任何一个环节超温超压,代价都不是“效果不好”能概括的。大模型可以做编排、做解释、做寻优的“提议者”,但实时闭环的控制权必须留在确定性的求解器和规则引擎手里。
2.1 先划边界:实时闭环这一层不能交给大模型
我们把平台的能力分成两类:一类是“必须确定性可复现”的,包括仿真求解、变量映射、状态机跳转、安全联锁判断;另一类是“允许概率性、允许人复核”的,包括自然语言建模指令解析、参数寻优建议、异常归因解释、报告生成、知识检索。前一类绝不交给大模型,后一类大模型做得比传统方法好得多。
这个边界不是拍脑袋定的。我们做过一次对照测试:让大模型直接根据当前工况吐出“下一步该把蒸汽流量调到多少”的数值建议,在二十组工况里,有六组给出的数值超出了我们设定的安全区间,还有两组虽然在区间内但方向是反的。而把它降级成“提议者”,由代理模型加贝叶斯优化给出候选点,大模型只负责解释“为什么建议往这个方向调、历史上类似工况怎么处理的”,效果立刻可用。
2.2 四层架构的分工与接口
最终落地的架构分四层,从下往上依次是数据与物性层、机理与代理模型层、编排与调度层、交互与解释层。每层之间只通过明确定义的接口通信,层内实现可以替换。
| 层级 | 主要职责 | 关键接口 | 是否允许大模型介入 |
|---|---|---|---|
| 数据与物性层 | 物性计算、单位归一、时序存储、版本管理 | 统一变量字典、REST/消息队列 | 否 |
| 机理与代理模型层 | 热工水力、电化学、热化学、降阶代理 | FMI标准接口、参数文件 | 否(训练数据可由其辅助生成) |
| 编排与调度层 | 仿真任务编排、工况序列、状态机、联锁 | 任务描述文件、事件总线 | 部分(生成任务描述) |
| 交互与解释层 | 自然语言问答、报告生成、归因分析 | 会话接口、知识库 | 是 |
第一层解决“数据从哪来、单位是什么、哪个版本”的问题,第二层解决“物理上对不对”的问题,第三层解决“按什么顺序跑、什么时候停”的问题,第四层解决“人怎么用、结果怎么读懂”的问题。这个分层最直接的好处是排障有方向:曲线不对,先看第一层的单位,再看第二层的参数,最后才怀疑第三层的调度顺序。没有分层的时候,所有问题都会挤到“平台有bug”这一个筐里。
2.3 为什么不做端到端:机理模型仍然是压舱石
有人会问,既然有代理模型了,为什么不干脆全用数据驱动,把机理模型扔掉?我们的答案是:代理模型只能在训练数据覆盖的范围内可信,而工况切换、故障降载、极端边界这些场景,恰恰是数据最稀疏的地方,也恰恰是最需要算准的地方。
举个具体例子。热源侧突然降负荷,载热剂流量在几分钟内从额定降到60%,这时候换热器的动态响应、蒸汽温度的下冲幅度、电解堆是否触发超温保护,全靠机理模型给出可信的瞬态。我们用纯数据驱动的代理模型试过同样的场景,预测的蒸汽温度下冲幅度比机理模型小了将近40%,因为它从没在训练集里见过这么剧烈的变化。机理模型的价值不在于快,而在于它在未知工况下仍然守住物理规律这条底线。
3. 把高温蒸汽电解和热化学循环搬进引擎:机理降阶的精度账
制氢侧的模型是整个平台里最重的部分。高温蒸汽电解堆的三维电化学-热耦合模型,网格细一点,单次稳态求解就要几十分钟,根本没法放进联动仿真。热化学循环更夸张,碘硫路线涉及三个反应器和两套循环回流,全机理模型跑一次瞬态要按小时计。必须降阶,但降阶的精度账得算清楚,否则联动出来的结果就是“看着像那么回事,实际不能用”。
3.1 高温蒸汽电解的电压-电流-温度曲面怎么标定
高温蒸汽电解堆的降阶,核心是把堆内的电化学行为压缩成一个电压-电流密度-温度的三维曲面。这个曲面的形状由开路电压、欧姆过电位、活化过电位、浓差过电位共同决定,工程上常用的是先建一个一维分布的机理模型,扫出一批工况点,再用这些点拟合一个参数化表达式。
我们的做法是分温度档标定:700℃、750℃、800℃、850℃四档,每档扫电流密度从0.1到1.5 A/cm²,记录电压。拟合出来的表达式在插值区间内误差控制在1.5%以内,外推则明确禁用,超出温度范围直接报错而不是硬算。这里有个经验:拟合的时候不要贪图一个高阶多项式通吃全区间,分档拟合加线性插值反而更稳,因为高阶多项式在区间边缘容易产生振荡,联动时会出现电压曲线在某个温度点突然“跳一下”的怪现象。
还有一个容易被忽略的量是蒸汽利用率。电解堆的蒸汽转化率通常在60%到80%之间,利用率越高,出口氢气浓度越高,但浓差损失也越大。降阶模型里必须把这个参数显式保留,不能默认固定值,因为联动时热源侧热量不足,操作员第一反应就是降流量提利用率,模型如果不响应这个操作,仿真结果就和实际完全对不上。
3.2 碘硫循环的三反应两循环怎么简化
碘硫热化学循环的完整机理包括本生反应、碘化氢分解、硫酸分解三个反应,加上碘循环和硫循环两条回流。全机理模型里光是相平衡计算就够喝一壶。我们把它简化成三个串联的反应器模型加两条物料回流,每个反应器只保留转化率和热效应两个关键参数,相平衡用简化的活度系数关联式替代。
简化的代价是精度,我们对比过:在稳态工况点附近,简化模型与全机理模型的氢气产率偏差在3%以内,热量需求偏差在5%以内;但在开停车瞬态过程中,偏差会放大到15%以上。所以我们在模型里加了一个“工况标识”,稳态段用简化模型,瞬态段自动切换到一张预先算好的瞬态响应表做修正。这张表是离线用全机理模型扫出来的,覆盖了从冷态启动到满负荷的典型路径。
这个思路本质上是“用离线算力换在线速度”。离线把难算的都算一遍,在线只做查表和插值。代价是灵活性差一些,遇到表里没有的路径要重新算,所以平台里必须保留一条离线重算的流水线,否则工况一变就得干等。
3.3 降阶误差控制在什么量级才不会拖垮联动
这个问题我们总结了三条线。第一条:单段模型的稳态误差控制在2%以内,这是底线,超过这个量级,联调时会和别的模块的误差叠加,最终结果没法解释。第二条:关键安全变量(电解堆温度、换热器出口蒸汽温度、系统压力)的最大误差控制在1%以内,因为这些变量直接触发联锁。第三条:瞬态过程的时间常数误差控制在10%以内,也就是说如果真实系统温度响应是100秒,模型给90到110秒都能接受,但给50秒就完全不能用。
降阶模型的验收不能只看拟合优度。我们要求每个降阶模型都跑一套标准验证工况,包含额定工况、70%负荷、50%负荷、启动、停机、以及两次人为扰动,每个工况都输出关键变量的时间序列,和全机理模型逐点比对,生成一份误差报告。这份报告是模型入库的准入条件,没有它不能进平台。
4. 联动的地基是一张变量映射表:单位、方向、时序三件事
前面说的都是单段模型,真正让它们“联动”起来的,是一张看起来平平无奇的变量映射表。我后来跟同行交流发现,很多平台做不下去,不是模型不行,是这张表没做扎实。它要解决三件事:变量怎么命名和统一单位、数据往哪个方向流、不同时间步之间怎么对齐。
4.1 变量命名与单位统一的硬规矩
命名我们用的是“层级_设备_物理量_位置”的四段式,比如pri_hx_secondary_T_out表示一回路侧中间换热器二次侧出口温度。这套命名看着啰嗦,但它能解决一个非常实际的问题:当团队里有十几个人同时看曲线的时候,没人需要问“这个T2是哪个温度”。单位统一在接口层强制做,模型内部可以保留自己习惯的单位,但进出接口必须转换,转换函数要单独测试。
| 变量类别 | 常见错误口径 | 平台标准口径 | 换算注意点 |
|---|---|---|---|
| 温度 | 摄氏度、开尔文混用 | 开尔文 | 温差与绝对温度区分,换热计算用温差,状态方程用绝对温度 |
| 压力 | 表压、绝压混用 | 帕斯卡(绝压) | 表压必须加当地大气压,不能默认101325 |
| 质量流量 | kg/h、kg/s混用 | kg/s | 注意湿蒸汽总流量与干蒸汽分量的区别 |
| 摩尔流量 | kmol/h、mol/s混用 | mol/s | 与质量流量换算需随组分变化更新摩尔质量 |
| 氢气产量 | Nm³/h、kg/h混用 | Nm³/s | 必须标注参考温压条件,不同标准差异可达3% |
这张表我们踩过的最大的坑是压力口径。有一次热源侧模型输出的是一回路压力表压,制氢侧模型按绝压处理,两边差了0.15 MPa,结果换热器模型的饱和温度算错,蒸汽温度怎么调都上不去。查了整整一天,最后是把所有压力变量打印出来逐个比对才发现的。从那以后,平台里所有压力变量在元数据里强制标注“绝压”字样,表压在接口层一次性转换。
4.2 时间步对齐与插值策略
时间步对齐的核心原则是:快层不能直接读慢层的瞬时值,必须读经过保持和外推处理的值;慢层读快层时要先做时间窗聚合,不能随便取一个采样点。我们用的策略是快层对慢层用零阶保持加一阶斜率外推,慢层对快层用时间窗内的加权平均。
插值方法的选择也有讲究。温度、压力这类连续物理量用线性插值够了;流量在阀门动作瞬间会有阶跃,线性插值会把阶跃抹平,导致快层看不到真实的扰动,这时候要用带限幅的阶跃保持;组分浓度变化平滑,可以用三次样条,但要防止过冲产生负浓度,插值后必须做非负截断。
提示:联动仿真里最容易出现的“假振荡”,八成来自时间步错配或插值方法不当,而不是模型参数错了。看到曲线上一排等间距的小毛刺,先去查时间步是不是整数倍关系、插值是不是把阶跃抹平了,再怀疑物理模型。
4.3 工况调度状态机的设计
联动不是让所有模型一直跑,而是按工况序列调度。我们设计了一个五状态的状态机:待机、启动、稳态运行、降载、停机。每个状态定义入口条件、出口条件、允许的动作集合、以及必须监控的安全变量。状态之间的跳转由事件驱动,事件来源包括操作员指令、仿真时间到达、安全变量越限。
状态机最怕的是“卡死”和“抖振”。卡死是指进入某个状态后出口条件永远不满足,比如启动状态下要求蒸汽温度达到800℃才转稳态,但热源侧还没升温,就永远出不来。解决办法是给每个状态加超时保护和降级路径,超时后转入安全状态并报警。抖振是指两个状态之间来回跳,比如降载和稳态反复切换,这通常是入口条件和出口条件有重叠区间,解决办法是加迟滞带,进入和退出的阈值错开一定幅度。
状态机的逻辑我们是用配置文件描述的,不是硬编码在代码里。这样调整工况序列不用重新编译,改配置文件就行。配置文件本身也纳入版本管理,每次仿真运行都记录用的是哪个版本的状态机配置,保证结果可追溯。
5. 让大模型调参而不是许愿:代理模型加持下的参数寻优闭环
大模型在参数寻优里的角色,我总结成一句话:它是提需求的和解释结果的,不是算数的。真正算数的是代理模型加优化算法。这个分工定下来之后,整个寻优模块的可靠性上了一个台阶。
5.1 采样、代理、寻优三步走
第一步是采样。设计空间通常是八到十二维,包括蒸汽温度、蒸汽流量、电解堆电流密度、入口压力、热源侧载热剂流量、换热器旁路开度等。维度不算高,用拉丁超立方采样两百到三百个点,覆盖整个设计空间。每个采样点跑一次机理模型,得到对应的目标值,比如系统能耗、氢气产率、电解堆最高温度。
第二步是建代理模型。我们用的是高斯过程回归,优点是它不只给预测值,还给预测方差,这个方差在寻优时非常有用——方差大的区域说明样本稀疏,值得去探索。样本量两百多的时候,高斯过程的训练时间在分钟级,完全够用。如果维度再高一些,可以换成随机森林或者梯度提升,但会丢掉方差信息。
第三步是寻优。用贝叶斯优化,采集函数选期望改进,每轮根据代理模型给的建议点跑一次机理模型验证,把结果加回样本集,更新代理模型,迭代二十到三十轮收敛。整个过程是半自动的,每轮的结果都展示给操作员,由人决定是否继续。
import numpy as np def expected_improvement(mu, sigma, best, xi=0.01): """期望改进采集函数,mu/sigma为代理模型预测均值与标准差""" from scipy.stats import norm imp = mu - best - xi z = imp / np.maximum(sigma, 1e-9) ei = imp * norm.cdf(z) + sigma * norm.pdf(z) ei[sigma < 1e-9] = 0.0 return ei def suggest_next(model, bounds, n_candidates=5000, best=0.0): """在边界内随机撒点,取期望改进最大的候选点""" cand = np.random.uniform(bounds[:, 0], bounds[:, 1], size=(n_candidates, bounds.shape[0])) mu, sigma = model.predict(cand, return_std=True) ei = expected_improvement(mu, sigma, best) return cand[np.argmax(ei)]这段代码是骨架,实际用的时候还要加上约束处理和批量建议。约束处理是关键,下面单独说。
5.2 约束怎么加才不会被优化器绕过
优化器是很“聪明”的,你给它一个目标函数,它会想尽办法钻空子。我们最初的做法是把超温、超压这些约束写成惩罚项加进目标函数,结果优化器学会了在约束边界附近反复横跳——因为惩罚项的系数不够大,它宁可稍微超一点温也要换取更高的产氢率。
后来改成硬约束:所有安全相关的约束在采样阶段就过滤掉,根本不进入候选集;代理模型训练时只用可行域内的样本;寻优时在候选点上先做可行性判断,不可行的直接给负无穷的采集值。这样优化器再聪明也绕不过去,因为它根本看不到不可行的区域。
| 约束类型 | 具体变量 | 处理方式 | 违反后动作 |
|---|---|---|---|
| 温度硬约束 | 电解堆最高温度 | 采样过滤+候选点过滤 | 该点直接丢弃 |
| 压力硬约束 | 系统最高压力 | 采样过滤+候选点过滤 | 该点直接丢弃 |
| 纯度要求 | 氢气出口纯度 | 软约束,转惩罚项 | 记录并降低评分 |
| 变化速率 | 蒸汽流量变化率 | 软约束,转惩罚项 | 记录并降低评分 |
| 经济性 | 单位氢能耗 | 目标函数 | 参与排序 |
软硬约束分开处理是这套方案的核心。硬约束保安全,一条都不能破;软约束保品质和经济性,允许在有限范围内权衡。这个划分要提前和运行人员确认清楚,不能由算法工程师单方面决定。
5.3 人机确认环节不能省
最后一道关口是人。优化器给出建议之后,平台不会自动执行,而是生成一份建议报告,内容包括建议的参数组合、预测的目标值提升幅度、预测的不确定性范围、以及大模型生成的归因解释——为什么往这个方向调、历史上类似工况调整后的实际效果如何。
操作员确认之后,建议才会进入仿真验证流程,跑一次完整的工况序列,看瞬态过程中有没有触发任何保护。只有仿真验证通过,建议才会被标记为“可用于实际操作参考”。整个流程听起来繁琐,但它把大模型的能力限制在“提议”和“解释”两个环节,任何可能影响实际运行的决策都有人和仿真双重把关。
6. 一次全链路联调的排查实录:从热源波动到氢气流量抖动
平台搭起来之后,最折腾的是联调。单段模型各自都验证过,凑到一起就是各种不对劲。这里挑一次印象最深的排查过程完整记录下来,因为它的排查链路很有代表性。
6.1 现象与第一轮怀疑方向
现象是这样的:热源侧载热剂入口温度按正弦规律做±15℃的扰动,周期600秒,观察制氢侧的氢气流量响应。理论上氢气流量应该有相应的波动,幅度不大但趋势跟随。实际跑出来,氢气流量确实在波动,但曲线上叠着一层高频毛刺,周期大约60秒,幅度占到总波动的三分之一。更奇怪的是,电解堆温度曲线很平滑,看不出任何对应的高频成分。
第一轮怀疑的是电化学模型。我们把电解堆模型的参数翻了个遍,极化曲线、活化能、交换电流密度,全部复核,没有问题。第二轮怀疑换热器模型,把换热系数、管壁热容、网格数都改了一遍,毛刺依旧。第三轮开始怀疑平台本身,把通信日志打开,逐帧看数据传递。
6.2 定位到时间步错配的那两个小时
日志打开之后问题很快就露馅了。快层是1秒步长,中层是60秒步长,慢层是600秒步长。中层向快层传递边界值时,本应该带斜率外推,但代码里实际写的是零阶保持。也就是说,快层每秒读到的蒸汽温度是同一个值,直到60秒后才跳变一次。这个阶梯状的输入进入电化学模型,在阶跃点上产生了数值振荡,表现出来就是60秒周期的毛刺。
更隐蔽的是,慢层向中层传递的时候用了线性插值,但插值的两个端点之间隔了600秒,而中层只有60秒步长,插值出来的中间值实际上是一条斜率固定的直线,和真实的物理变化完全不符。两层问题叠加,毛刺就出现了,而且因为电解堆的热惯性大,温度曲线把这个高频成分滤掉了,所以只有流量曲线能看到。
修复用了不到二十行代码:中层向快层改成零阶保持加一阶斜率外推,慢层向中层改成带限幅的分段线性插值。改完之后毛刺消失,氢气流量曲线平滑跟随热源扰动,相位滞后大约90秒,和理论估算的换热器热惯性时间常数吻合。
提示:联动仿真里所有跨时间步的数据传递,都要在代码里显式写明“保持方式”和“外推方式”,不要依赖框架默认值。框架默认值往往是零阶保持,对慢变信号没问题,对快层就是灾难。
6.3 修完之后又冒出内存与日志的问题
毛刺解决之后,我们放开了仿真时长,从原来的600秒延长到8小时,模拟一个完整的启停周期。跑到第4个小时左右,进程被系统杀掉了。查日志发现内存占用一路涨到十几GB。原因是每个时间步都把完整的变量快照写进了内存里的历史缓冲区,8小时按1秒步长就是两万八千多个快照,每个快照几百个变量,内存直接爆掉。
改成环形缓冲区加落盘策略:内存里只保留最近十分钟的快照用于回看,更早的按分钟聚合成摘要落盘,原始数据用列式存储压缩后异步写入。改完之后内存稳定在1GB以内,8小时仿真顺利跑完。顺带还发现日志本身也有问题——原来每个时间步都打一条INFO日志,8小时下来日志文件几个GB,排查的时候根本打不开。改成按状态切换和异常事件打日志,正常步进不打,日志量降了两个数量级。
这次联调给我们的教训是:短时间跑通不等于能长时间跑。凡是涉及时间积分的系统,一定要在早期就做长时长测试,内存泄漏、缓冲膨胀、日志爆炸这些问题,只有跑久了才会暴露。
7. 从能跑到能交付:任务队列、结果追溯与复现
平台在实验室里跑通是一回事,交付给别人用是另一回事。中间最大的差距是“可复现”——同一个工况,今天跑和下周跑,结果必须一致;换一台机器跑,结果也必须一致。这一点在科研场景里可能没那么严格,但在工程交付里是硬要求。
7.1 仿真任务不能同步跑
最初的设计是同步执行:用户点击运行,平台阻塞等待结果。单工况没问题,一旦做参数扫描或者多工况序列,用户界面就卡死了。后来改成任务队列,用户提交任务后立刻返回任务编号,后台工作进程从队列里取任务执行,执行状态通过消息通道推送给前端。
队列设计里有几个细节值得说。第一是任务优先级,交互式单工况仿真优先级高于批量扫描,否则用户点一下要等半小时。第二是资源隔离,每个任务在独立容器里跑,限制CPU和内存上限,防止一个跑飞的任务拖垮整台机器。第三是任务超时,超过设定时长自动终止并标记为超时,避免死任务占着资源不放。
第四是断点续跑。长时长仿真跑到一半机器重启,从头再来代价太大。我们在每个慢层时间步结束时保存一次完整状态快照,重启后从最近快照恢复。这个功能实现起来不复杂,但要求所有模型的状态都能完整序列化和反序列化,这一点在选模型框架的时候就要考虑进去,事后补很麻烦。
7.2 每次仿真都要能复现
复现这件事,说到底是版本管理。一次仿真运行涉及至少五类版本:机理模型版本、代理模型版本、物性数据库版本、状态机配置版本、平台代码版本。这五个版本号必须和仿真结果绑定存储,缺一个都没法复现。
我们的做法是给每次运行生成一个清单文件,记录这五类版本的具体标识,加上输入参数的完整快照和随机数种子。结果数据单独存储,通过清单文件里的哈希值关联。这样任何时候想复现,拿着清单文件就能把环境还原出来。
有个细节:浮点运算的结果在不同硬件上可能有微小差异,严格来说做不到比特级复现。我们的验收标准是“关键变量的相对偏差小于0.1%”,这个量级在工程上足够用,也符合实际。如果有人要求比特级复现,那得从编译选项和数学库版本层面去控制,成本很高,一般没必要。
平台交付之后又陆续加了一些东西,比如结果对比视图、敏感性分析、批量报告的自动生成。但核心架构一直没动,那四层分工和那张变量映射表,从第一版到现在都是地基。回过头看,最初那个“把热源温度压20度,制氢侧多久跟上”的问题,现在可以在平台上点几下就给出答案,附带完整的瞬态曲线和归因解释。这个过程里我体会最深的一点是:大模型带来的最大改变不是算得更准,而是让人和这套复杂系统之间的对话变得顺畅了——以前要写脚本、查手册、对变量名,现在可以用自然语言问“如果蒸汽流量降到70%,电解堆会不会超温”,平台能把这个问题翻译成仿真任务、跑完、再用大白话解释结果。但前提是底下那套机理模型、变量映射、状态机、版本管理一样都不能少,缺了任何一块,上面这层对话就是空中楼阁。