本节摘要上一节生成了三组拉杆工况的 Abaqus 输入文件本节继续完成结果闭环。我们将认识 ODB 中“分析步—结果帧—场变量—节点或单元值”的层级用 Abaqus Python 批量提取位移、轴向应力、应变和支座反力保存为 CSV并与解析解自动比较。学习重点是准确定位结果、理解输出变量的物理意义以及区分“作业结束”和“结果可信”。一、为什么还需要专门学习结果提取假设三个 Abaqus 作业都显示运行结束我们是否可以直接宣布模型正确还不能。一个作业可能顺利结束但仍存在单位误用、载荷方向写反、取错节点、取错分析帧等问题。因此本节要建立一条完整链路上一节的 .inp 文件 ↓ Abaqus 求解得到 .odb ↓ 按工况、节点和单元提取结果 ↓ 与 expected_results.csv 中的理论值比较 ↓ 通过检查后才进入后续数据分析Abaqus 的 ODB 将结果组织为分析步、结果帧和场输出U、RF等变量又包含位于具体节点上的数值。这种层级结构决定了我们不能只写“读取应力”还必须说清楚哪个工况、哪个分析步、哪一帧、哪个单元的哪种应力。Abaqus ODB对象结构说明 · 场输出读取说明二、回顾本节要读取的三个工况沿用第四十五节的两单元均匀拉杆\[ L1000\ \mathrm{mm},\qquad A100\ \mathrm{mm^2},\qquad E210000\ \mathrm{MPa}. \]节点 1 固定节点 3 分别承受 \(5000\)、\(10000\)、\(15000\ \mathrm N\) 的轴向拉力。节点1 ───── 单元1 ───── 节点2 ───── 单元2 ───── 节点3 → F x0 x500 mm x1000 mm第四十五节已经生成bar_F05000.inp bar_F10000.inp bar_F15000.inp expected_results.csv本节需要在 Abaqus 求解后获得同名的三个.odb文件。理论位移、应力和反力分别满足\[ u_2\frac{F(L/2)}{EA},\qquad u_3\frac{FL}{EA},\qquad \sigma\frac{F}{A},\qquad R_{1x}-F. \]例如 \(F10000\ \mathrm N\) 时理论上应有\[ u_20.238095\ \mathrm{mm},\quad u_30.476190\ \mathrm{mm},\quad \sigma100\ \mathrm{MPa},\quad R_{1x}-10000\ \mathrm N. \]三、先分清要提取的变量ODB变量本节读取的位置物理意义U1节点 2、3全局 \(x\) 方向位移RF1节点 1固定端全局 \(x\) 方向反力S11单元 1、2桁架局部轴向应力E11单元 1、2桁架轴向应变本例的杆沿全局 \(x\) 轴放置因此轴向应力与我们熟悉的 \(\sigmaF/A\) 可以直接比较。如果将来把桁架旋转一个角度S11仍表示沿杆轴的局部应力不能不加判断地把它叫作全局 \(x\) 向应力。另一个重要区别是位移和反力属于节点结果应力和应变属于单元结果。Abaqus 的场值对象分别提供nodeLabel、elementLabel等定位信息只写values[0]而不核对编号在复杂模型中很容易取错值。Abaqus FieldValue 属性说明四、先运行三个 Abaqus 作业进入第四十五节生成的工况目录在配置好 Abaqus 的命令行环境后依次执行abaqus jobbar_F05000 inputbar_F05000.inp interactive abaqus jobbar_F10000 inputbar_F10000.inp interactive abaqus jobbar_F15000 inputbar_F15000.inp interactive正常情况下目录中会出现对应的.odb。但请先查看.sta、.msg、.log中的完成状态和错误信息再进行后处理ODB 文件存在不等于分析已经成功完成。Abaqus 官方也将状态文件、消息文件与 ODB 区分为不同用途的输出文件。Abaqus输出文件说明下面的后处理代码面向使用 Python 3 的 Abaqus 2024 及更新版本。Abaqus 2024 起其内置 Python 转向 Python 3.10更早版本需要按对应版本修改脚本。版本说明五、完整代码批量提取并自动验收把下面程序保存为lesson46_extract_odb.py。将它复制到存放三个 ODB 和expected_results.csv的目录在该目录运行abaqus python lesson46_extract_odb.py .这里必须使用Abaqus 自带的 Python。普通 Python 环境通常无法导入odbAccess。Abaqus 官方提供了abaqus python 脚本名.py的运行方式。Abaqus Python运行说明Read Lesson 45 ODB files and compare them with analytical bar results. import csv import os import sys def component(frame, field_name, component_name, label_kind, label): field frame.fieldOutputs[field_name] labels tuple(field.componentLabels) if component_name not in labels: raise ValueError( {} has no component {}.format(field_name, component_name) ) values [ value for value in field.values if getattr(value, label_kind, None) label ] if len(values) ! 1: raise ValueError( {} at {} {}: expected one value, found {}.format( field_name, label_kind, label, len(values) ) ) try: data values[0].data except Exception: data values[0].dataDouble index labels.index(component_name) try: return float(data[index]) except TypeError: if index: raise return float(data) def read_odb(path): from odbAccess import openOdb odb openOdb(pathpath, readOnlyTrue) try: step odb.steps[PULL] if not step.frames: raise ValueError(PULL contains no result frames) frame step.frames[-1] if abs(frame.frameValue - step.timePeriod) 1e-5: raise ValueError( PULL has not reached its final step time ) return { u2_mm: component( frame, U, U1, nodeLabel, 2 ), u3_mm: component( frame, U, U1, nodeLabel, 3 ), s1_MPa: component( frame, S, S11, elementLabel, 1 ), s2_MPa: component( frame, S, S11, elementLabel, 2 ), e1: component( frame, E, E11, elementLabel, 1 ), e2: component( frame, E, E11, elementLabel, 2 ), rf1_N: component( frame, RF, RF1, nodeLabel, 1 ), } finally: odb.close() def self_test(): from types import SimpleNamespace value SimpleNamespace( nodeLabel2, data(0.238095, 0.0) ) field SimpleNamespace( componentLabels(U1, U2), values(value,) ) frame SimpleNamespace( fieldOutputs{U: field} ) assert component( frame, U, U1, nodeLabel, 2 ) 0.238095 print(Self-test OK) def main(folder): expected_path os.path.join( folder, expected_results.csv ) with open(expected_path, newline) as handle: expected_rows list(csv.DictReader(handle)) if not expected_rows: raise ValueError( expected_results.csv has no jobs ) rows [] for expected in expected_rows: job expected[job] if os.path.basename(job) ! job: raise ValueError( Invalid job name: {}.format(job) ) odb_path os.path.join( folder, job .odb ) if not os.path.isfile(odb_path): raise FileNotFoundError( Missing ODB: {}.format(odb_path) ) actual read_odb(odb_path) load float(expected[load_N]) passed ( abs( actual[u2_mm] - float(expected[u2_mm]) ) 1e-4 and abs( actual[u3_mm] - float(expected[u3_mm]) ) 1e-4 and abs( actual[s1_MPa] - float(expected[sigma_MPa]) ) 0.02 and abs( actual[s2_MPa] - float(expected[sigma_MPa]) ) 0.02 and abs( actual[rf1_N] - float(expected[reaction_node1_N]) ) 0.2 and abs( actual[rf1_N] load ) 0.2 ) rows.append([ job, load, actual[u2_mm], actual[u3_mm], actual[s1_MPa], actual[s2_MPa], actual[e1], actual[e2], actual[rf1_N], PASS if passed else FAIL, ]) output_path os.path.join( folder, actual_results.csv ) with open( output_path, w, newline ) as handle: writer csv.writer(handle) writer.writerow([ job, load_N, u2_mm, u3_mm, s1_MPa, s2_MPa, e1, e2, rf1_N, check, ]) writer.writerows(rows) for row in rows: print( {}: u3{:.6f} mm, S11({:.3f}, {:.3f}) MPa, RF1{:.3f} N, {}.format( row[0], row[3], row[4], row[5], row[8], row[9] ) ) print(Saved {}.format(output_path)) if any(row[-1] FAIL for row in rows): raise SystemExit( At least one case failed the checks ) if __name__ __main__: if --self-test in sys.argv: self_test() else: folder os.path.abspath( sys.argv[1] if len(sys.argv) 1 else . ) main(folder)这里没有使用 Pandas本节只需读取和写入 CSVPython 标准库中的csv已经足够。六、代码是如何找到“正确结果”的最关键的一句不是openOdb()而是这一串定位step odb.steps[PULL] frame step.frames[-1] field frame.fieldOutputs[U]它依次指定第四十五节输入文件中名为PULL的分析步该分析步的最后一个结果帧最后一帧中的位移场U。随后程序再按nodeLabel3找到节点 3按componentNameU1取得 \(x\) 向位移。Abaqus 官方示例同样通过steps[...]、frames[-1]和fieldOutputs[...]访问结果。分析步与结果帧读取说明程序还检查了最后一帧是否到达分析步的终止时间。这个检查可以挡住一部分“计算中途停止却拿已有帧当最终结果”的情况但它不能代替查看作业状态文件和错误日志。七、预期结果与程序输出对于三组工况理论目标值如下工况\(u_2\) / mm\(u_3\) / mm两单元 \(S_{11}\) / MPa两单元 \(E_{11}\)\(RF_1\) / Nbar_F050000.1190480.238095500.000238095−5000bar_F100000.2380950.4761901000.000476190−10000bar_F150000.3571430.7142861500.000714286−15000如果 Abaqus 实算结果满足设定的容差终端输出预计接近bar_F05000: u30.238095 mm, S11(50.000, 50.000) MPa, RF1-5000.000 N, PASS bar_F10000: u30.476190 mm, S11(100.000, 100.000) MPa, RF1-10000.000 N, PASS bar_F15000: u30.714286 mm, S11(150.000, 150.000) MPa, RF1-15000.000 N, PASS Saved ...\actual_results.csv注意上面是依据解析解写出的预期输出不是本机的 Abaqus 实测记录。当前环境没有安装 Abaqus因此我只运行了脚本内置的--self-test其实际输出为Self-test OK尚未对真实 ODB 完成求解器级验证。八、怎样理解程序中的“通过”本节设置了示例验收容差节点位移与理论值相差小于 \(10^{-4}\ \mathrm{mm}\)两个单元的轴向应力与理论值相差小于 \(0.02\ \mathrm{MPa}\)支座反力与理论值相差小于 \(0.2\ \mathrm N\)外力与支座反力之和接近零。容差不是“正确性定律”。本例是简单线弹性杆理论解明确所以可以设得较严格。将来分析接触、塑性、裂纹或复杂网格时应根据数值离散误差、输出精度和工程精度要求重新制定验收标准。还有两个额外的物理检查值得手动做一遍\[ u_3\approx 2u_2, \qquad S_{11}^{(1)}\approx S_{11}^{(2)}. \]前者来自两段等长、同材料、同截面杆的均匀伸长后者来自两个单元传递相同轴力。若它们明显不成立即使某一项恰好显示PASS也应返回模型检查材料、截面、节点连接与载荷设置。九、常见错误让报错成为诊断线索现象优先检查No module named odbAccess是否误用了普通 Python改用abaqus pythonMissing ODB作业是否真的完成文件名是否与清单一致找不到PULL输入文件中的分析步名称是否被修改找不到S11或E11是否请求了相应输出使用的是否仍为本节桁架模型某节点找到多个值是否混入了多个部件实例不能只靠节点编号定位应力大小正确、反力符号错误检查载荷方向和反力所在节点位移相差约 \(1000\) 倍优先检查 mm、m、MPa、Pa 的单位混用本节脚本针对第四十五节的单实例、两单元拉杆。复杂装配中不同部件实例可能各自拥有同样的节点编号那时应进一步按instance和节点集筛选而不能把这里的简单编号提取方式原样推广。十、练习与本节小结基础练习在 Abaqus/Viewer 中手动读取bar_F10000.odb的节点 3 位移再与actual_results.csv的u3_mm比较。两种读取方式应指向同一分析步、同一帧和同一节点。进阶练习把第二个工况的载荷改为 \(12000\ \mathrm N\)重新生成.inp和理论清单、重新提交 Abaqus 作业再运行本节脚本。只改 CSV、却不重新求解 ODB应被检查流程识别为不一致。本节完成的闭环是\[ \text{参数化输入} \longrightarrow \text{Abaqus求解} \longrightarrow \text{ODB提取} \longrightarrow \text{解析解对照} \longrightarrow \text{可信数据集}. \]下一节可以在这条管道上继续扩展工况参数构建真正可用于机器学习的 Abaqus 仿真数据表。