AFT二次开发教程(03):第一个可运行的自动化算例——三场景 + 批量运行 + Excel 取数一条龙
版本与事实声明
- 产品与版本:AFT Fathom 15 / AFT Impulse 12(当前);本篇操作细节引用官方Fathom 13帮助文档正文(菜单项与行为在版本间稳定),当前版界面以官方文档为准。
- 语言/环境:Python 3.x(pandas + openpyxl);AFT Fathom 桌面端 + Excel。
- 本文目标:读完能独立跑完"三个工况 → 一次批跑 → 一份结构化结果表"的最小闭环,并留下可对账的基准值。
- 所有水力数值为示例性建模,不代表任何标准规定,亦不对应任何真实装置。
- 许可与价格以 Datacor 官方渠道为准。
一句话结论:AFT 最小可用自动化闭环 = 在Scenario Manager里把 N 个工况建成同一模型文件内的场景 + 用Excel Export Manager(File 菜单)预先指定导出到哪张表的哪个单元格 + 用File > Start Batch Run选Scenarios in Current Model后台批跑并勾选Save Using Excel Export Manager,最后用 pandas 读回导出工作簿——这条闭环的产物就是你后续所有脚本的"对账真值"。
〇、本篇要解决的认知问题
- Q1:Scenario Manager、Start Batch Run、Excel Export Manager 三者的正确使用顺序是什么?顺序错了会怎样?
- Q2:为什么"一个工况一个
.fth文件"是反模式?同一文件内多场景好在哪? - Q3:Excel Export Manager 里Excel Starting Cell / Excel Ending Cell两个字段为什么是防错设计?
- Q4:批跑模式下的 Excel 导出,与手工单跑时有什么官方明确的行为差异?
- Q5:怎么为一个自动化结果建立"手工基准线",并在后续脚本里持续对账?
一、机制解析
3.1 三件套的正确顺序(这一步最容易做反)
把三件套当柱子,正确施工顺序是:
① 建场景树 ② 配导出项 ③ 批跑 ④ 取数对账 Scenario Manager → Excel Export → Start Batch Run → pandas 读回 (先有工况骨架) Manager(先备好出口) (一次跑完所有场景) (落成长表)为什么导出项必须排在批跑之前:官方原文明确,“The Excel Export option can only be chosen if the batch run type is for Scenarios in Current Model.Additionally, the desired output must first be configured in the Excel Export Managerunder the File menu for any data to be exported.” 翻译成人话:批跑对话框里那个"Save Using Excel Export Manager"开关,只有在 Excel Export Manager 里已经配好导出项时才有意义。先批跑、后配导出 = 白跑一遍。
为什么先建场景树:Batch Run Type里选Scenarios in Current Model的前提是"当前模型有场景";官方还补了一句——如果当前模型没有场景,就只有"Models from Different Files"这一种类型可选。也就是说:场景树决定了你有没有资格用最省事的那种批跑方式。
3.2 为什么不"一工况一文件"
| 组织方式 | 文件系统 | 继承与复用 | 官方对比能力 | 结论 |
|---|---|---|---|---|
一工况一.fth | N 个文件,容易版本错乱 | 无 | 只能靠Export Model Data出.inp文本手工 diff | 反模式 |
| 同文件多场景 | 1 个.fth | linked 属性跨代继承 | Scenario Comparison Tool / Grid | 推荐 |
官方对 Scenario Manager 的能力描述里,"Pass changes from a scenario to its variants(把改动从一个场景传递到其变体)"与"See what scenarios have output by automatically changing the text color to blue if they have output(有输出的场景文字自动变蓝)"这两条,恰恰是"一工况一文件"永远得不到的。前者让你的"改一个共同参数"变成一次操作;后者让你一眼看出哪个场景还没跑出结果——批跑前肉眼 QA 极其好用。
3.3 Starting/Ending Cell:一个被低估的防错设计
Excel Export Manager 每一项(row)都定义"数据导去哪":Excel Sheet(目标表名)+Excel Starting Cell(左上角单元格)+Excel Ending Cell(自动推算的右下角)。官方对 Ending Cell 的说明是:“The automatically calculated lower-right cell of the data…It is important to recognize that the Excel Starting Cells can be defined in a way that data gets overwritten. The Ending Cell column is a handy reference to help avoid this.”
翻译:起始单元格是可以互相压塌的——你把两项都指向A1,第二项就把第一项盖了。Ending Cell 是给你**核对"这一块占多大地方"**用的。所以 Excel Export Manager 界面下方还有一个网格preview,专门显示重叠区域。这就是为什么"导出前先目检重叠"是必须动作,也是第 06 篇要写"冲突检测脚本"的原因。
3.4 批跑模式的行为差异(官方两条)
官方在"Exporting To Excel"页专门用一节讲多场景导出的区别(铁律 9 的来源):
- 工作簿粒度:If multiple sheets are specified in the Excel Export Manager, the only way to export multiple scenarios is toexport every scenario to its own Workbook. To keep data from multiple scenarios in one Workbook,there can only be one sheetspecified.
- 保存时机:the Excel File will be saved after each scenario.In normal single-scenario exports the file is never saved, but in batch mode it is required.
含义:手工单跑导数是"不自动保存"的(你得自己在 Excel 里存);批跑模式下每个场景跑完就强制保存一次。这带来两个工程后果:(a) 你机器上会真的出现那个文件,不用手动存;(b) 如果你在批跑中途把那个工作簿用 Excel 打开着改,可能撞上文件锁——批跑期间不要打开目标工作簿。
还有一条隐藏福利:Batch Run 里有Export Only (Do Not Run)选项,可以只导出、不重跑(复用已有输出)。官方同时提醒:如果某个场景没有输出,Export Only 不会为它导出任何数据(因为不运行就不产生输出)。
3.5 手工基准线的价值
在你机器、你版本、你流体库下"人肉跑出来"的这组结果,是后续一切自动化的回归基准(regression baseline)。第 09 篇的扫描、第 17 篇的端到端项目,验收方式都是:脚本复现基准工况,误差在导出精度内一致。没有基准,批量结果错到多远你都不知道。
二、完整代码与逐行剖析
本篇代码分两段:一段规划场景表(把工况设计成数据结构,天然可复用),一段读回导出件(把 Excel 导出工作簿变成(scenario, object, parameter, unit, value)长表)。两段都自带--selftest,不依赖 AFT 即可验证链路。
代码 3-1:scenario_plan.py(工况骨架的数据化)
# -*- coding: utf-8 -*-""" scenario_plan.py —— 把"要跑几个工况、每个改什么"写成可校验的数据结构。 这是第 09 篇参数扫描的前身:先有规划的骨架,再有变更表。 运行:python scenario_plan.py --selftest """importjsonimportsys# 场景路径名遵循官方全限定格式,例:'Base Scenario\\US Units\\Pump A'SCENARIO_PLAN={"model":"demo_network.fth",# 示例性文件名"base_path":r"Base Scenario\US Units",# 全限定前缀(铁律5)"scenarios":[{"name":"Pump A Only","changes":{"P3.Fixed Speed (%)":100.0}},{"name":"Pump B Only","changes":{"P3.Fixed Speed (%)":0.0,"P4.Fixed Speed (%)":100.0}},{"name":"Pump A+B","changes":{"P4.Fixed Speed (%)":100.0}},],}deffull_path(scn_name:str)->str:"""拼出全限定 Scenario Path Name(跨场景变更必须用它,否则报场景名不唯一)。"""returnSCENARIO_PLAN["base_path"]+"\\"+scn_namedefvalidate(plan:dict)->list:"""校验:场景名唯一、路径前缀非空、每个场景至少一处变更。返回问题列表。"""problems,seen=[],set()ifnotplan.get("base_path"):problems.append("base_path 为空:跨场景变更必须用全限定路径名")forsinplan.get("scenarios",[]):ifs["name"]inseen:problems.append(f"场景名重复:{s['name']}")seen.add(s["name"])ifnots.get("changes"):problems.append(f"场景{s['name']}没有任何变更,属空场景")returnproblemsdefselftest():problems=validate(SCENARIO_PLAN)assertproblems==[],problemsassertfull_path("Pump A Only")==r"Base Scenario\US Units\Pump A Only"# 故意制造一个重复场景,验证校验器能抓到bad=json.loads(json.dumps(SCENARIO_PLAN))bad["scenarios"].append({"name":"Pump A Only","changes":{"X":1}})assertvalidate(bad),"重复场景名应被检出"print("SELFTEST OK:规划校验器工作正常(正例通过、反例被拒)。")print(json.dumps(SCENARIO_PLAN,ensure_ascii=False,indent=2))if__name__=="__main__":if"--selftest"insys.argv:selftest()逐行剖析:
SCENARIO_PLAN把"工况设计"从 GUI 操作里抽出来变成数据结构,这是本系列的贯穿手法:先在代码里定义工况,再去 GUI 落实。好处是工况可以 diff、可以 review、可以自动生成变更表。full_path()直接实现铁律 5。官方原文给的示例正是Base Scenario\US Units\Pump A这种反斜杠分隔的层级路径——注意在 Python 里用 raw string(r"...")或双反斜杠,否则\U会被解释成 Unicode 转义报错(这也是真实踩过的坑,见报错 3-3)。validate()的三条校验(路径非空、场景名唯一、非空场景)分别对应官方 Excel 导入错误表里的Scenario name is not unique、Scenario not found in the model和"空场景白跑"两类浪费。selftest()用"复制一份故意加坏数据"的写法验证校验器真的会拒绝——这是测试的正确姿势:只测正例是自欺。
代码 3-2:read_export.py(Excel 导出件 → 长表)
# -*- coding: utf-8 -*-""" read_export.py —— 读取 Excel Export Manager 产物(含多场景)→ 标准长表 输出列:scenario, object, parameter, unit, value 运行: python read_export.py --selftest python read_export.py export.xlsx --sheet Output --out long.csv """importargparseimportioimportreimportsysimportpandasaspd UNIT_RE=re.compile(r"^\s*(?P<name>.*?)\s*(?:\[(?P<unit>[^\]]+)\])?\s*$")# 'Pressure [psia]'defsplit_unit(col:str):m=UNIT_RE.match(str(col))return(m.group("name").strip(),(m.group("unit")or"").strip())ifmelse(str(col).strip(),"")defread_any(path:str,sheet=None)->pd.DataFrame:"""xlsx/csv 通吃;读 CSV 一律 utf-8-sig(VBA/Excel 产物常带 BOM)。"""ifpath.lower().endswith((".xlsx",".xlsm")):returnpd.read_excel(path,sheet_name=sheetor0,engine="openpyxl")returnpd.read_csv(path,encoding="utf-8-sig",dtype=str)defto_long(df:pd.DataFrame,scenario_col=None,object_col=None)->pd.DataFrame:"""宽表(首列=对象,其余=参数[单位])→ 长表。"""df=df.rename(columns=lambdac:str(c).strip())object_col=object_colordf.columns[0]scenario_col=scenario_color("Scenario"if"Scenario"indf.columnselseNone)recs=[]forcolin[cforcindf.columnsifcnotin(object_col,scenario_col)]:name,unit=split_unit(col)for_,rowindf.iterrows():ifpd.isna(row[object_col]):continuerecs.append({"scenario":(str(row[scenario_col]).strip()ifscenario_colelse"Base Scenario"),"object":str(row[object_col]).strip(),"parameter":name.lower().replace(" ","_"),"unit":unit,"value":row[col],})long=pd.DataFrame(recs,columns=["scenario","object","parameter","unit","value"])long["value"]=pd.to_numeric(long["value"],errors="coerce")# 'N/A'/'—' → NaN 而不崩returnlongdefselftest():demo=("Scenario,Junction,Pressure [psia],Flow [gpm]\n""Pump A Only,P3,120.5,500.0\n""Pump B Only,P3,118.2,498.6\n""Pump A Only,P4,121.0,502.1\n")df=pd.read_csv(io.StringIO(demo),dtype=str)long=to_long(df,scenario_col="Scenario",object_col="Junction")assertlen(long)==6,len(long)# 3 行 × 2 参数assertsorted(long["scenario"].unique())==["Pump A Only","Pump B Only"]assertset(long["unit"])=={"psia","gpm"}print("SELFTEST OK:3 行 × 2 参数 = 6 行长表;场景与单位列解析正确。")print(long.to_string(index=False))defmain():ap=argparse.ArgumentParser()ap.add_argument("path",nargs="?")ap.add_argument("--sheet",default=None)ap.add_argument("--out",default="long.csv")ap.add_argument("--selftest",action="store_true")a=ap.parse_args()ifa.selftestornota.path:selftest()return0long=to_long(read_any(a.path,a.sheet))long.to_csv(a.out,index=False,encoding="utf-8-sig")print(f"对象{long['object'].nunique()}个,参数{long['parameter'].nunique()}个,"f"场景{long['scenario'].nunique()}个,共{len(long)}行 →{a.out}")return0if__name__=="__main__":sys.exit(main())逐行剖析:
UNIT_RE把列名拆成"名称 + 单位",单位随行入库。这是消灭"裸数静默错误"的关键——你永远不知道一个孤零零的120.5是 psia 还是 bar,除非它旁边跟着单位。read_any对 xlsx 走 openpyxl、对 CSV 走utf-8-sig,与第 11 篇的落盘契约一致。to_long里scenario_col默认找"Scenario"列:Excel Export Manager 的导出件在配置了Other类型 / Export Guide 时可能带场景名列;没有就归到"Base Scenario",保证长表 schema 恒定。pd.to_numeric(errors="coerce"):把N/A、—、空串统一变 NaN 而不抛异常——导出件里这些符号很常见,管线必须容忍。selftest()断言len(long) == 6:3 行 × 2 参数。给你一个可量化的判定:读者改代码后重跑自检,数字不对立刻发现。
三、常见报错与排查
报错 3-1:批跑对话框里"Save Using Excel Export Manager"是灰的,选不了。
现象:想批跑直接导 Excel,选项不可点。根因:两个官方前提缺一不可——(a)Batch Run Type必须是Scenarios in Current Model;(b)必须先在 Excel Export Manager 里配好导出项(铁律:导出项先于批跑)。解法:先 File > Excel Export Manager 建至少一项并确认 Ending Cell 不与他人重叠,再开批跑。
报错 3-2:多场景导出后,工作簿里只剩最后一个场景的数。
现象:明明跑了三个场景,导出件只有一份结果。根因:Excel Export Manager 里指定了多个工作表,而官方规定"多 sheet 多场景只能每场景一个独立工作簿"。解法:要么只指定一个 sheet让所有场景进同一工作簿,要么接受"每场景一个工作簿"并在第 12 篇用聚合脚本合并。
报错 3-3:Python 里写"Base Scenario\\US Units"报 Unicode 相关错误。
现象:UnicodeDecodeError或SyntaxWarning: invalid escape sequence '\U'。根因:\U是 Python 的 Unicode 转义前缀,常规字符串里直接写会出问题。解法:用 raw stringr"Base Scenario\US Units",或双反斜杠。官方示例的场景路径正是这种反斜杠层级结构。
报错 3-4:批跑到一半打开目标 Excel 工作簿,导致导出报错/文件被锁。
现象:某个场景后开始报写入失败。根因:批跑模式下每个场景跑完就保存一次目标工作簿;此时若你正用 Excel 打开它,就撞上文件锁。解法:批跑期间不要打开目标工作簿;要看进度就等批跑结束。
报错 3-5:手工单跑导出后去找文件,找不到。
现象:单跑导出后磁盘上没有那个 xlsx。根因:官方明确"Excel Export Manager does not save Excel files for non-batch run exports"——单跑导出不自动保存,需你手动在 Excel 里存。解法:单跑时导出后立即"另存为";或干脆用批跑(就能自动保存)。
四、动手练习
- 练习 1(场景骨架):按代码 3-1 的结构,为你自己的(或示例的)模型设计 3 个场景,每个至少一处参数变更。判定:
python scenario_plan.py --selftest输出SELFTEST OK;且你手写的base_path符合Base Scenario\<单位制>\<工况名>层级。 - 练习 2(跑通闭环):在 AFT 里落实三个场景 → 配 1 项 Excel Export →
Start Batch Run后台跑 → 得到导出工作簿。判定:导出工作簿的三个场景都有数据;Excel Export Manager 的 Ending Cell 与起始单元格无重叠(界面网格无红色重叠区)。 - 练习 3(读回对账):跑
python read_export.py 你的导出件.xlsx --out long.csv。判定:长表行数 = 对象数 × 参数数 × 场景数(写出你的乘法式);unit列无空值(或空值行都能解释为无单位参数)。 - 练习 4(手工基准):挑一个基准场景,把关键结果值记入笔记并标注"示例性建模"。判定:至少 5 个关键值入库,且后续你任何脚本跑同一场景时,这些值能对回到导出精度内。
五、小结与下一篇预告
本篇跑通了 AFT 自动化的最小闭环:场景骨架数据化 → Excel Export Manager 先备好出口 → Start Batch Run 后台批跑 → pandas 读回长表。三条必须记住的机制:导出项必须先于批跑配好、多 sheet 多场景只能每场景独立工作簿、批跑模式下工作簿每场景保存一次(单跑不保存)。你现在有了第一份"手工基准线"。
第 04 篇《工程模型文件与对象模型》:我们钻进底层——四种模型扩展名(.fth/.aro/.imp/.xtr)与*.bakX-001备份级联机制、为什么模型文件虽是分节文本但绝不能手改(官方报错原文为证),以及从 AFT Transfer 参数表反推出的Pipe 与 Junction 对象类型全景。
FAQ(与第〇节一一对应)
Q1:Scenario Manager、Start Batch Run、Excel Export Manager 的正确使用顺序?
A:先建场景树(Scenario Manager),再配导出项(Excel Export Manager),再批跑(File > Start Batch Run,选 Scenarios in Current Model 并勾 Save Using Excel Export Manager),最后读回导出件;顺序反了会白跑一遍,因为批跑对话框的 Excel 导出开关只有在已配置导出项时才可用。
Q2:为什么一个工况一个 .fth 文件是反模式?
A:因为同文件多场景能拿到 Scenario Manager 独有的 linked 属性跨代继承与 Scenario Comparison 对比能力,还能用文字变蓝一眼看出哪些场景有输出;一工况一文件则文件系统易乱、无继承、只能靠导出 .inp 文本手工 diff。
Q3:Excel Starting Cell 与 Ending Cell 是什么作用?
A:Starting Cell 指定数据导到的左上角单元格,Ending Cell 是自动推算的右下角单元格;官方明确起始单元格可被定义成互相覆盖,Ending Cell 与界面预览网格就是用来避免数据被覆盖、检查重叠区域的。
Q4:批跑模式下的 Excel 导出与单跑有什么官方差异?
A:两点:若指定多个工作表,多场景导出只能每场景一个独立工作簿(要进同一工作簿只能指定一个 sheet);批处理模式下 Excel 文件会在每个场景跑完后保存(单场景导出则不保存),因此批跑期间不要打开目标工作簿。
Q5:怎么为自动化结果建立并持续对账的手工基准线?
A:在你自己的机器和版本下手工跑一个基准场景,记录关键结果值并标注示例性建模;此后一切脚本的验收方式都是复现基准工况、误差在导出精度内一致,本篇的 read_export.py 长表就是把结果对账的数据底座。