COMSOL多物理场二次开发教程(08):结果取数的四条通道——export Data/Table、结果表与派生值
版本与事实声明
- 版本锚点:COMSOL Multiphysics® 6.3。导出与结果表的 API 形态取自官方 API 参考(Data 导出、Table 导出、Result Table 三处);表格属性名(
alwaysask、complexnotation)逐字取自官方 API 参考。- 派生值(Derived Values)节点的类型字符串在本文未逐字确证,正文标注"以官方文档为准",并给出用录制取得真名的动作(铁律 1)。
- 示例数值仅用于教学,不代表任何标准规定。
一句话结论:结果取数有且只有四条官方通道——① 数据导出model.result().export().create("data1","Data")后export("data1").run();② 表导出create("tbl1","Table")(其alwaysask属性在无 GUI 时被忽略);③ 结果表直接落盘model.result().table(tag).save(filename, fullPrecision);④ 图组model.result("pg1").run()与派生值节点(类型字符串以录制为准);无论走哪条,都必须用"量级断言 + 精度检查"把"取到数"和"取对数字"分开。
〇、本篇要解决的认知问题
- Q1:
export "Data"与export "Table"有什么区别?各自适合什么场景? - Q2:为什么结果表的
save(filename, fullPrecision)里那个布尔参数很关键? - Q3:
model.result("pg1").run()到底做了什么?"绘图"和"取数"是一回事吗? - Q4:派生值(最大值、积分、平均值)怎么自动化?为什么不能猜节点类型?
- Q5:导出件格式会随版本漂移,解析脚本应该怎么设计才不脆弱?
一、机制解析
1.1 价值锚点:取数是自动化链条里"最容易被忽略的一环"
建模脚本写对了、求解跑通了,任务只完成了一半。真正的交付物是数据。而在实际项目里,失败的往往不是求解,而是取数——原因有三:
- 格式漂移:导出的表结构随版本/设置变化,昨天能解析的文件今天报列数不匹配;
- 精度丢失:默认导出的有效位数不足以做差分/敏感性分析,事后无法补救;
- 语义混淆:拿到的表里列名是"表达式"而不是"物理量名",汇总时张冠李戴。
本篇的四个通道就是针对这三类风险的工程答案。
1.2 通道一:数据导出"Data"
官方 API 形态(原文):
model.result().export().create(ftag,"Data");// 建数据导出特征model.result().export().create(ftag,dtag,"Data");// 指定数据集 dtagmodel.result().export(etag).set(property,value);// 设属性model.result().export(etag).run();// 执行导出model.result().export(etag).getAllowedPropertyValues(property);// 枚举某属性的允许值"Data"导出适合结构化数值输出:把解在格点上的值写成文本/表格/其他格式。“Data” 这个名字容易让人困惑——它不是"某一份数据",而是一类导出特征的类型名(就像"HeatTransfer"是一类物理场)。
getAllowedPropertyValues(property)是这篇里最实用的一行:它让你在不看界面的情况下知道某个属性有哪些合法取值。为什么要用?因为导出属性里常有on|off、fromprefs|rectangular|polar这类枚举,猜错就报错;用它能一次问清。
1.3 通道二:表导出"Table"
model.result().export().create(ftag,"Table");官方 API 参考给出的两个属性值得记住:
| 属性 | 允许值(默认) | 说明 |
|---|---|---|
alwaysask | on | off(off) | 保存时是否总询问文件名。在没有 GUI 运行时该属性被忽略 |
complexnotation | fromprefs | rectangular | polar(fromprefs) | 复数记法:沿用 Table 窗口偏好,或强制直角/极坐标 |
alwaysask那一条是批处理场景的关键:批处理时没有 GUI,所以它会被忽略,不会弹窗中断——这意味着批量脚本不必为了它做特殊处理,但也意味着文件名必须由脚本显式指定,否则你要确认默认命名规则(以官方文档为准)。
1.4 通道三:结果表落盘——fullPrecision的意义
官方 API 参考给出的结果表方法族:
model.result().table(ftag).set(property,value);model.result().table(ftag).save(filename);model.result().table(ftag).save(filename,fullPrecision);// ← 注意这个布尔参数model.result().table(ftag).loadFile(tempFile,...);model.result().table(ftag).saveFile(tempFile,...);save(filename, fullPrecision)是四个通道里最需要经验的一处:
经验法则:只要下游要做差分、敏感性分析、优化记录或回归比对,一律用
fullPrecision = true。默认精度下的数字看起来"够用",但差分会把有效位数损失放大到不可接受,而且丢失的精度不可能事后恢复。
这也解释了为什么"重新导出一次"经常是修复数值异常的最快手段——很多"结果对不上"其实是导出精度问题,不是求解问题。
1.5 通道四:图组与派生值
图组(图组是"结果分支下的绘图节点"):
model.result().create("pg1","PlotGroup1D");model.result("pg1").feature().create("lngr1","LineGraph");model.result("pg1").run();// 出图必须显式 run"绘图"和"取数"不是一回事:图组产出的是图形(以及可选的绘图数据),而数值汇总要靠导出特征与派生值。图组 run 的真实用途是:① 生成交付用图;② 让绘图数据(如曲线上的采样点)可供导出。
派生值(最大值、体积分、平均值、探针)是"把场压缩成一个数"的工具。它们的节点类型字符串本文不写死——请用三步录制法获取(第 07 篇 1.3 节同法):
- 在界面Results > Derived Values下新建一个你要的派生值(如体最大值);
- 开 Record Method,改一次它的设置再改回;
- 方法里生成的
model.result().numerical().create(<tag>, <TYPE>);中<TYPE>就是权威类型名。
警告:本文不给出任何具体的派生值类型字符串(如"体积分""全局求值"等形态的猜测),因为底账 U1 明确将此类未验证字符串列为不可断言项。用录制取真名,是唯一合规且可靠的做法。
1.6 容错解析:把"格式漂移"变成"可检测的失败"
导出件解析脚本必须假设版式会变。三条设计原则(最佳实践):
- 按列名匹配,不按列序号。用
pandas.read_csv(..., sep=None, engine='python')让 pandas 嗅探分隔符,然后按列名取数; - 入口做 schema 校验:期望列不存在就显式抛错并打印实际列名(绝不静默填 NaN);
- 出口做量级断言:对关键量设物理量级区间(如温度 200~2000 K),超出即报警。这是唯一能拦住"列错位仍然解析成功"的手段。
1.7 取数五条纪律(可直接抄进团队规范)
把本篇的机制收敛成五条可以贴在工位上的纪律:
- 动作必须显式执行:导出特征要
run(),图组要run(),结果表要save()——没有一步是自动的; - 输出路径一律绝对路径,且脚本自己负责创建目录;相对路径在批处理环境里等于随机位置;
- 表达式清单显式写出(
set("expr", new String[]{...})),不依赖模型里遗留的界面状态; - 进入数值流程一律全精度(结果表的
fullPrecision参数); - 解析按列名、断言按量级,并且"解析成功但数值可疑"要用不同的退出码表达。
第 5 条最容易被忽略,但它决定了整条链路能否被自动化调度:调度层需要知道"是失败还是可疑"——失败要重跑,可疑要人工介入。两者混成一个退出码,就等于把人的判断成本转嫁到了每次运行上。
一条附带的经验法则:导出件的列数不宜过多。一次性导出几十个表达式会让文件变大、解析变慢,且其中多数列在汇总阶段用不上。推荐做法是每次导出只取本次分析需要的 3~8 个量,不同用途导出不同文件,在汇总层做连接(join)。
二、完整代码与逐行剖析
代码 2-1:导出特征与结果表落盘(Java)
// ===== 四条取数通道的自动化(在第 03/07 篇模型基础上运行) =====// ---- 通道一:数据导出 ----model.result().export().create("data1","Data");// [1] 建"数据"类导出特征model.result().export("data1").set("filename","D:\\work\\out\\field_data.txt");// [2]model.result().export("data1").set("expr",newString[]{"T","cA"});// [3] 导出表达式// 属性名不确定时,先问允许值(官方 API:getAllowedPropertyValues)// debugLog(model.result().export("data1").getAllowedPropertyValues("filename").toString());model.result().export("data1").run();// [4] 执行导出// ---- 通道二:表导出 ----model.result().export().create("tbl1","Table");model.result().export("tbl1").set("filename","D:\\work\\out\\table_export.csv");model.result().export("tbl1").set("alwaysask","off");// [5] 无 GUI 时该属性被忽略(官方明示)model.result().export("tbl1").run();// ---- 通道三:结果表落盘(注意 fullPrecision) ----// 假设已有一个结果表节点(tag 以实际为准,可用 model.result().table().tags() 枚举)// model.result().table("tbl1").save("D:\\work\\out\\table_full.csv", true); // [6] fullPrecision = true// ---- 通道四:图组出图 ----model.result().create("pg1","PlotGroup1D");// [7] 与第 03 篇同构model.result("pg1").feature().create("lngr1","LineGraph");model.result("pg1").feature("lngr1").set("xdata","expr");model.result("pg1").feature("lngr1").set("xdataexpr","x");model.result("pg1").feature("lngr1").set("data","parent");model.result("pg1").feature("lngr1").selection().geom("geom1",1);model.result("pg1").feature("lngr1").selection().set(1);model.result("pg1").run();// [8] 出图必须显式 run(且只需一次)// ---- 派生值:类型字符串用录制取得 ----// model.result().numerical().create("mx1", "<DERIVED_VALUE_TYPE>"); // [9] 占位符,非猜测逐行剖析
- [1]
create("data1","Data"):第一个参数是tag(你定的),第二个是类型(固定字符串)。两个参数的角色不要搞反——这是新手最常犯的"参数顺序"错误之一。 - [2] 输出路径用绝对路径。批处理时工作目录不确定,相对路径会把文件写到意想不到的地方。Windows 路径中的反斜杠在 Java 字符串里要写
\\(官方文档对此有明确提醒)。 - [3]
expr是导出哪些表达式的属性;传的是字符串数组。建议每篇脚本都显式写出表达式清单,而不是依赖界面里遗留的列表——否则换台机器导出内容就变了。 - [4]
run():导出动作必须显式执行。导出是动作,不是状态。 - [5]
alwaysask = "off":设置它更多是为了可读性(明确声明"不询问"),因为无 GUI 时官方会忽略该属性。真正决定文件名的是 [2]/[5] 里的filename。 - [6]
save(filename, true):第二个参数是 fullPrecision。凡是进入数值流程的结果,一律用true。 - [7][8] 图组的创建与 run。注意
selection().geom("geom1", 1)与selection().set(1)的用法与第 03 篇一致——这些是已验证的写法。 - [9]
<DERIVED_VALUE_TYPE>:刻意留白。用录制取真名后替换;不要从网上抄。
代码 2-2:Python 容错解析与量级断言(可直接运行)
# -*- coding: utf-8 -*-""" parse_export.py —— 解析 COMSOL 导出的表格,并做 schema 校验 + 量级断言 设计原则:按列名取数、缺失即报错、物理量级越界即报警 """importsysimportpandasaspd# 期望出现的列(按"列名"而非"列序号"匹配;名称需与导出的表达式一致)EXPECTED=["x","T","cA"]# 物理量级断言区间(示例性,仅用于拦"列错位/单位错"这类事故)SANITY={"T":(200.0,2000.0),# 温度,K"cA":(0.0,1.0e4),# 浓度,mol/m^3}defload_export(path:str)->pd.DataFrame:# sep=None + engine='python' 让 pandas 嗅探分隔符(导出件可能是逗号/制表符/空格)df=pd.read_csv(path,sep=None,engine="python",comment="%")# [1] COMSOL 文本导出常含 % 注释头df.columns=[c.strip().lstrip("%").strip()forcindf.columns]# [2] 清洗表头空白与注释符号returndfdefcheck_schema(df:pd.DataFrame)->None:missing=[cforcinEXPECTEDifcnotindf.columns]ifmissing:# [3] 显式失败,并把实际列名打出来 —— 绝不静默填 NaNraiseKeyError(f"缺少期望列{missing};实际列名为{list(df.columns)}")defcheck_sanity(df:pd.DataFrame)->list:bad=[]forcol,(lo,hi)inSANITY.items():ifcolnotindf.columns:continuev=pd.to_numeric(df[col],errors="coerce")# [4] 非数值一律变 NaN,不崩n_bad=int(((v<lo)|(v>hi)).sum())ifn_bad:bad.append((col,n_bad,lo,hi,float(v.min()),float(v.max())))returnbaddefmain(path:str)->int:df=load_export(path)check_schema(df)print(f"[OK] schema 通过:{df.shape[0]}行 x{df.shape[1]}列")bad=check_sanity(df)ifbad:forcol,n,lo,hi,vmin,vmaxinbad:print(f"[WARN]{col}:{n}个值越界(期望 [{lo},{hi}],实际 [{vmin},{vmax}])")return2print("[OK] 量级断言通过,可进入下游汇总。")return0if__name__=="__main__":sys.exit(main(sys.argv[1]iflen(sys.argv)>1else"field_data.txt"))逐行剖析
- [1]
sep=None, engine="python":让 pandas 嗅探分隔符。导出件的分隔符可能随版本/设置变化,硬编码sep=","是格式漂移事故的第一大来源。comment="%"处理 COMSOL 文本导出常见的%注释头。 - [2] 清洗表头:导出件表头常带前导
%、空格或单位括注。统一在此处规整,下游就不用各自处理。 - [3]显式抛错并打印实际列名:这是本篇最重要的设计。格式漂移应当表现为"立刻失败",而不是"少了几列但程序继续跑"。
- [4]
pd.to_numeric(..., errors="coerce"):把无法解析的单元格变成NaN而不是抛异常——让量级检查去发现它们(NaN 参与比较结果为 False,所以还要额外统计 NaN 数,生产版建议加 NaN 计数)。 - 返回码
2表示"解析成功但数值可疑":把"可疑"与"失败"分成不同退出码,调度层就能区别对待(第 20 篇的回归测试依赖这个约定)。
三、常见报错与排查
报错 3-1:export("data1").run()报找不到表达式。
现象:导出失败或导出成空表。根因:expr里的表达式名与模型里实际存在的变量名不一致(温度变量名、浓度变量名随物理场/版本变化)。解法:用model.result().export(etag).getAllowedPropertyValues(...)与录制法核对;在界面"派生值/导出"里先手工导一次,看导出的表头里到底叫什么名字。
报错 3-2:导出文件里的数字精度不够,差分结果跳变。
现象:下游差分/敏感性分析结果噪声大。根因:未使用全精度保存(通道三的fullPrecision参数)。解法:model.result().table(tag).save(filename, true);或改用"Data"导出并设置更高的有效位(具体属性以官方文档为准)。
报错 3-3:批量运行时导出被"另存为"对话框卡住。
现象:无人值守任务挂起。根因:alwaysask的行为在不同环境下的差异;或文件名未显式指定导致需要交互。解法:显式设置filename绝对路径,并把alwaysask设为off。注意官方明示:无 GUI 运行时alwaysask会被忽略,所以真正的保障是显式文件名。
报错 3-4:pandas 读导出件报列数不匹配 / 只有一列。
现象:ParserError或整个文件被读成一列。根因:分隔符猜错(制表符被当普通字符);或表头行前有非注释的多余行。解法:按代码 2-2 用嗅探 +comment;先用文本编辑器看前 5 行,确认注释符号与分隔符;必要时给skiprows/header参数(以实际文件为准)。
报错 3-5:派生值脚本报"未知类型"。
现象:numerical().create(tag, "<猜的类型>")失败。根因:类型字符串是你猜/抄的(底账 U1 明确此类字符串不可断言)。解法:三步录制法取真名。不要再试第二个猜测值——猜不中是常态。
四、动手练习
- 练习 1(四通道打卡):在第 03 篇模型上依次跑通四条通道。判定:生成 4 类产出(数据文件、表导出文件、结果表文件、图组),且至少两个文件能被代码 2-2 解析通过 schema 校验。
- 练习 2(精度对照实验):对同一组结果分别以默认精度与
fullPrecision=true保存,然后对两种文件做一阶差分。判定:全精度版本差分的"毛刺"显著更少(可用差分序列的标准差对比),并能说出精度损失为什么不可事后恢复。 - 练习 3(格式漂移演练):手工把导出文件的表头改成 COMSOL 常见形态(加
%前缀、加单位括注、改成制表符分隔),再跑代码 2-2。判定:程序要么正常解析(得益于嗅探与表头清洗),要么以明确的列名报错退出——不允许出现"解析成功但数值错乱"。 - 练习 4(取数纪律,思考题):说明为什么"派生值类型字符串不能猜"。验证要点(至少 3 点):① 类型字符串随物理模块与版本变化,模块未装时根本不存在;② 合法来源只有官方文档、录制生成代码、Java Shell 实跑通过(铁律 1);③ 猜错的代价是"报错误类型"或更糟——静默生成错误的量,污染下游全部汇总。
五、小结与下一篇预告
本篇把"取数"变成可复用的工程能力:四条官方通道("Data"导出、"Table"导出、结果表save(filename, fullPrecision)、图组与派生值),一条精度纪律(进入数值流程一律全精度),一套容错解析范式(嗅探分隔符 + 清洗表头 + 按列名校验 + 量级断言 + 分级退出码),以及一条合规纪律(派生值类型字符串用录制取真名,不猜不改)。
第 09 篇《LiveLink for MATLAB 深入》将换一条入口来做同样的事:用mphstart连上 COMSOL Server、用import com.comsol.model.*打开 API、用mphload(filename, mtag)载入模型,并在 MATLAB 循环里改参、求解、mphsave——同时讲透"MATLAB 循环里的模型不会自动保存"这个官方明示的坑,以及mphrun(6.1 新增)在批量推进研究步骤时的用法。
本篇认知问题回显(FAQ)
Q1:export "Data"与export "Table"有什么区别?
A:两者都是model.result().export()下的导出特征类型名:"Data"用于把解在格点上的结构化数值写出,适合字段级数据交换;"Table"用于把结果以表格形式导出,官方 API 参考给出其alwaysask(保存时是否询问文件名,无 GUI 时被忽略)与complexnotation(fromprefs/rectangular/polar)两个属性。两者都必须显式调用.run()才执行。
Q2:为什么结果表的save(filename, fullPrecision)里那个布尔参数很关键?
A:因为它决定是否以全精度写出数值。默认精度下的数字看似够用,但下游做差分、敏感性分析、优化记录或回归比对时,差分会把有效位数损失放大到不可接受,而且丢失的精度无法事后恢复。因此凡进入数值流程的结果表一律用fullPrecision = true。
Q3:model.result("pg1").run()到底做了什么?绘图和取数是一回事吗?
A:run()执行该图组的绘图计算并刷新图形,是为交付出图与获取绘图采样数据的动作;但它不等于"取数"。批量汇总应通过导出特征("Data"/"Table")与派生值节点获取数值,绘图只负责可视化。另外图组 run 只调用一次即可,录制代码里出现两次属于冗余。
Q4:派生值(最大值、积分、平均值)怎么自动化?为什么不能猜节点类型?
A:自动化方式是在model.result().numerical()下创建派生值节点并运行;但其类型字符串未逐字确证,必须用三步录制法取得本机真名后替换占位符。不能猜的原因是该字符串随物理模块与版本变化、模块未安装时不存在,且猜错可能静默生成错误的量并污染下游汇总。
Q5:导出件格式会随版本漂移,解析脚本应该怎么设计?
A:三条原则:① 按列名而非列序号取数,并用sep=None, engine='python'让 pandas 嗅探分隔符、用comment处理%注释头;② 入口做 schema 校验,期望列缺失时显式抛错并打印实际列名,绝不静默填 NaN;③ 出口做量级断言(如温度落在 200~2000 K),并对"解析成功但数值可疑"使用与"解析失败"不同的退出码,便于调度层区别处理。