资讯详情

资讯详情

【零基础学智能仿真-48】Abaqus全场数据提取:节点位移与积分点应力不能混成一张表

本节摘要上一节建立了18个参数化工况并把通过验收的整体响应整理成数据集。本节进一步打开 ODB提取每个工况的节点位移、坐标、反力以及单元积分点的应力和应变。学习者将掌握全场数据的定位键理解“节点值”和“积分点值”为何必须分表保存并避免把可视化软件平滑后的节点应力误认为求解器的原始应力。一、从“一工况一行”走向“一工况多行”第四十七节的ml_dataset.csv每个工况只有一行适合预测末端位移、最大应力等整体指标。但如果将来想研究应力场预测、损伤定位或网格图神经网络仅有一行是不够的。以两单元拉杆为例一个工况至少包含两类空间数据节点1 ───── 单元1 ───── 节点2 ───── 单元2 ───── 节点3 U、RF S、E U、RF S、E U、RF这里的U、RF定位在节点S、E默认按单元积分点输出。Abaqus 官方文档明确区分节点、积分点、单元节点及形心等结果位置*Element Output的默认位置为积分点。结果位置说明 · 单元输出关键词说明因此本节不会把三条节点记录和两条单元记录按“第1行对第1行”强行拼接而是分别输出两张表。二、先设计清楚两张表的“身份证”第一张是节点表field_nodes.csv每一行由以下信息定位\[ (\text{job},\ \text{step},\ \text{frame},\ \text{instance},\ \text{node\_label}). \]它保存节点坐标、位移和反力。第二张是积分点表field_integration_points.csv每一行由以下信息定位\[ (\text{job},\ \text{step},\ \text{frame},\ \text{instance},\ \text{element\_label},\ \text{integration\_point}). \]它保存 \(S_{11}\) 和 \(E_{11}\)。其中instance不可随意省略复杂装配中不同部件实例可以各有一个“节点1”。Abaqus 的FieldValue提供实例、节点号、单元号和积分点号等属性正是为了准确标识结果位置。FieldValue官方说明本节只读取每个工况的PULL分析步最终帧。未来处理瞬态问题时帧号或时间也必须成为数据键的一部分否则不同时刻的结果可能被误当作同一条样本。三、用一个工况先预测表格长什么样取第四十七节中的AL_A080_F05000\[ E70000\ \mathrm{MPa},\quad A80\ \mathrm{mm^2},\quad F5000\ \mathrm N. \]按解析公式三个节点的 \(x\) 向位移应接近节点\(x\) / mm理论 \(U_1\) / mm10025000.446429310000.892857两单元的理论轴向应力和应变则应接近\[ S_{11}\frac{F}{A}62.5\ \mathrm{MPa}, \]\[ E_{11}\frac{S_{11}}{E} \frac{62.5}{70000} \approx0.000892857. \]因此两个积分点的结果预计相同。节点1的 \(x\) 向反力预计为 \(-5000\ \mathrm N\)。这些仍然只是验收用理论值不是尚未运行的 ODB 实测数据。四、完整代码导出两种位置的场数据本程序接在第四十六、四十七节之后使用。运行前工况目录里应已有18个经过 Abaqus 求解的.odb第四十六节生成的actual_results.csv且每个工况均为PASS。把下面代码保存为lesson48_export_fields.pyExport nodal and integration-point fields from Lesson 47 ODBs. import csv import os import sys def component(field, value, name): labels tuple(field.componentLabels) if name not in labels: raise ValueError( {} missing from {}.format(name, field.name) ) try: data value.data except Exception: data value.dataDouble index labels.index(name) try: return float(data[index]) except TypeError: if index: raise return float(data) def field_rows(job, odb_path): from odbAccess import openOdb from abaqusConstants import INTEGRATION_POINT odb openOdb( pathodb_path, readOnlyTrue ) try: step odb.steps[PULL] if not step.frames: raise ValueError( {}: PULL has no frames.format(job) ) frame step.frames[-1] if abs( frame.frameValue - step.timePeriod ) 1e-5: raise ValueError( {}: final step time not reached.format(job) ) fields frame.fieldOutputs u, rf, stress, strain ( fields[name] for name in (U, RF, S, E) ) instances { value.instance.name: value.instance for value in u.values } coordinates { (name, node.label): node.coordinates for name, instance in instances.items() for node in instance.nodes } reactions { (value.instance.name, value.nodeLabel): value for value in rf.values } nodes [] for value in u.values: key ( value.instance.name, value.nodeLabel ) x, y coordinates[key][:2] reaction reactions.get(key) nodes.append(( job, PULL, frame.frameValue, key[0], key[1], x, y, component(u, value, U1), component(u, value, U2), ( component(rf, reaction, RF1) if reaction is not None else ), )) def ip_key(value): if value.position ! INTEGRATION_POINT: raise ValueError( {}: expected integration-point output.format(job) ) return ( value.instance.name, value.elementLabel, value.integrationPoint, ) stresses { ip_key(value): value for value in stress.values } strains { ip_key(value): value for value in strain.values } if ( len(stresses) ! len(stress.values) or len(strains) ! len(strain.values) ): raise ValueError( {}: duplicate integration-point key.format(job) ) if set(stresses) ! set(strains): raise ValueError( {}: stress/strain locations differ.format(job) ) points [ ( job, PULL, frame.frameValue, instance, element, ip, component( stress, stresses[(instance, element, ip)], S11, ), component( strain, strains[(instance, element, ip)], E11, ), ) for instance, element, ip in sorted(stresses) ] if ( sorted(row[4] for row in nodes) ! [1, 2, 3] or sorted(row[4] for row in points) ! [1, 2] ): raise ValueError( {}: expected nodes 1-3 and elements 1-2.format(job) ) return nodes, points finally: odb.close() def main(folder): summary_path os.path.join( folder, actual_results.csv ) with open( summary_path, newline ) as handle: validated list(csv.DictReader(handle)) if ( not validated or any( row[check] ! PASS for row in validated ) ): raise ValueError( All jobs must have PASS status in actual_results.csv ) jobs [ row[job] for row in validated ] if len(set(jobs)) ! len(jobs): raise ValueError( Duplicate job identifiers ) all_nodes [] all_points [] for summary in validated: job summary[job] odb_path os.path.join( folder, job .odb ) if not os.path.isfile(odb_path): raise FileNotFoundError( Missing ODB: {}.format(odb_path) ) nodes, points field_rows( job, odb_path ) node3 next( ( row for row in nodes if row[4] 3 ), None ) element1 next( ( row for row in points if row[4] 1 ), None ) if ( node3 is None or element1 is None or abs( node3[7] - float(summary[u3_mm]) ) 1e-5 or abs( element1[6] - float(summary[s1_MPa]) ) 0.01 ): raise ValueError( {}: ODB differs from the validated summary.format(job) ) all_nodes.extend(nodes) all_points.extend(points) node_path os.path.join( folder, field_nodes.csv ) point_path os.path.join( folder, field_integration_points.csv ) if ( os.path.exists(node_path) or os.path.exists(point_path) ): raise FileExistsError( Move or rename existing field CSV files before export ) with open( node_path, w, newline ) as handle: writer csv.writer(handle) writer.writerow(( job, step, frame_time, instance, node_label, x_mm, y_mm, U1_mm, U2_mm, RF1_N, )) writer.writerows(all_nodes) with open( point_path, w, newline ) as handle: writer csv.writer(handle) writer.writerow(( job, step, frame_time, instance, element_label, integration_point, S11_MPa, E11, )) writer.writerows(all_points) print( Exported {} jobs: {} nodal rows, {} integration-point rows.format( len(jobs), len(all_nodes), len(all_points), ) ) def self_test(): from types import SimpleNamespace field SimpleNamespace( nameU, componentLabels(U1, U2) ) value SimpleNamespace( data(0.238095, 0.0) ) assert ( component(field, value, U1) 0.238095 ) print(Self-test OK) if __name__ __main__: if ( len(sys.argv) 2 and sys.argv[1] --self-test ): self_test() elif len(sys.argv) 2: main(os.path.abspath(sys.argv[1])) else: raise SystemExit( Usage: abaqus python lesson48_export_fields.py cases_dir )五、如何运行以及预期得到什么在配置好 Abaqus 的命令行环境中执行abaqus python lesson48_export_fields.py D:\abaqus_demo\cases请把示例目录改成自己的工况目录。脚本将读取actual_results.csv中的工况名逐个打开对应 ODB并在全部工况成功提取后生成field_nodes.csv field_integration_points.csv如果第四十七节的18个工况全部求解且通过验收预计有\[ 18\times354 \]条节点记录以及\[ 18\times236 \]条积分点记录。终端预计显示Exported 18 jobs: 54 nodal rows, 36 integration-point rows这是按模型规模计算的预期输出不是当前环境中的 Abaqus 实跑记录。本机没有 Abaqus 和这18份 ODB我只验证了代码的本地--self-test实际输出为Self-test OK六、为什么不直接导出“每个节点的应力”在 Abaqus/CAE 中我们经常看到平滑、连续的应力云图于是容易以为求解器直接在每个节点计算了唯一应力。实际并不总是这样。对通常的单元应力输出Abaqus 在积分点获得结果要在节点显示应力软件可能先将单元结果外推到节点再根据选项平均来自相邻单元的多个贡献。不同的平均设置可能改变图上的节点值。因此云图上的平滑节点应力不应被无条件当作原始积分点应力。Abaqus节点平均说明 · 路径结果生成说明本节刻意保留原始位置位移节点应力、应变积分点两者通过job、实例及网格拓扑关联而不是靠 CSV 行号关联。七、从拉杆推广到二维板时要增加哪些信息本节代码严格针对两单元 T2D2 拉杆并检查“3个节点、2个单元”。迁移到二维板、三维实体或断裂模型时至少要重新考虑变量分量。二维应力可能包含 \(S_{11}\)、\(S_{22}\)、\(S_{12}\)不能只导出 \(S_{11}\)。单元位置。一个单元可能有多个积分点壳单元还可能有不同厚度截面点。几何坐标。node.coordinates给出节点坐标不要把节点坐标直接冒充积分点坐标。Abaqus节点坐标说明网格不一致。不同工况若重新划分网格“节点100”未必对应同一空间位置。做全场学习前需要统一网格、映射到共同坐标系或采用能处理非结构化网格的模型。标识键扩展。对多实例或壳单元可能还要加入截面点、材料区域等字段才能唯一标识一条结果。八、课堂练习与小结基础练习只查看一个工况的两张导出表回答“为什么节点表有3行而积分点表只有2行”进阶练习选择同一材料和截面、载荷分别为 \(5000\) 与 \(10000\ \mathrm N\) 的两个工况检查节点3的 \(U_1\) 与单元1的 \(S_{11}\) 是否都约增大一倍。思考练习若某一二维板的两个相邻单元在同一个节点附近给出明显不同的积分点应力直接平均成一个“节点应力”会掩盖什么信息本节最重要的原则是任何一个仿真数值都必须连同“来自哪个工况、哪个时刻、哪个位置”一起保存。只有位置与来源清晰全场数据才能可靠地用于后续可视化、比较和机器学习。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →