拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HTRI二次开发教程(08):实战一——Xist 壳管式换热器参数扫描与断点续扫

HTRI二次开发教程(08):实战一——Xist 壳管式换热器参数扫描与断点续扫

HTRI二次开发教程(08):实战一——Xist 壳管式换热器参数扫描与断点续扫

版本与事实声明

  • 版本锚点:当前Xchanger Suite 9.4;9.4 起支持"run selected cases individually"(可单选中案例运行),对大规模案例集管理有直接价值。
  • 官方对照物:HTRI Parametric Study Tool(基于 Excel 与 OLE,按模板*.htri批量改变参数),官方称其"intended for users with experience using and knowledge of the underlying data models"。
  • 示例代码中所有标识符为占位符;所有数值为示例性建模,不代表任何标准规定,不对应任何真实装置。

一句话结论:Xist 参数扫描的正确工程形态是"模板案例 + 变量-目标契约 + 状态机 + 断点账本"——用一份*.htri模板派生 N 个案例,每个案例的状态(待跑/成功/超时/失败)落进账本,脚本可随时中断再续,结果汇成"案例编号 × 变量 × 目标"的长表;这与官方 Parametric Study Tool 是同一思路,区别只在于自建脚本更灵活、官方工具更省事。

〇、本篇要解决的认知问题

  • Q1:官方 Parametric Study Tool 已经能批量跑案例,为什么还要自己写扫描脚本?
  • Q2:一个规范的"变量-目标契约"应该长什么样,为什么它是扫描工程的地基?
  • Q3:全因子扫描和拉丁超立方(Latin Hypercube)扫描各自适合什么场景?
  • Q4:批量状态机(提交→运行→超时→重试)怎么设计,才能既跑得完又不失控?
  • Q5:断点账本为什么是批量扫描"敢中断、能续跑"的关键?

一、机制解析

1.1 官方工具有多能,自建脚本补什么

官方知识库对 Parametric Study Tool 的描述很明确:用 Excel + OLE,基于模板*.htri文件在系列中运行大量案例、改变你选择的参数,用于研究灵敏度;同时强调它面向"熟悉底层数据模型"的用户。

那么自建脚本的定位是:

维度官方 Parametric Study Tool自建脚本(本篇)
上手成本低(Excel 界面)中(需要探测 + 编码)
灵活性受工具设计约束高(任意变量组合、任意目标函数)
与外部系统集成弱强(可接优化器、数据库、报表管道)
无人值守/调度一般强(可进计划任务/CI)
适用快速灵敏度研究平台级批量核算、参数辨识、多目标筛选

决策逻辑(经验法则):一次性灵敏度研究先用官方工具;要进生产流水线(定时、断点续跑、结果落库、联动优化)则自建脚本。两者底层都是同一条 Automation Server 通道,思路一致。

1.2 变量-目标契约

扫描工程的地基是一张契约表:扫哪些变量、每个变量的取值集合、看哪些目标量、单位是什么。契约最少含:

variable: name(规范路径), values[] 或 (min, max, step) 或 distribution target: name(规范路径), unit, aggregation(last/mean/max) fixed: 保持不变的字段(模板里已设好,扫描时不动) mode: rating / simulation / design(决定变量可写性)

为什么它是地基:没有契约,“扫描"就退化成"随手改数”。契约让扫描可复现、可审计、可交给同事;它还是断点账本的 schema 来源。

1.3 全因子 vs 拉丁超立方

方法特点维度爆炸适合
全因子(full factorial)每个变量每个水平都组合,覆盖完整极快(k^n)变量少(2~3 个)、水平少、要完整响应面
拉丁超立方(LHS)每维分层采样,样本在空间中分布均匀线性(给定样本数)变量多、只要趋势/统计特征

反直觉点:全因子听起来"最完整",但 5 个变量各 5 水平 = 3125 个案例——在桌面 OLE 上按每个案例几十秒到几分钟算,是数天到数周的量级。先算清"案例数 × 单案例耗时",再决定采样方法,这是扫描设计的第一颗刹车。

1.4 批量状态机

单个案例的四个状态:pending(待跑)→running(运行中)→done(成功)或timeout/failed。状态机要点:

  • 超时即转 timeout,不阻塞后续案例;
  • 重试有上限(如 1 次),避免"永远重试";
  • 失败分类:可重试(超时、偶发 COM 错误)vs 不可重试(字段写错、模式不匹配);
  • 每案例记账,账本即状态机的外部存储。

1.5 断点账本

断点续扫的关键:每完成(或失败)一个案例就落一次账。脚本重启时先读账本,跳过已经是done的案例,只补pending/timeout/failed。

