本节摘要
前两节完成了 Abaqus 输入文件生成和 ODB 结果验收。本节把两单元拉杆扩展为材料、截面积、载荷三维参数研究:自动生成18个工况,逐个求解并复用上一节的验收脚本,最后只将通过检查的结果汇总为机器学习数据表。学习者将理解工况编号、理论基准、批量运行和数据追溯的重要性,同时认识“小样本演示数据”与“可信工程训练集”的区别。
一、这一节解决什么问题?
第四十五节,我们生成了三个不同载荷的模型;第四十六节,我们学会从 ODB 中提取结果并与理论值比较。现在的问题是:
如果需要同时改变材料、截面积和载荷,能否有条理地生成、计算、检查并保存所有工况?
本节建立的流程是:
设计参数表 ↓ 18份 .inp 输入文件 ↓ Abaqus逐个求解,得到18份 .odb ↓ 复用第46节脚本验收 ↓ 只合并通过验收的结果 ↓ ml_dataset.csvAbaqus 支持从已有输入文件创建并提交作业;命令行也提供job、input和interactive等选项。因此,我们不必为每一种参数组合手工重复建模。Abaqus作业说明 · 求解命令说明
二、设计一个规模适中的参数空间
仍使用两单元、长度 \(L=1000\ \mathrm{mm}\) 的拉杆。本节改变三个参数:
| 参数 | 取值 |
|---|---|
| 弹性模量 \(E\) | 70000、110000、210000 MPa |
| 截面积 \(A\) | 80、120 mm² |
| 轴向拉力 \(F\) | 5000、10000、15000 N |
总工况数为
\[ N=3\times2\times3=18. \]
我们用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\) 增大 | 增大 | 不变 |
例如固定 \(F=10000\ \mathrm N\)、\(A=80\ \mathrm{mm^2}\) 时,将 \(E\) 从 \(70000\) 提高到 \(210000\ \mathrm{MPa}\),末端位移会降为原来的三分之一;但由 \(\sigma=F/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, type=T2D2, elset=BAR 1, 1, 2 2, 2, 3 *Material, name=MAT *Elastic {modulus:g}, 0.3 *Solid Section, elset=BAR, material=MAT {area:g} *Boundary 1, 1, 2 2, 2, 2 3, 2, 2 *Step, name=PULL, nlgeom=NO *Static *Cload 3, 1, {force:g} *Output, field *Node Output U, RF *Element Output S, E *End Step """ def generate(): ROOT.mkdir(exist_ok=True) 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), encoding="ascii" ) 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="", encoding="utf-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( f"Created {len(rows)} input files " f"and expected_results.csv in {ROOT}" ) print( f"Example: {rows[0][0]}, " f"u3={float(rows[0][3]):.6f} mm, " f"stress={float(rows[0][4]):.3f} MPa" ) def read_csv(path): with path.open( newline="", encoding="utf-8" ) as handle: rows = list(csv.DictReader(handle)) if ( not rows or len({row["job"] for row in rows}) != len(rows) ): raise ValueError( f"Empty 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 " f"before overwriting: {output}" ) with output.open( "w", newline="", encoding="utf-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( f"Saved {len(rows)} " f"validated 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, u3=0.892857 mm, stress=62.500 MPa其中首个工况的理论计算为
\[ u_3= \frac{5000\times1000}{70000\times80} \approx0.892857\ \mathrm{mm}, \]\[ \sigma=\frac{5000}{80} =62.5\ \mathrm{MPa}. \]
六、第二步:先试算一例,再批量求解
进入cases目录后,先选一个工况试算:
abaqus job=AL_A080_F05000 input=AL_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条记录不足以可靠评估复杂模型。
- 物理关系过于简单。输出主要由 \(u=FL/(EA)\) 和 \(\sigma=F/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,以及通过验收的证据。