☰
HTRI二次开发教程(07):输入读写与运行控制——load、change、run、retrieve、save 生命周期
2026/9/30 8:48:48 网站建设 项目流程

HTRI二次开发教程(07):输入读写与运行控制——load、change、run、retrieve、save 生命周期

版本与事实声明

  • 版本锚点:当前Xchanger Suite 9.4;官方 Automation Server 演示环境为 Visual Studio 2013 + .NET/C#。9.4 增强项含"run selected cases individually"(可单选中案例运行),对批量脚本有参考意义。
  • 示例代码中的 ProgID、方法名、属性名一律为占位符,必须用第 05/06 篇探测所得真实标识符替换后才可运行;本系列不写死任何 API 字符串。
  • 文中所有数值均为示例性建模,不代表任何标准规定,不对应任何真实装置。

一句话结论:HTRI 自动化的骨架就是官方演示的五动作——load(打开案例)→ change(改输入)→ run(运行)→ retrieve(取输出)→ save(保存);把它落成可复用脚本的关键不是"记住方法名",而是三件事:模式守卫(只写当前模式可写的字段)、运行完成判定(区分"跑完了"和"跑对了")、释放三步(超时 +try/finally+ 残留进程清理)。

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

  • Q1:官方演示的五个动作,在真实工程里对应怎样一条生命周期?
  • Q2:打开案例后,为什么"当前活动案例"是必须显式管理的隐式状态?
  • Q3:改输入时,"模式守卫"和"输出字段只读"两条规则怎么落到代码里?
  • Q4:运行触发了,怎么判断它是"跑完了"还是"跑对了"?
  • Q5:为什么释放三步(超时、try/finally、清理残留进程)是驱动桌面 OLE 的铁律?

一、机制解析

1.1 官方五动作 → 工程生命周期

官方 TechTip 明确列出 Automation Server 能做的五件事:loading an existing case、changing input data in a case、running a case、retrieving output data from a case、saving a case。映射到工程:

[准备] 许可/环境检查 → 定位模板案例 ↓ [load] 打开案例(副本!) ↓ [change] 按模式守卫写入输入字段 ↓ [run] 触发行计算 → 判定完成与收敛 ↓ [retrieve] 读取输出字段(仅在完成后) ↓ [save] 另存为派生案例(绝不覆盖模板) ↓ [释放] 断开连接 → 清残留进程

为什么这对你重要:这七步就是后续所有脚本(第 08/09/10/16/17 篇)的公共骨架。把骨架写对一次,参数扫描、批量校核、平台化都只是在这个骨架上换"改什么、扫什么"。

1.2 显式会话:拒绝"当前活动案例"

官方 Features 页称软件可"同时打开多个案例文件"。这在批处理里是陷阱:如果你的代码依赖"当前活动案例"这种隐式状态,一旦有并行会话或异常恢复,你极可能把 A 案例的输入写到 B 案例、把 B 的输出记到 A 名下。

最佳实践:一个案例一个显式句柄。用变量持有案例对象,所有读写都通过它进行,禁止任何"再取一次当前案例"的操作。异常恢复时,凭句柄判断"我操作的是谁"。

1.3 模式守卫与输出字段只读

第 04 篇的Node.writable[mode]与"输出字段所有模式皆不可写"两条规则,在 change 阶段必须强制执行:

  • 写之前查writable[mode],为假则跳过并记录(而不是抛异常中断整批);
  • 输出字段(如out.summary.overall_u)永不在 change 阶段出现;
  • 遇到"留空由能量平衡补算"的量,显式声明"本次给还是不给"。

反直觉点:跳过不等于忽略。跳过要记账——每批任务结束后统计"哪些字段因模式不匹配被跳过",这是发现"我是不是理解错了模式"的重要信号。

1.4 运行完成判定:跑完了 ≠ 跑对了

触发行计算只是"发起"。判定运行结果要分两层:

  1. 完成判定:进程/计算是否结束(超时是重要信号);
  2. 质量判定:是否有警告(warning)、是否收敛、结果是否物理合理。

官方 Xist 提供振动" screening 警告"、局部/整体结果报表等;质量判定这些信号在哪、叫什么,依赖本机探测(U2)。工程上至少要做到:默认不信任"返回了就算成功",而是去读"警告/状态"类输出字段或报表。

1.5 释放三步(铁律 6)

桌面 OLE 自动化最典型的故障是挂死与残留进程。因此铁律 6:任何驱动桌面 OLE 的代码必须含:

  1. 超时:给整个会话设上限(如总时长 + 单次运行上限);
  2. try/finally释放:无论成功失败,都执行断开连接;
  3. 残留进程清理:任务结束后清点进程,清理没退干净的实例。

