本节摘要前两节完成了 Abaqus 输入文件生成和 ODB 结果验收。本节把两单元拉杆扩展为材料、截面积、载荷三维参数研究自动生成18个工况逐个求解并复用上一节的验收脚本最后只将通过检查的结果汇总为机器学习数据表。学习者将理解工况编号、理论基准、批量运行和数据追溯的重要性同时认识“小样本演示数据”与“可信工程训练集”的区别。一、这一节解决什么问题第四十五节我们生成了三个不同载荷的模型第四十六节我们学会从 ODB 中提取结果并与理论值比较。现在的问题是如果需要同时改变材料、截面积和载荷能否有条理地生成、计算、检查并保存所有工况本节建立的流程是设计参数表 ↓ 18份 .inp 输入文件 ↓ Abaqus逐个求解得到18份 .odb ↓ 复用第46节脚本验收 ↓ 只合并通过验收的结果 ↓ ml_dataset.csvAbaqus 支持从已有输入文件创建并提交作业命令行也提供job、input和interactive等选项。因此我们不必为每一种参数组合手工重复建模。Abaqus作业说明 · 求解命令说明二、设计一个规模适中的参数空间仍使用两单元、长度 \(L1000\ \mathrm{mm}\) 的拉杆。本节改变三个参数参数取值弹性模量 \(E\)70000、110000、210000 MPa截面积 \(A\)80、120 mm²轴向拉力 \(F\)5000、10000、15000 N总工况数为\[ N3\times2\times318. \]我们用AL、TI、STEEL作为三组弹性模量的教学标签。这些标签只表示本节选用的 \(E\)并未定义真实材料的牌号、屈服强度或失效准则。尤其不能因为线弹性求解器给出了结果就认定某一载荷在实际材料中不会屈服。对每一个工况仍可以先计算理论基准\[ u_2\frac{F(L/2)}{EA},\qquad u_3\frac{FL}{EA},\qquad \sigma\frac{F}{A},\qquad R_{1x}-F. \]这些公式还能帮助我们预测参数的影响参数增大其他不变末端位移 \(u_3\)轴向应力 \(\sigma\)\(E\) 增大减小不变\(A\) 增大减小减小\(F\) 增大增大增大\(L\) 增大增大不变例如固定 \(F10000\ \mathrm N\)、\(A80\ \mathrm{mm^2}\) 时将 \(E\) 从 \(70000\) 提高到 \(210000\ \mathrm{MPa}\)末端位移会降为原来的三分之一但由 \(\sigmaF/A\) 可知应力不会因此改变。三、先规定文件和数据表的结构每个工况都需要一个不会混淆的编号。例如AL_A080_F05000它表示材料组AL、截面积 \(80\ \mathrm{mm^2}\)、载荷 \(5000\ \mathrm N\)。本节采用“一工况一文件”的形式AL_A080_F05000.inp → 模型输入 AL_A080_F05000.odb → Abaqus实算结果同时保留三个表expected_results.csv输入参数与解析解生成输入文件时写入。actual_results.csv从 ODB 提取的实算值及PASS/FAIL由第四十六节脚本写入。ml_dataset.csv仅合并通过验收的工况由本节脚本写入。请特别注意expected_results.csv中的解析解不是 Abaqus 仿真结果更不能把它改名后冒充训练数据。四、完整程序生成工况并合并已验收结果将下面代码保存为lesson47_batch.py。它只使用 Python 标准库不需要安装 Pandas。程序分为两个操作generate生成输入文件merge在 Abaqus 求解及第四十六节验收完成后合并数据。Generate Abaqus bar cases, then merge validated results into a CSV. import csv import sys from itertools import product from pathlib import Path ROOT Path(__file__).resolve().parent / cases LENGTH_MM 1000.0 MATERIALS ( (AL, 70000.0), (TI, 110000.0), (STEEL, 210000.0), ) AREAS_MM2 (80.0, 120.0) LOADS_N (5000.0, 10000.0, 15000.0) def deck(job, modulus, area, force): return f*Heading ** {job}; units: N, mm, MPa *Node 1, 0., 0. 2, {LENGTH_MM / 2:g}, 0. 3, {LENGTH_MM:g}, 0. *Element, typeT2D2, elsetBAR 1, 1, 2 2, 2, 3 *Material, nameMAT *Elastic {modulus:g}, 0.3 *Solid Section, elsetBAR, materialMAT {area:g} *Boundary 1, 1, 2 2, 2, 2 3, 2, 2 *Step, namePULL, nlgeomNO *Static *Cload 3, 1, {force:g} *Output, field *Node Output U, RF *Element Output S, E *End Step def generate(): ROOT.mkdir(exist_okTrue) if ( any(ROOT.glob(*.odb)) or (ROOT / actual_results.csv).exists() ): raise RuntimeError( Existing solver results found; use a new case folder ) rows [] for (material, modulus), area, force in product( MATERIALS, AREAS_MM2, LOADS_N ): job f{material}_A{area:03.0f}_F{force:05.0f} inp_path ROOT / f{job}.inp inp_path.write_text( deck(job, modulus, area, force), encodingascii ) u2 force * (LENGTH_MM / 2) / (modulus * area) u3 force * LENGTH_MM / (modulus * area) assert abs(u3 - 2 * u2) 1e-12 rows.append(( job, force, f{u2:.6f}, f{u3:.6f}, f{force / area:.6f}, -force, material, modulus, area, LENGTH_MM, )) expected_path ROOT / expected_results.csv with expected_path.open( w, newline, encodingutf-8 ) as handle: writer csv.writer(handle) writer.writerow(( job, load_N, u2_mm, u3_mm, sigma_MPa, reaction_node1_N, material, E_MPa, A_mm2, L_mm, )) writer.writerows(rows) print( fCreated {len(rows)} input files fand expected_results.csv in {ROOT} ) print( fExample: {rows[0][0]}, fu3{float(rows[0][3]):.6f} mm, fstress{float(rows[0][4]):.3f} MPa ) def read_csv(path): with path.open( newline, encodingutf-8 ) as handle: rows list(csv.DictReader(handle)) if ( not rows or len({row[job] for row in rows}) ! len(rows) ): raise ValueError( fEmpty or duplicate jobs in {path} ) return rows def build_dataset(expected_rows, actual_rows): actual { row[job]: row for row in actual_rows } expected_jobs { row[job] for row in expected_rows } if expected_jobs ! set(actual): raise ValueError( Expected and actual job lists differ ) if any( row[check] ! PASS for row in actual_rows ): raise ValueError( At least one ODB result failed validation ) return [ [ row[job], row[material], row[E_MPa], row[A_mm2], row[L_mm], row[load_N], actual[row[job]][u2_mm], actual[row[job]][u3_mm], actual[row[job]][s1_MPa], actual[row[job]][s2_MPa], actual[row[job]][e1], actual[row[job]][e2], actual[row[job]][rf1_N], ] for row in expected_rows ] def merge(): expected read_csv( ROOT / expected_results.csv ) actual read_csv( ROOT / actual_results.csv ) rows build_dataset( expected, actual ) output ROOT / ml_dataset.csv if output.exists(): raise FileExistsError( Move or rename the existing file fbefore overwriting: {output} ) with output.open( w, newline, encodingutf-8 ) as handle: writer csv.writer(handle) writer.writerow(( job, material, E_MPa, A_mm2, L_mm, load_N, u2_mm, u3_mm, s1_MPa, s2_MPa, e1, e2, rf1_N, )) writer.writerows(rows) print( fSaved {len(rows)} fvalidated rows to {output} ) def self_test(): expected [{ job: demo, material: AL, E_MPa: 70000, A_mm2: 80, L_mm: 1000, load_N: 5000, }] actual [{ job: demo, check: PASS, u2_mm: 0.446429, u3_mm: 0.892857, s1_MPa: 62.5, s2_MPa: 62.5, e1: 0.000892857, e2: 0.000892857, rf1_N: -5000, }] assert ( build_dataset(expected, actual)[0][7] 0.892857 ) print(Self-test OK) if __name__ __main__: if ( len(sys.argv) ! 2 or sys.argv[1] not in (generate, merge, --self-test) ): raise SystemExit( Usage: python lesson47_batch.py generate|merge|--self-test ) actions { generate: generate, merge: merge, --self-test: self_test, } actions[sys.argv[1]]()T2D2仍是上一节的二维桁架单元截面积通过Solid Section指定这使三组参数变化在输入文件中都能直接找到。Abaqus桁架截面说明五、第一步生成18个工况在普通 Python 环境中运行py lesson47_batch.py generate我已实际运行生成部分输出如下Created 18 input files and expected_results.csv in ...\cases Example: AL_A080_F05000, u30.892857 mm, stress62.500 MPa其中首个工况的理论计算为\[ u_3 \frac{5000\times1000}{70000\times80} \approx0.892857\ \mathrm{mm}, \]\[ \sigma\frac{5000}{80} 62.5\ \mathrm{MPa}. \]六、第二步先试算一例再批量求解进入cases目录后先选一个工况试算abaqus jobAL_A080_F05000 inputAL_A080_F05000.inp interactive核对其.sta、.msg等文件并确认 ODB 可以打开后再处理剩余工况。在 Windows PowerShell 中可用下面的循环顺序提交Get-ChildItem -Filter *.inp | ForEach-Object { $jobName $_.BaseName abaqus job$jobName input$($_.Name) interactive if ($LASTEXITCODE -ne 0) { throw 作业未正常退出$jobName } }这里选择顺序运行便于初学者发现第一处错误也避免一次提交18个作业占用过多许可证和计算资源。即使命令返回了退出码零仍须检查求解器的状态与诊断信息。如果刚才已经手工完成了首个工况批量循环会再次遇到同名作业运行前请确认是否允许 Abaqus 覆盖原有作业输出或从待运行列表中排除该工况。不要在未确认结果备份的情况下盲目重复提交。七、第三步复用第四十六节的验收程序将第四十六节的 [lesson46_extract_odb.py]放到cases目录在 Abaqus 环境下运行abaqus python lesson46_extract_odb.py .该程序会按照expected_results.csv中的工况编号寻找同名 ODB读取节点位移、两个单元的应力和应变、左端反力并写出actual_results.csv。我们没有为第47节重新编写 ODB 读取器工况数量从3变成18但读取规则并没有变。这也是本节代码只负责生成和合并、不重复实现结果提取的原因。八、第四步只合并通过检查的结果所有工况的actual_results.csv均已生成且通过验收后回到保存lesson47_batch.py的目录运行py lesson47_batch.py merge如果18个工况都通过预期提示为Saved 18 validated rows to ...\cases\ml_dataset.csv若某个工况缺失、编号不一致或第46节检查结果为FAIL程序会停止而不会生成一张看似完整、实际混有问题数据的训练表。ml_dataset.csv中E_MPa、A_mm2、L_mm、load_N是输入条件u2_mm、u3_mm、s1_MPa等来自 ODB 的输出结果。job是追溯编号不是应直接喂给模型的数值特征。九、得到CSV之后还不能立刻宣称“有了高质量训练集”本节的18个工况适合学习数据管道却不适合用来证明某个机器学习算法很强原因有三样本太少。18条记录不足以可靠评估复杂模型。物理关系过于简单。输出主要由 \(uFL/(EA)\) 和 \(\sigmaF/A\) 决定训练神经网络并不能说明它胜过解析公式。参数覆盖有限。本节固定了长度、单元类型和边界条件模型无法据此证明自己适用于裂纹、接触、塑性或不同结构。将来真正构建力学代理模型时应记录几何定义、网格密度、材料模型、边界条件、单位体系和求解器版本并设计独立的测试工况。若同一几何下生成大量相近载荷样本随意逐行随机划分训练集和测试集容易高估模型在“新几何”上的能力。十、课堂练习基础练习找出相同 \(E\)、相同 \(A\)但载荷分别为 \(5000\) 和 \(10000\ \mathrm N\) 的两个工况。预测并检查\[ \frac{u_3(10000)}{u_3(5000)} \approx2,\qquad \frac{\sigma(10000)}{\sigma(5000)} \approx2. \]进阶练习保持 \(E\) 和 \(F\) 不变将截面积从 \(80\) 增至 \(120\ \mathrm{mm^2}\)。先根据公式预测位移和应力应变为原来的多少再从两个 ODB 中提取数据验证。思考练习如果我们把同一 ODB 的多个时间帧都作为独立样本随机分到训练集和测试集为什么可能产生数据泄漏本节最终建立的不是一张“数字很多”的表而是一条可追溯的流程每一行结果都能找到对应参数、输入文件、ODB以及通过验收的证据。