最佳实践:账本写"追加 + 幂等"。同一个案例编号允许多条记录,取最新一条为当前状态——这样"重跑"天然安全,不会产生重复行歧义。

1.6 扫描设计的三条刹车

为什么这对你重要:参数扫描最容易犯的错是"直接开跑",跑到一半发现案例数失控、或者结论根本无法解释。三条刹车必须在设计阶段踩下。

刹车一:先算"案例数 × 单案例耗时"。全因子是 k^n,5 个变量各 5 水平就是 3125 个案例;若单案例 90 秒,串行约 78 小时——这不叫"扫一下",叫"跑一周"。算不出这个数就不许按 Run。

刹车二:先圈定"哪些变量真的需要扫"。第 03 篇说过,Xist 自带 Grid design option(按步长扫几何)与 Smart Design(启发式找最优)。凡是程序内能替你扫的,就别在外层脚本再扫一遍——外层扫描应聚焦"程序内做不了的组合"(如跨模块、跨工况、带外部数据)。变量裁剪的先问:"改这个变量,工程上会做出不同决策吗?"会,才扫。

刹车三:先定"看完结果做什么"。扫描的产物是"决策依据"而非"数据量"。开始前就要写下:看哪个目标量、超过什么阈值算关注、不同结论分别触发什么动作。没有这三句的扫描,跑完只会得到一张"没人会用"的表。

一条纪律:模板只读。扫描的每一个案例都从同一份原始模板派生(第 07 篇的"绝不覆盖模板"),模板置为只读或单独备份。模板一旦被某次运行污染,后续所有案例都是"垃圾进垃圾出",而且这种错误不会报错。

二、完整代码与逐行剖析

代码 8-1:扫描契约与案例矩阵生成

# -*- coding: utf-8 -*-""" sweep_contract.py —— 定义扫描契约并生成案例矩阵(不触碰 HTRI) 用法:python sweep_contract.py 输出:cases.csv(案例矩阵,含 case_id 与各变量取值) """importitertoolsimportcsvimportjson# 扫描契约(示例性建模)CONTRACT={"template":"template.htri","mode":"rating","variables":{"geometry.exchanger.shell_id":[700.0,800.0,900.0],# mm"geometry.tube_geometry.tube_length":[5000.0,6000.0],# mm"geometry.exchanger.baffle_spacing":[250.0,300.0,350.0],# mm},"targets":{"outputs.summary.overall_u":{"unit":"W/(m2·K)","agg":"last"},"outputs.summary.shell_dP":{"unit":"kPa","agg":"last"},},"method":"full_factorial",# full_factorial / lhs"disclaimer":"示例性建模,不代表任何标准规定,不对应任何真实装置",}defgen_full_factorial(variables):keys=list(variables.keys())forcomboinitertools.product(*(variables[k]forkinkeys)):yielddict(zip(keys,combo))defmain():rows=[]fori,comboinenumerate(gen_full_factorial(CONTRACT["variables"]),start=1):row={"case_id":f"case_{i:04d}"}row.update(combo)rows.append(row)withopen("cases.csv","w",newline="",encoding="utf-8-sig")asf:w=csv.DictWriter(f,fieldnames=list(rows[0].keys()))w.writeheader()w.writerows(rows)withopen("sweep_contract.json","w",encoding="utf-8")asf:json.dump(CONTRACT,f,ensure_ascii=False,indent=2)n=len(rows)print(f"案例矩阵:{n}个案例 -> cases.csv")print(f"提示:请先估算{n}× 单案例耗时,再决定是否改用拉丁超立方。")if__name__=="__main__":main()

逐行剖析:

  • 契约把"变量取值集合"与"目标量"分开列:variables是自变量,targets是观测量。目标的agg(聚合方式)字段很重要——同一目标可能有多处值(总览/局部),需声明取哪个。
  • gen_full_factorial用itertools.product生成笛卡尔积:3×2×3 = 18 个案例,是个"能一眼看懂"的小例子。
  • case_id用零填充编号(case_0001):保证字符串排序与数值排序一致,账本里好排查。
  • sweep_contract.json落盘:契约本身也是待审计产物(第 10 篇的落盘纪律)。
  • 打印"先估算实例数与耗时":把 1.3 节的刹车机制写进脚本输出,提醒使用者别一头冲进维度爆炸。

代码 8-2:带断点账本的批量扫描器(占位符)

# -*- coding: utf-8 -*-""" sweep_xist.py —— Xist 参数扫描 + 断点续扫(占位符,须替换真实标识符) 用法:python sweep_xist.py 依赖:cases.csv、sweep_contract.json、datadict.csv(real_identifier 已填) 账本:ledger.csv(追加 + 幂等,取每个 case_id 的最新记录为当前状态) """importcsvimportjsonimporttimeimportdatetimefromdrive_caseimportsession,set_field,load_writable_map,cleanup_leftover PROGID="<HTRIAutomationServer.ProgID(本机枚举所得)>"OP_LOAD="<打开案例的方法(探测所得)>"OP_RUN="<运行案例的方法(探测所得)>"OP_SAVE="<另存案例的方法(探测所得)>"RUN_TIMEOUT_S=300MAX_RETRY=1defload_ledger(path="ledger.csv"):"""读账本 -> {case_id: 最新状态}"""state={}try:forrincsv.DictReader(open(path,encoding="utf-8-sig")):state[r["case_id"]]=r# 后出现覆盖先出现 = 取最新exceptFileNotFoundError:passreturnstatedefappend_ledger(path,case_id,status,detail,results=None):header=["case_id","status","detail","timestamp","results_json"]exists=Truetry:open(path,encoding="utf-8-sig").close()exceptFileNotFoundError:exists=Falsewithopen(path,"a",newline="",encoding="utf-8-sig")asf:w=csv.DictWriter(f,fieldnames=header)ifnotexists:w.writeheader()w.writerow({"case_id":case_id,"status":status,"detail":detail,"timestamp":datetime.datetime.now().isoformat(timespec="seconds"),"results_json":json.dumps(resultsor{},ensure_ascii=False),})defrun_one(case_row,contract,wmap,out_dir):"""跑单个案例,返回 (status, results, detail)"""case_id=case_row["case_id"]skipped=[]withsession(PROGID,RUN_TIMEOUT_S+60)as(app,start):case=getattr(app,OP_LOAD)(contract["template"])forvar_pathincontract["variables"]:set_field(case,var_path,float(case_row[var_path]),contract["mode"],wmap,skipped)getattr(case,OP_RUN)()iftime.time()-start>RUN_TIMEOUT_S:return"timeout",{},"运行超时"results={}fortgtincontract["targets"]:entry=wmap.get(tgt)ifnotentry:continuenode=caseforpartinentry["ident"].split("."):node=getattr(node,part)results[tgt]=getattr(node,"<叶子属性名(探测所得)>")getattr(case,OP_SAVE)(f"{out_dir}/{case_id}.htri")ifskipped:return"done",results,f"跳过{len(skipped)}个字段"return"done",results,""defmain():contract=json.load(open("sweep_contract.json",encoding="utf-8"))cases=list(csv.DictReader(open("cases.csv",encoding="utf-8-sig")))wmap=load_writable_map()state=load_ledger()forrowincases:cid=row["case_id"]cur=state.get(cid)ifcurandcur["status"]=="done":continue# 断点续扫:跳过已完成attempt=0whileattempt<=MAX_RETRY:try:status,results,detail=run_one(row,contract,wmap,"out")breakexceptExceptionase:# noqa: BLE001status,results,detail="failed",{},f"{type(e).__name__}:{e}"attempt+=1append_ledger("ledger.csv",cid,status,detail,results)print(f"{cid}:{status}{detail}")cleanup_leftover()print("扫描结束。结果是 ledger.csv 中每个 case_id 的最新记录。")if__name__=="__main__":main()

逐行剖析:

  • load_ledger用"后出现覆盖先出现"取最新记录:实现"追加 + 幂等"的账本语义,重跑安全。
  • append_ledger每次只追加一行:崩溃也不会损坏已写数据,这是断点续扫能成立的前提。
  • run_one把"模式守卫(复用set_field)→ run → 超时判定 → 取目标 → 另存"封装成一次完整生命周期,直接复用第 07 篇的组件——组件化让本篇只需关心"扫描逻辑"。
  • while attempt <= MAX_RETRY把重试限定为有限次:避免"永远重试"把批处理拖死;可重试/不可重试的分类,实际工程里应按异常类型细化。
  • 跳过status == "done"的案例:断点续扫的核心动作,一行搞定。
  • 结果写进账本的results_json列:账本既是状态机、又是结果暂存;第 10 篇会把它规范化为结果长表。

三、常见报错与排查

报错 3-1:扫描跑了几十个案例后,HTRI 实例堆积、机器变卡。
现象:进程数随时间增长。根因:某条异常路径没走到释放。解法:确保所有运行都包在session里;每 N 个案例调一次cleanup_leftover清点;必要时把批处理拆成多个短进程(每个进程跑几十个案例后退出)。

报错 3-2:某个案例反复失败,脚本卡在那一个。
现象:批处理停住。根因:重试无上限,或该案例触发了桌面弹窗阻塞。解法:设MAX_RETRY(如 1);把不可重试的错误分类出来记failed后继续;对弹窗类阻塞,超时后转timeout不强等。