这三步不是"锦上添花",而是"批量任务能否无人值守"的分水岭。

1.6 为什么五步的顺序不能变

为什么这对你重要:生命周期里最贵的错误,往往不是"某一步写错了",而是"顺序错了"——因为顺序错误通常不报错,只给你一个看起来正常的错结果。

四条顺序铁律:

  • load 必须在 change 之前:没有案例句柄,你改的是空气;
  • change 必须在 run 之前:run 是对"当前输入"的求解,改完才跑;
  • retrieve 必须在 run 完成之后:这是第 09 篇纪律一的根源——早取数拿到的是上一轮;
  • save 必须用新路径:覆盖模板会让后续所有派生案例错乱(第 08 篇"模板只读"的根源)。

一条反直觉结论:"顺序"比"正确性"更难保证。语法错误会立刻报错,顺序错误会静默传播——你会在几天后发现"整批结果都偏移了",却找不到是哪一步错了。所以生命周期骨架应该写死成函数、只允许按序调用,而不是散在业务代码里靠自觉。这正是代码 7-1 把它封装成session+run_one的原因。

二、完整代码与逐行剖析

代码 7-1:完整生命周期脚本(占位符 + 释放三步)

# -*- coding: utf-8 -*-""" drive_case.py —— 单案例 OLE 生命周期:load→change→run→retrieve→save 用法:python drive_case.py "template.htri" "output_case.htri" 【重要】所有 <...> 标识符必须换成第 05/06 篇探测所得真实值。 工程护栏:模式守卫 + 完成判定 + 释放三步(铁律 6)。 """importsysimporttimeimportjsonimportsubprocessimportwin32com.clientaswcfromcontextlibimportcontextmanager PROGID="<HTRIAutomationServer.ProgID(本机枚举所得)>"OP_LOAD="<打开案例的方法(探测所得)>"OP_RUN="<运行案例的方法(探测所得)>"OP_SAVE="<另存案例的方法(探测所得)>"SESSION_TIMEOUT_S=600# 整个会话上限(秒)RUN_TIMEOUT_S=300# 单次运行上限(秒)defload_writable_map(path="datadict.csv"):"""从数据字典读入 规范路径 -> (real_identifier, writable[mode])。"""importcsv out={}forrincsv.DictReader(open(path,encoding="utf-8-sig")):ident=r.get("real_identifier","").strip()ifnotident:continue# 未探测确认的字段一律不进映射out[f'{r["group"]}.{r["field"]}']={"ident":ident,"w":{"rating":r["writable_rating"]=="True","simulation":r["writable_simulation"]=="True","design":r["writable_design"]=="True",},}returnoutdefset_field(case,node_path,value,mode,wmap,skipped):"""按模式守卫写入一个字段;不可写则记录并跳过。"""entry=wmap.get(node_path)ifentryisNone:skipped.append((node_path,"未在数据字典登记"))returnifnotentry["w"].get(mode,False):skipped.append((node_path,f"模式{mode}下不可写"))returnnode=caseforpartinentry["ident"].split("."):# 逐层下降(真实标识符路径)node=getattr(node,part)setattr(node,"<叶子属性名(探测所得)>",value)@contextmanagerdefsession(progid,timeout_s):"""会话上下文:负责创建、超时守护与 finally 释放。"""ifprogid.startswith("<"):raiseRuntimeError("PROGID 仍是占位符:请先完成探测")app=wc.gencache.EnsureDispatch(progid)start=time.time()try:yieldapp,startfinally:# 释放:无论成功失败都尝试断开try:app=None# 释放引用触发 COM 释放exceptException:# noqa: BLE001passdefcleanup_leftover():"""清点可能的残留进程(只做只读清点;是否结束由用户决定)。"""try:out=subprocess.run(["tasklist","/FO","CSV","/NH"],capture_output=True,text=True,timeout=30)lines=[lforlinout.stdout.splitlines()if"htri"inl.lower()or"xchanger"inl.lower()]iflines:print(f"[警告] 检测到{len(lines)}个疑似残留进程(关键字 htri/xchanger):")forlinlines[:10]:print(" "+l)print(" 请人工确认后处理;不要在未确认时批量结束进程。")else:print("[OK] 未发现明显残留进程。")exceptExceptionase:# noqa: BLE001print(f"[提示] 进程清点跳过:{e}")defmain():iflen(sys.argv)<3:print('用法:python drive_case.py "template.htri" "output_case.htri"')returntemplate,out_case=sys.argv[1],sys.argv[2]mode="rating"# 本次运行意图(示例)wmap=load_writable_map()skipped=[]withsession(PROGID,SESSION_TIMEOUT_S)as(app,start):case=getattr(app,OP_LOAD)(template)# load(用副本!)# change:示例写入(值均为示例性建模)set_field(case,"geometry.exchanger.shell_id",800.0,mode,wmap,skipped)set_field(case,"process.duty",1500.0,mode,wmap,skipped)# run:发起并等待(真实 API 形式以探测为准)getattr(case,OP_RUN)()iftime.time()-start>RUN_TIMEOUT_S:raiseTimeoutError("运行超时")# retrieve:完成后再取(示例:从输出分组逐字段读取)results={}fornorm_path,entryinwmap.items():ifnotnorm_path.startswith("outputs."):continue# 只取输出分组node=caseforpartinentry["ident"].split("."):node=getattr(node,part)results[norm_path]=getattr(node,"<叶子属性名(探测所得)>")print(f"[retrieve] 取到{len(results)}个输出字段")getattr(case,OP_SAVE)(out_case)# save(另存,不覆盖模板)cleanup_leftover()ifskipped:print("[跳过字段]")forpath,whyinskipped:print(f" -{path}:{why}")if__name__=="__main__":main()

