HTRI二次开发教程(09):结果取数——从对象模型与报表导出到 DataFrame
版本与事实声明
- 版本锚点:当前Xchanger Suite 9.4。9.4 在 Final Results Report 中新增"temperature effectiveness"(冷热两侧温度有效度)等输出项,说明报表内容随版本演进——解析脚本必须容错。
- 官方 Outputs 含 spreadsheet-style output reports(可导出 Excel)、Summary reports、Detailed reports(局部剖面)、标准 TEMA specification sheet、2D/3D drawings、user-defined graphs。
- 示例代码中所有标识符为占位符;数值均为示例性建模,不代表任何标准规定。
一句话结论:结果取数有两条路——对象模型逐字段读取(精确、可控、依赖探测所得标识符)与报表导出件解析(稳、可审计、依赖 Excel 文件结构),工程上的最优解是两者并用:用对象模型取"少量关键指标"入账本,用报表导出件做"完整结果归档与交叉校验",最后统一规范化成"案例编号 × 指标 × 单位"的长表。
〇、本篇要解决的认知问题
- Q1:对象模型取数与报表导出件解析,各自的长处与短处是什么?
- Q2:summary 报表与 detailed 报表在结构上有什么本质差异,为什么 detailed 不能简单"一键转表"?
- Q3:Xist 支持多单位集,取数时单位不统一会带来什么后果,怎么防?
- Q4:导出件解析时,列契约(column contract)为什么比"列名"更重要?
- Q5:怎么把两条取数路的结果统一成一张可分析的长表?
一、机制解析
1.1 两条取数路
| 维度 | 对象模型取数 | 报表导出件解析 |
|---|---|---|
| 数据来源 | Automation Server 对象树的输出字段 | 导出的 spreadsheet-style report(Excel) |
| 精度控制 | 高(可精确定位单字段) | 中(受导出格式/分页影响) |
| 依赖 | 探测所得真实标识符(U2) | Excel 文件结构(随版本漂移) |
| 批量速度 | 快(内存直读) | 慢(需落盘再解析) |
| 审计留痕 | 弱(只留你记录的值) | 强(导出件即原始凭证) |
| 推荐组合 | 取关键指标入账本 | 完整归档 + 交叉校验 |
为什么两条都要:对象模型取数适合"批量循环里取 3~5 个关键量";但它不提供"官方版式的原始记录"。报表导出件是可交给第三方复核的凭证。生产系统里"账本关键指标 + 报表归档"是标配。
1.2 summary 与 detailed 的结构差异
- Summary reports:一两页的总览结果——整体传热系数、总压降、裕量、热负荷等"标量"。结构接近"键-值"表,适合逐行解析成宽表。
- Detailed reports:温度、压力、传热系数、热流等局部剖面——沿换热器长度/区段的一系列值。结构是"多组剖面曲线",每组的自变量(位置)与因变量(多个参数)不同。
反直觉点:detailed 报表不能简单"读成一张二维表"——它是"一组表的集合"。硬塞进一个 DataFrame 会得到一张稀疏、错位的表。正确做法是先按区段/剖面分组,再各自成表(long format:section, position, parameter, value)。
1.3 多单位集:取数的隐形地雷
Xist 官方明确支持"input and output can be displayed inmultiple unit sets, including user-defined sets。Dynamic display of values in all unit sets." 这带来一个陷阱:同一个量在不同单位集下数值不同,而导出件用哪套单位取决于案例设置。
铁律级实践:落盘时每个数值必须带单位,或全系列统一到一套 SI 单位。禁止"裸数字入库"。做法:
- 在列契约里显式声明每个指标的
unit; - 取数时若发现单位与契约不符,走显式换算表(如 kPa↔Pa、mm↔m),换算因子写死在代码里并注释来源;
- 绝不用"数字看起来像"来猜单位。
1.4 列契约与缺失值
列契约规定每列的:规范名、单位、数据类型、允许缺失、来源(对象模型/报表)。它比"列名"更重要的原因是:列名会变(版本漂移、语言切换),契约不变。解析脚本按契约校验,列名只是"契约到具体文件的映射"。
缺失值处置三态:缺失(真的没有)、不适用(该模式/该模块无此项)、解析失败(格式变了)。三者语义不同,不能一律填 0 或 NaN——否则后期无法区分"设备没这项"和"程序没读到"。
1.5 取数的三条精度纪律
纪律一:取数必须发生在运行完成之后。第 07 篇已强调:对象模型读的是"当前案例的输出字段",若在 run 完成前读,你拿到的是上一轮或空值。取数脚本的第一行逻辑应是"确认本次运行已完成",而不是直接开始读。
纪律二:同一指标,跨来源必须交叉校验。对象模型与报表导出件是两条独立的路,它们的差值就是你的"取数可信度"。若两条路结果一致,说明取数链路可靠;若不一致,要么单位没对齐,要么其中一条路读错了字段。永远不要只走一条路就宣布取数成功——交叉校验是免费的验证。
纪律三:精度不"四舍五入"到看不清差异。报表导出件常自带位数截断;对象模型读到的是完整精度。做交叉校验时用对象模型的全精度值做基准,报表只作旁证;若把两边都先截断到 2 位再比,会掩盖真实的单位错误(1000 倍偏差截断后仍是 1000 倍,但小偏差会被吃掉)。
一条反直觉提醒:“取到的数"不等于"可用的数”。取数是技术动作,可用性是工程判断——一个传热系数取出来了,但它是不是对应"设计工况"、是不是在"清洁状态"、单位是不是"设计单位集",都需要在取数时同步记录上下文(第 12 篇的振动几何上下文就是同一思想)。脱离上下文的数,等于没取。
二、完整代码与逐行剖析
代码 9-1:报表导出件解析为规范化长表
# -*- coding: utf-8 -*-""" parse_report.py —— 把 Xist 导出的报表解析为规范化长表 用法:python parse_report.py "export.xlsx" "case_0001" 输出:results_long.csv(case_id, section, parameter, value, unit, source) 说明:导出件版式随版本漂移,本脚本按"内容定位 + 列契约"的思路解析, 所有数值均示例性建模,不对应任何真实装置。 """importsysimportreimportpandasaspd# 列契约:规范名 -> (单位, 类型, 允许缺失)CONTRACT={"整体传热系数":("W/(m2·K)","float",False),"壳侧压降":("kPa","float",False),"管侧压降":("kPa","float",False),"热负荷":("kW","float",False),}# 显式换算表:把导出件可能出现的单位统一到契约单位CONV={("壳侧压降","Pa"):0.001,# Pa -> kPa("壳侧压降","kPa"):1.0,("管侧压降","Pa"):0.001,("管侧压降","kPa"):1.0,("整体传热系数","W/m2-K"):1.0,# 归一化写法 -> 契约写法}defnorm_unit(u):return(str(u).strip().lower().replace("·","-").replace("^","").replace(" ",""))deflocate_header(raw,keys,max_scan=40):foriinrange(min(max_scan,len(raw))):txt=" | ".join(str(x)forxinraw.iloc[i].tolist())ifall(kintxtforkinkeys):returnireturnNonedefparse_summary(path,case_id):raw=pd.read_excel(path,header=None,engine="openpyxl")hdr=locate_header(raw,["参数","数值"])orlocate_header(raw,["Parameter","Value"])rows=[]ifhdrisNone:returnrows body=raw.iloc[hdr+1:]for_,rinbody.iterrows():name=str(r.iloc[0]).strip()ifnameinCONTRACT:unit,_,_=CONTRACT[name]try:val=float(r.iloc[1])except(ValueError,TypeError):continue# 解析失败:跳过,不填 0# 若导出件带了单位列(第 3 列),按其换算found_unit=unitiflen(r)>2:raw_unit=str(r.iloc[2])f=CONV.get((name,norm_unit(raw_unit)))iffisnotNone:val*=f rows.append({"case_id":case_id,"section":"summary","parameter":name,"value":val,"unit":unit,"source":"report_export",})returnrowsdefmain():iflen(sys.argv)<3:print('用法:python parse_report.py "export.xlsx" "case_0001"')returnpath,case_id=sys.argv[1],sys.argv[2]rows=parse_summary(path,case_id)df=pd.DataFrame(rows,columns=["case_id","section","parameter","value","unit","source"])out=f"{case_id}_results_long.csv"df.to_csv(out,index=False,encoding="utf-8-sig")print(f"解析完成:{len(df)}条记录 ->{out}")ifnotdf.empty:print(df.to_string(index=False))if__name__=="__main__":main()逐行剖析:
CONTRACT把"规范名 → 单位/类型/是否允许缺失"固化:解析脚本据此校验,而不是盲目信任导出件列名。CONV是显式换算表,键是(规范名, 归一化单位):把"Pa→kPa"这类换算写成代码常量,杜绝"凭感觉估单位"。norm_unit归一化单位写法(W/m2·K与W/(m2·K)视为同一):导出件单位栏写法可能不统一,先归一化再查表。- 解析失败(
ValueError/TypeError)时**continue跳过**而不是填 0:这是 1.4 节"三态缺失"中"解析失败"态的处理——宁可缺、不可假。 - 输出固定四列
case_id/section/parameter/value/unit/source的长表:source标注来源,便于与"对象模型取数"合并时区分。 section="summary":本篇先做 summary;detailed 剖面(1.2 节)在下面代码 9-2 单独处理。
代码 9-2:对象模型取数 + 与报表结果合并
# -*- coding: utf-8 -*-""" extract_results.py —— 对象模型取关键指标,并与报表解析结果合并 用法:python extract_results.py 【重要】所有 <...> 标识符须替换为探测所得真实值。 """importcsvimportpandasaspdfromdrive_caseimportsession# 复用第 07 篇会话PROGID="<HTRIAutomationServer.ProgID(本机枚举所得)>"OP_LOAD="<打开案例的方法(探测所得)>"# 规范路径 -> (本机真实标识符, 单位) —— 只列已探测确认的OUT_FIELDS={"outputs.summary.overall_u":("<真实标识符>","W/(m2·K)"),"outputs.summary.shell_dP":("<真实标识符>","kPa"),}defread_object_model(case_path,case_id):rows=[]withsession(PROGID,300)as(app,_start):case=getattr(app,OP_LOAD)(case_path)fornorm,(ident,unit)inOUT_FIELDS.items():if"<"inident:continue# 未探测确认:跳过,不猜node=caseforpartinident.split("."):node=getattr(node,part)val=getattr(node,"<叶子属性名(探测所得)>")rows.append({"case_id":case_id,"section":"summary","parameter":norm.split(".")[-1],"value":float(val),"unit":unit,"source":"object_model",})returnrowsdefmain():om=pd.DataFrame(read_object_model("<case.htri>","case_0001"),columns=["case_id","section","parameter","value","unit","source"])try:rp=pd.read_csv("case_0001_results_long.csv",encoding="utf-8-sig")exceptFileNotFoundError:rp=pd.DataFrame(columns=om.columns)merged=pd.concat([om,rp],ignore_index=True)# 交叉校验:同一 case+parameter,两来源应一致pivot=merged.pivot_table(index=["case_id","parameter"],columns="source",values="value",aggfunc="last")merged.to_csv("results_merged_long.csv",index=False,encoding="utf-8-sig")print(f"合并后{len(merged)}条记录 -> results_merged_long.csv")if"report_export"inpivotand"object_model"inpivot:diff=(pivot["report_export"]-pivot["object_model"]).abs()pk=pivot[pivot["report_export"].notna()&pivot["object_model"].notna()]ifnotpk.empty:rel=(pk["report_export"]-pk["object_model"]).abs()/pk["object_model"].abs()print("两来源相对偏差(应趋近 0):")print(rel.round(6).to_string())if__name__=="__main__":main()逐行剖析:
OUT_FIELDS里再次执行"未探测确认就跳过"(if "<" in ident):把铁律 1 的护栏放到取数入口——取数同样不能用猜的标识符。source列区分object_model与report_export:合并后仍可追溯每条记录来自哪条路。pivot_table(..., aggfunc="last")把两来源并排:这是交叉校验的最小实现——报表导出件与对象模型对同一指标应给出一致数值。- 相对偏差打印:精度/一致性检查,也是发现"单位没对齐"的有效手段(若偏差恰为 1000 倍,恭喜你抓到一个单位 bug)。
- 合并结果落
results_merged_long.csv:长表结构(case_id/section/parameter/value/unit/source)是后续第 10 篇落盘与第 17/19 篇分析的统一输入。
三、常见报错与排查
报错 3-1:两来源数值差整数倍(如 1000×)。
现象:交叉校验偏差恰好 1000 或 1000000 倍。根因:单位没对齐(Pa vs kPa,W/m²K 与其它)。解法:检查CONV换算表是否覆盖该指标与单位;核对导出件的单位栏;禁止用"看起来像"来推断。
报错 3-2:detailed 报表解析出来稀疏错位。
现象:一个 DataFrame 里大量 NaN、列对不上。根因:detailed 是"多组剖面的集合",被强行读成一张二维表。解法:按section, position, parameter, value长表格式分别解析每个剖面;不要把不同剖面的横坐标混在一列。
报错 3-3:导出件列名是英文/中文混杂,解析匹配不上。
现象:summary 解析结果为空。根因:Xist 界面语言或导出模板变了,列名/指标名本地化。解法:locate_header支持多语言关键字(参数/数值与Parameter/Value);把"契约名 → 各语言列名"做成映射,契约是稳定层。
报错 3-4:某一指标偶尔解析失败被静默跳过。
现象:某案例缺某指标,但没报错。根因:解析失败态被continue跳过(设计如此)。解法:统计每批次的"解析失败率";若某指标缺失率异常高,说明格式变了,需要更新解析器。静默跳过要有监控,否则会漏掉系统性错误。
报错 3-5:对象模型取值拿到的是上一次的结果。
现象:改了输入,对象模型读数不变。根因:在 run 完成前取数,或复用了旧会话。解法:取数严格排在运行完成后;每案例独立会话(第 07 篇纪律)。
报错 3-6:只走报表导出件一条路,漏掉单位错误。
现象:交付后才发现某指标差整数倍。根因:没有交叉校验(1.5 节纪律二)。解法:对关键指标同时用对象模型与报表两条路取数并比对;相对偏差非 0 且为整数倍时,优先查单位换算表。
四、动手练习
- 练习 1(summary 解析):对第 03 篇的导出件运行代码 9-1。判定:生成
*_results_long.csv,含case_id/section/parameter/value/unit/source六列;至少解析出 2 个契约内指标。 - 练习 2(单位换算):构造一个"壳侧压降以 Pa 给出"的导出件,运行解析。判定:换算后数值与 kPa 版一致(差值为 0),且
unit列仍是kPa。 - 练习 3(交叉校验):对同一案例同时用对象模型与报表两条路取"整体传热系数"。判定:两来源相对偏差 < 1%(若偏差为整数倍,说明单位未对齐,需修复)。
- 练习 4(detailed 长表):取一份 detailed 剖面(如沿程温度),按
section, position, parameter, value形成长表。判定:长表行数 = 剖面点数 × 参数数;无稀疏错位;position 单调。
五、小结与下一篇预告
本篇把"取数"从"读一个字段"升级为"一套取数工程":两条路(对象模型 / 报表导出件)各有长短、应当并用;summary 是键值表、detailed 是多组剖面不能一键转表;多单位集是隐形地雷,每个数值必须带单位或统一换算;列契约比列名稳定。最终产出统一长表results_merged_long.csv,并以交叉校验验证一致性。
第 10 篇《结构化落盘》:长表已经有了,但"怎么把它稳稳地写进磁盘/数据库、怎么管理案例矩阵的唯一键、怎么设计断点账本、怎么保证可复现"是另一门功夫。我们会把 CSV/Parquet/JSON 元数据的取舍讲透,并给出幂等的账本读写组件。
本篇认知问题回显(FAQ)
Q1:对象模型取数与报表导出件解析各自的长处与短处?
A:对象模型取数精确可控、批量速度快,适合在循环里取少量关键指标,但依赖探测所得的真实标识符且审计留痕弱;报表导出件解析凭官方版式的 Excel 文件、便于第三方复核与归档,但批量速度慢、受版式随版本漂移影响。生产系统最优解是两者并用:对象模型取关键指标入账本,报表导出件做完整归档与交叉校验。
Q2:summary 与 detailed 报表结构上有什么本质差异?
A:Summary reports 是一两页的总览,结构接近"键-值"表(整体传热系数、总压降、裕量、热负荷等标量),适合逐行解析为宽表;Detailed reports 是温度、压力、传热系数、热流等的局部剖面,本质是"多组剖面曲线的集合",不能简单读成一张二维表,正确做法是按section, position, parameter, value长表格式分别解析。
Q3:多单位集取数会带来什么后果,怎么防?
A:Xist 支持 input/output 在多单位集(含用户自定义集)下动态显示,同一量在不同单位集数值不同,导出件用哪套单位取决于案例设置,容易造成"差 1000 倍"的静默错误。防法是铁律级实践:每个数值落盘必须带单位或全系列统一到 SI;用显式换算表(如 Pa→kPa 系数 0.001)并在代码中注释来源,绝不用"数字看起来像"猜单位。
Q4:为什么列契约比列名重要?
A:列名会随版本漂移与界面语言变化,而契约不变。列契约规定每列的规范名、单位、数据类型、是否允许缺失与来源(对象模型/报表导出)。解析脚本按契约校验,列名只是"契约到具体文件的映射";当导出件改为英文列名时,只改映射,不动业务逻辑。
Q5:怎么把两条取数路的结果统一成一张长表?
A:统一用case_id, section, parameter, value, unit, source六列长表,source区分 object_model 与 report_export。合并后用pivot_table(index=["case_id","parameter"], columns="source", values="value", aggfunc="last")把两来源并排,计算相对偏差做交叉校验(应趋近 0;若为整数倍则单位未对齐),结果落results_merged_long.csv作为后续落盘与分析的统一输入。