报错 3-3:重跑扫描后结果重复/矛盾。
现象:结果表里同一case_id出现矛盾值。根因:账本语义没约定"取最新",或结果既写账本又写独立文件造成两处不一致。解法:约定"账本取最新记录",结果落盘以账本为唯一真相源(第 10 篇规范化)。

报错 3-4:扫描结果整体偏移,像被"平移"了。
现象:趋势对但数值系统性偏差。根因:模板被save覆盖,后续案例基于被改过的模板派生。解法:断言out_case != template;模板置为只读;每轮从原始模板 load。

报错 3-5:全因子案例数远超预期,跑不完。
现象:矩阵 3000+ 案例。根因:维度爆炸没提前估算。解法:先按 1.3 节估算实例数×耗时;改用 LHS 采样;或先在程序内用 Xist 的 Grid design option 缩小范围,再对关键点做脚本扫描。

四、动手练习

  • 练习 1(契约与矩阵):修改代码 8-1 的variables为你的场景,运行生成cases.csv。判定:案例数 = 各变量取值数之积(全因子);case_id连续且零填充。
  • 练习 2(案例数估算):在笔记里写下"案例数 × 单案例实测耗时"的估算值,并据此决定是否改用 LHS。判定:能给出具体数字(如 18 × 90 s ≈ 27 min),并说明结论。
  • 练习 3(断点续扫):跑代码 8-2 到一半时用 Ctrl+C 中断,再重跑。判定:重跑时已完成案例被跳过(账本中done的case_id不再出现在运行日志);最终ledger.csv每个case_id的最新记录为done。
  • 练习 4(失败注入):故意把一个变量值设成非法值(如负壳径)跑一次。判定:该案例记为failed或timeout,但整批继续;账本中其余案例正常done。

五、小结与下一篇预告

本篇把生命周期骨架套进真实批量场景:契约先行(变量-目标-模式)、采样方法按案例数决定(全因子 vs LHS)、状态机(pending/running/done/timeout/failed + 有限重试)、断点账本(追加 + 幂等 + 取最新)。产出sweep_contract.json、cases.csv、ledger.csv三件套,为平台化打下骨架。同时明确了与官方 Parametric Study Tool 的分工:一次性用官方,生产化自建。

第 09 篇《结果取数》:扫描产出了账本,但"逐字段读对象"只是取数的一条路。我们要系统比较对象模型取数与报表导出件解析两条路,讲清 summary/detailed 报表的结构差异、多单位集的换算纪律,并把取数结果规范化为可分析的 DataFrame。

本篇认知问题回显(FAQ)

Q1:官方 Parametric Study Tool 已能批量跑案例,为什么还要自建扫描脚本?
A:官方工具基于 Excel 与 OLE、按模板*.htri批量改参数,上手成本低,适合一次性灵敏度研究;但它灵活性受工具设计约束、与外部系统集成弱、无人值守与调度能力一般。自建脚本底层走同一条 Automation Server 通道,胜在可任意组合变量、接优化器/数据库/报表管道、进计划任务与 CI,适合平台级批量核算与参数辨识。

Q2:规范的变量-目标契约应该长什么样?
A:至少含四类信息:变量(规范路径 + 取值集合或范围/分布)、目标(规范路径 + 单位 + 聚合方式 last/mean/max)、固定项(模板已设、扫描不动)、模式(rating/simulation/design,决定变量可写性)。契约是扫描可复现、可审计、可交接的地基,也是断点账本 schema 的来源。

Q3:全因子与拉丁超立方各适合什么场景?
A:全因子(full factorial)覆盖完整,适合变量少(2~3 个)、水平少、需要完整响应面的场景,但案例数随维度呈 k^n 爆炸;拉丁超立方(LHS)每维分层采样、样本分布均匀,案例数线性可控,适合变量多、只取趋势或统计特征的场景。决策前先算"案例数 × 单案例耗时"。

Q4:批量状态机怎么设计才既跑得完又不失控?
A:单案例四状态 pending→running→done/timeout/failed;超时即转 timeout 不阻塞后续;重试设上限(如 1 次)避免永远重试;区分可重试(超时、偶发 COM 错误)与不可重试(字段写错、模式不匹配);每个案例完成或失败立即落一次账,账本即状态机的外部存储。

Q5:断点账本为什么关键?
A:账本让扫描"敢中断、能续跑"。做法是每个案例完成(或失败)即追加一行记录,脚本重启时先读账本、跳过done的案例,只补 pending/timeout/failed。约定"同一 case_id 取最新记录"实现幂等,重跑天然安全,不会产生重复行歧义,也便于失败注入演练后回看每案例状态。

返回列表