说明:为控制篇幅,代码中retrieve段用了一个占位骨架——真实取数应逐字段读取输出节点(第 09 篇给出完整实现),此处只强调"取数必须发生在 run 完成之后"。

逐行剖析:

  • load_writable_map只收real_identifier非空的条目:未经探测确认的字段不进映射,从机制上杜绝"用猜的标识符编码"。
  • set_field的两级守卫:先查数据字典是否登记、再查模式是否可写;不可写就skipped.append并静默跳过——批量任务里"跳过并记账"比"抛异常中断整批"友好得多。
  • session用@contextmanager把"创建 + 超时起点 + finally 释放"封装起来:这是铁律 6 前两步的落地;app = None释放引用是 pywin32 释放 COM 对象的常用手法。
  • cleanup_leftover只做只读清点并提示人工确认:脚本不擅自结束进程——误杀他人会话是安全事故,工程纪律要求"只报警不越权"。
  • OP_LOAD/OP_RUN/OP_SAVE与叶子属性名都是占位符:官方演示告诉我们"有这五个动作",但具体方法名要探测(U2)。
  • out_case与template分离:绝不覆盖模板——模板是批处理的基准,一旦被改写,后续全部派生案例都会错。

代码 7-2:超时守护装饰器(独立可运行)

# -*- coding: utf-8 -*-""" with_timeout.py —— 给任意阻塞型调用加超时(Windows 采用线程 + 兜底标记) 说明:OLE 调用是阻塞的,Python 无法直接强杀线程;本脚本提供 "软超时"——到点后抛出 TimeoutError 让上层释放会话并清理进程。 """importthreadingimportfunctoolsimporttimedefsoft_timeout(seconds):defdeco(fn):@functools.wraps(fn)defwrapper(*args,**kwargs):result={}deftarget():try:result["ok"]=fn(*args,**kwargs)exceptExceptionase:# noqa: BLE001result["err"]=e t=threading.Thread(target=target,daemon=True)t.start()t.join(seconds)ift.is_alive():# 无法强杀线程,交由上层释放 COM + 清理进程raiseTimeoutError(f"{fn.__name__}超过{seconds}s 未返回")if"err"inresult:raiseresult["err"]returnresult.get("ok")returnwrapperreturndeco@soft_timeout(2)defdemo_blocking():time.sleep(5)return"done"if__name__=="__main__":print("演示:2 秒超时对 5 秒阻塞函数的判定")try:demo_blocking()print("未超时(不应出现)")exceptTimeoutErrorase:print("已按预期超时:",e)

逐行剖析:

  • 必须承认的机制限制:Python 不能强行终止一个正在阻塞的线程。所以这里是"软超时"——到点就抛错,把清理交给上层。诚实标注限制比假装"能强杀"更工程。
  • daemon=True:让超时后的守护线程不阻止主程序退出;但线程仍在跑,因此上层必须做进程清理(cleanup_leftover)。
  • 异常经result["err"]回传再重抛:把子线程异常带回主线程,避免"子线程报错、主线程以为没事"。
  • 独立可运行:这段代码零外部依赖,任何机器都能跑通,验证超时判定逻辑本身是否工作。

三、常见报错与排查

报错 3-1:任务跑完后进程还在,越跑越多。
现象:批量循环后tasklist里 HTRI 实例堆积。根因:没有在finally里释放 COM 引用,或异常路径跳过了释放。解法:用session上下文管理器(代码 7-1)保证 finally 必执行;任务后跑cleanup_leftover清点。

报错 3-2:com_error: 服务器运行失败 (RPC_E_SERVERFAULT)或调用挂死。
现象:某次run长时间不返回。根因:桌面 OLE 调用的典型阻塞;也可能是案例本身算不动或弹了对话框。解法:加软超时(代码 7-2),超时后释放会话并清理进程;注意桌面交互弹窗会阻塞无人值守,批处理机应避免需要人工确认的弹窗(以本机行为为准)。

报错 3-3:脚本覆盖了模板案例,后续全部派生案例错乱。
现象:模板被改写,扫描结果整体偏移。根因:save写到了模板路径。解法:强制"load 模板 + save 到全新路径";可在脚本里断言out_case != template。

报错 3-4:写入字段无异常,但结果不变。
现象:set_field不报错,结果没动。根因:模式不可写(已被守卫跳过)或写入的是只读节点。解法:查看skipped输出;对照datadict.csv的 writable 三列;若确应可写,用第 06 篇测绘复核real_identifier是否指向了正确叶子。

报错 3-5:取输出取到的是上一轮的结果。
现象:本轮改了输入,读出的结果与上轮相同。根因:在 run 完成前就取数,或复用了上轮会话。解法:取数严格排在 run 完成后;每轮用独立会话/独立案例句柄,禁止跨轮复用。

四、动手练习

  • 练习 1(骨架跑通):用探测所得真实标识符替换代码 7-1 的占位符,对一个副本案例跑通 load→change→run→retrieve→save。判定:生成派生案例文件;模板文件修改时间不变。
  • 练习 2(模式守卫):故意在同一案例上以 rating 模式写入一个"仅 simulation 可写"的字段。判定:脚本不报错中断,skipped中出现该字段及原因"模式 rating 下不可写"。
  • 练习 3(超时演练):运行代码 7-2。判定:控制台打印"已按预期超时",且在 2 秒左右返回(不是 5 秒)。
  • 练习 4(释放三步复核):在一次完整运行后执行cleanup_leftover。判定:输出为"[OK] 未发现明显残留进程";若报"检测到残留",能解释是哪一步没释放并修复。

五、小结与下一篇预告

本篇把官方五动作落成了一条带工程护栏的生命周期:load 用副本、change 带模式守卫并记账、run 判定完成与质量、retrieve 严格在完成后、save 另存不覆盖;并用try/finally+ 软超时 + 残留进程清点实现释放三步(铁律 6)。骨架对了,后面所有批量场景都是"换变量、换目标"。

第 08 篇《实战一:Xist 参数扫描》:我们把这条骨架套进一个真实的批量场景——对管壳式换热器做全因子/拉丁超立方参数扫描,配上变量-目标契约、批量状态机(提交→运行→超时→重试)与断点账本,并对照官方 Parametric Study Tool 说明"何时用官方工具、何时自己写"。

本篇认知问题回显(FAQ)

Q1:官方五动作在工程里对应怎样一条生命周期?
A:load(打开案例)→ change(按模式守卫改输入)→ run(触发行计算并判定完成/收敛)→ retrieve(仅在完成后取输出)→ save(另存为派生案例,绝不覆盖模板),前后各加"准备(许可/路径检查)“与"释放(断开连接 + 清残留进程)”。这套七步骨架是第 08/09/10/16/17 篇共用的公共结构。

Q2:为什么"当前活动案例"必须显式管理?
A:官方称软件可同时打开多个案例文件;若代码依赖"当前活动案例"这种隐式状态,一旦有并行会话或异常恢复,就可能把 A 的输入写到 B、把 B 的输出记到 A。最佳实践是一个案例一个显式句柄,所有读写都经该句柄进行,禁止"再取一次当前案例"。

Q3:模式守卫与输出字段只读怎么落代码?
A:写字段前先查数据字典的writable[mode],为假则跳过并记入skipped清单(静默跳过并记账,而非抛异常中断整批);输出字段(如out.summary.overall_u)永不出现在 change 阶段。set_field还强制要求该字段已在数据字典登记且real_identifier非空,从机制上杜绝用猜的标识符编码。

Q4:怎么判断运行是"跑完了"还是"跑对了"?
A:分两层判定。完成判定看计算是否结束,超时是关键信号;质量判定要看是否有警告、是否收敛、结果是否物理合理(Xist 有振动 screening 警告与各类结果报表,具体字段以本机探测为准)。默认不信任"有返回即成功",而应显式读取状态/警告类输出。

Q5:为什么释放三步是驱动桌面 OLE 的铁律?
A:桌面 OLE 调用最典型的故障是挂死与残留进程堆积。三步为:给会话与单次运行设超时(Python 无法强杀阻塞线程,故为"软超时",到点抛错交由上层清理);用try/finally(代码中用 session 上下文管理器)保证任何路径都释放 COM 引用;任务后清点残留进程。脚本只做只读清点并提示人工确认,不擅自结束进程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询