Abaqus 二次开发教程(8):分析步、边界与输出请求——数据在 Model 仓库、作用域在 Step
Abaqus 二次开发教程(8):分析步、边界与输出请求——数据在 Model 仓库、作用域在 Step
版本声明块
- 工具/软件:Abaqus 2026(2026 FD01),内嵌 Python 3.10.5(2024 GA 起;此前为 Python 2.7.15,旧版本)
- 语言/环境:Python + Abaqus Scripting Interface(ASI),
Step/BoundaryCondition/OutputRequest都挂在 Model 仓库上;脚本可运行于 Abaqus/CAE 或abaqus cae noGUI=(第 1 篇)- 本文目标:把“步、边界、输出”三件事的分工写清楚,并给出一条可数判定:输出帧数必须与脚本请求一致
一句话结论:ASI 里数据挂在 Model 仓库、作用域由createStepName决定——写在Initial上的边界条件是初始状态、写在LoadStep上的边界条件是该步的变化量;场输出(field output)与历史输出(history output)是两个独立仓库必须分别请求,frequency是计数语义(每frequency个增量写一帧),未请求的变量在 ODB 里读不到;长时显式分析必须显式开重启动(restart),否则中断只能从头算。
〇、本篇要解决的认知问题
createStepName='Initial'与createStepName='LoadStep'的边界条件(BoundaryCondition),在输入文件里的语义到底有什么不同?StaticStep的增量控制参数(initialIncrement、maxNumIncrements等)为什么能让一个“简单静力”在第一个增量就失败?这些默认值该信任还是显式写?- 场输出与历史输出为什么是两个独立仓库?只请求场输出时,后处理读能量类历史变量会发生什么?
frequency设成 1 与设成 100,分别如何改变 ODB 的帧数与体积?脚本层怎么用可数数字判定“输出请求真的生效了”?- 重启动(restart)与输入文件里的
*RESTART是什么关系?为什么显式长时分析必须显式请求,而默认行为不一定给你?
一、机制解析
1.1 三个仓库、一个作用域:数据在 Model,生效在 Step
model.steps ← 步仓库('Initial' 永远存在,但域与语义不同于普通 Step) model.boundaryConditions ← 边界条件仓库:引用 region + createStepName(字符串) model.loads ← 载荷仓库:同样引用 region + createStepName model.fieldOutputRequests ← 场输出请求:createStepName + variables + frequency model.historyOutputRequests ← 历史输出请求:createStepName + variables + frequency model.amplitudes ← 幅值曲线(Amplitude):被载荷/边界按名字引用 model.timePoints ← 时间点(TimePoint):被输出请求按名字引用 model.keywordBlock ← 唯一绕过 ASI、直接写关键字的口子(第 5 篇)这套设计的核心是引用与生效分离:边界条件对象本身不属于某个步,它属于 Model 仓库,只是通过createStepName这个字符串声明“在哪个步生效”。三个直接后果:
changeKey改名后,createStepName存的字符串不会跟着变(第 5 篇的命名陷阱在这里直接咬人);- 删除一个步不会自动删除引用它的边界条件,提交时会在输入文件里出现引用不存在的步;
- 输出请求写在
Initial上合法,但它只描述初始状态,不产生时域结果。
为什么这对你重要:批量参数扫描里“结果读不到”最常见的原因不是求解失败,而是作用域错配——边界条件写在Initial、输出请求写在另一个步,两者在 ODB 里对不上;而脚本层无法从报错看出这一点,因为它根本没报错。
过渡句:作用域解释了“写在哪里”,增量控制解释了“算得多细”。这两件事在隐式静力里最容易互相干扰。
1.2 增量控制:隐式与显式的默认行为差异
| 项目 | Abaqus/Standard(隐式静力 / 隐式动力) | Abaqus/Explicit(显式动力) |
|---|---|---|
| 时间增量 | 由牛顿迭代自适应,受initialIncrement、minIncrement、maxIncrement约束 | 由稳定条件决定(依赖单元尺寸与波速sqrt(E/ρ)) |
| 典型失败 | 增量收缩到下限 → 收敛不上(现象文本以官方文档为准) | 步长过小 → 计算时间爆炸 |
| 与第 7 篇的联系 | 无直接依赖 | 密度单位错 → 质量错 → 稳定增量步长错 → 用时错 |
| 帧来源 | 每增量或按frequency计数 | 按frequency(显式增量数以万计,必须降频) |
关于默认值必须说清楚:ASI 各步对象的参数默认值在参考文档里是显式列出的(例如initialIncrement、maxNumIncrements、amplitude),本系列不凭记忆复述具体数字——以官方文档为准。脚本层的安全写法是把这些参数显式写出:显式写initialIncrement与maxNumIncrements,比在结果异常后倒推默认值便宜得多。
【经验法则】隐式静力在第一个增量就失败,通常不是网格问题,而是载荷阶跃过大——用幅值曲线(Amplitude)把载荷改成渐变,比加密网格更有效;这是本系列反复出现的一条判断。
过渡句:求解器算得多细是一回事,你在 ODB 里能否读到是另一回事。输出请求不参与求解,只参与写盘。
1.3 场输出与历史输出:为什么必须分开请求
FieldOutputRequest → 每一帧在“空间”上的一组值(节点/单元/积分点变量) 例:U、V、A、S、E、PE;总量类(ALLIE 等)不在此列 判定:frames 数量 = 按 frequency 计数得到的帧数 HistoryOutputRequest → 在“某个特定区域或点”上随时间变化的标量序列 例:反力、能量(ALLIE、ALLKE、ALLHLL)、指定节点位移 判定:ODB 的 histories 必须含该变量;没请求 → 读不到三条反直觉的事实:
- 默认请求是一套变量,不是全部变量:脚本没写请求时,ODB 里通常仍含一组默认变量,但不能假设它包含你要读的那一项。判定必须以 ODB 实际内容为准(第 9 篇用
namedQueries与getSubset打印实际变量清单)。【最佳实践】后处理脚本在读取前先打印请求清单与 ODB 实际变量清单,两者不一致即判失败。 - 历史变量不会出现在场输出里:能量类是历史输出。只请求场输出的脚本在后处理里读不到沙漏能量——第 6 篇【经验法则】的 1% 沙漏阈值因此无法判定。要做这类判定的脚本必须显式请求历史输出。
frequency是计数语义:frequency=1表示每增量写一帧;显式分析一个步可能有几十万增量,所以显式的输出频率通常必须远大于 1。【经验法则】显式后处理的帧数量级按后处理需求确定(脚本层用“帧数 = 1 + 增量数 ÷ frequency”做可数判定),具体默认值以官方文档为准。
过渡句:下面三段代码把这三层机制写成可判定规则——第一段完整建模并显式请求,第二段用纯 Python 把帧数与体积写成硬判据,第三段验证“没请求就读不到”。
二、完整代码与逐行剖析
2.1 完整可运行脚本:步、边界、幅值与输出请求
# step_bc_output.py —— 作用域、增量与输出请求的完整写法(mm-N-s)fromabaqusimportmdb,session# noGUI 下 session 不可用(【铁律】第 8 条)fromabaqusConstantsimport*#【铁律】第 3 条:符号常量不可省略importregionToolsetimportsys MODEL,JOB='Cantilever','Cantilever'L,H,W=100.0,10.0,10.0F=100.0model=mdb.Model(name=MODEL)sketch=model.ConstrainedSketch(name='profile',sheetSize=200.0)sketch.rectangle(point1=(0.0,0.0),point2=(W,H))part=model.Part(name='Beam',dimensionality=THREE_D,type=DEFORMABLE_BODY)part.BaseExtrusion(sketch=sketch,depth=L)part.regenerate()# isOutOfDate=1 时后续操作不可靠(第 5 篇)model.Material(name='Steel')model.materials['Steel'].Elastic(table=((210000.0,0.3),))model.materials['Steel'].Density(name='Density',table=((7.85e-9,),))#【铁律】第 1 条model.HomogeneousSolidSection(name='Section',material='Steel',thickness=1.0)part.Set(name='AllCells',cells=part.cells)part.SectionAssignment(region=regionToolset.Region(cells=part.cells),sectionName='Section')inst=model.rootAssembly.Instance(name='BeamInst',part=part,dependent=ON)# 关键决策点 1:Initial 上的 BC 是“初始状态”,步里的 BC 是“该步的变化量”,语义不同part.Set(name='FixedFace',faces=part.faces.findAt(((0.0,W/2.0,H/2.0),)))model.DisplacementBC(name='FixedInitial',createStepName='Initial',region=inst.sets['FixedFace'],u1=0.0,u2=0.0,u3=0.0)# 关键决策点 2:显式写增量控制参数,不依赖默认(默认值以官方文档为准)model.StaticStep(name='LoadStep',previous='Initial',timePeriod=1.0,initialIncrement=0.1,maxNumIncrements=1000)# 关键决策点 3:把阶跃载荷写成渐变——比加密网格更能救收敛(1.2 节的经验法则)model.TabularAmplitude(name='Ramp',timeSpan=1.0,data=((0.0,0.0),(1.0,1.0)))part.Surface(name='TipSurf',side1Faces=part.faces.findAt(((W/2.0,H/2.0,L),)))model.Pressure(name='TipLoad',createStepName='LoadStep',region=inst.surfaces['TipSurf'],magnitude=F/(W*H),amplitude='Ramp')# 关键决策点 4:场输出与历史输出是两个仓库,必须分别请求(1.3 节)if'F-1'notinmodel.fieldOutputRequests:model.FieldOutputRequest(name='F-1',createStepName='LoadStep',variables=('U','S'),frequency=1)if'H-1'notinmodel.historyOutputRequests:# 历史变量集合由请求决定;这里显式请求能量类,用于第 6 篇的沙漏判定model.HistoryOutputRequest(name='H-1',createStepName='LoadStep',frequency=10)# 自检:三条可观察判定(作用域、请求内容、引用完整性)print('判定 1:步仓库 = %s(含 Initial)'%sorted(model.steps.keys()))print('判定 2:BC 作用域 = %s'%model.boundaryConditions['FixedInitial'].createStepName)print('判定 3:场输出变量 = %s;历史输出请求数 = %d'%(sorted(model.fieldOutputRequests['F-1'].variables),len(model.historyOutputRequests)))# 重启动:关键字块是绕过 ASI 的口子;方法名与参数以官方文档为准,故用 try 包住try:model.keywordBlock.write(syntax='Abaqus',line='*RESTART, WRITE, OVERWRITE, FREQUENCY=100')print('判定 4:关键字块已写入 restart 请求(长时显式分析必写)')exceptExceptionase:print('判定 4:关键字块写入方式(%s)以官方文档为准'%type(e).__name__)mdb.Job(name=JOB,model=MODEL,numCpus=1)job=mdb.jobs[JOB]job.submit()job.waitForCompletion()#【最佳实践】异步提交必须显式等待(第 3 篇)print('判定 5:job.status = %s(用符号常量比较,不用字符串)'%job.status)sys.exit(0)逐行看关键决策点:
createStepName='Initial'与createStepName='LoadStep'分处两个语义域:前者是初始状态、后者是步内变化量。同一个边界条件若写在LoadStep上、值为 0.0,它表达的是“相对初始状态不变”,这与写在Initial上并不等价——输入文件里的写法不同。TabularAmplitude的smooth、timeSpan等参数默认值以官方文档为准;脚本显式传timeSpan与data,比依赖默认更稳。HistoryOutputRequest没写variables:默认历史变量集合由求解器决定(以官方文档为准)。如果脚本要做能量判定,必须显式写出变量名,否则第 6 篇的 1% 阈值无法判定——这是本篇与第 6 篇的直接接口。- 段末的
job.status用符号常量比较(第 2 篇),不要用字符串。
2.2 输出帧数与体积:写成可数判定的预算脚本
# output_budget.py —— 纯 Python 的帧数/体积预算(可独立运行,不依赖 Abaqus 环境)importsys NODES=125# 第 3 篇的硬校验:125 节点INCREMENTS=40# 本次 LoadStep 的增量数(假设值;实际增量数以 .sta/.msg 为准)defframes_by_frequency(frequency,increments):"""frequency 是计数语义:每 frequency 个增量写一帧;第 1 帧通常是初始状态。"""iffrequency<1:return1+incrementsreturn1+increments//frequencydefestimate_kb(frames,n_vars,entities,bytes_per_value=8):"""量级估算:帧数 × 变量数 × 实体数 × 每值字节(浮点)。这是【经验法则】量级,不是精确值。"""returnframes*n_vars*entities*bytes_per_value/1024.0cases=[(1,2),(5,2),(1,6),(10,2),(1,12)]print('%-10s %-8s %-8s %s'%('frequency','变量数','帧数','估算体积 KB'))forfreq,n_varsincases:frames=frames_by_frequency(freq,INCREMENTS)print('%-10d %-8d %-8d %.1f'%(freq,n_vars,frames,estimate_kb(frames,n_vars,NODES)))# 硬判据 1:frequency=1 时帧数必须等于 1 + 增量数 = 41expected_frames=frames_by_frequency(1,INCREMENTS)print('硬判据 1:frequency=1 → 帧数必须为 %d'%expected_frames)# 硬判据 2:同帧数下,6 个变量的估算体积必须是 2 个变量的 3 倍(允许 10% 偏差)v2=estimate_kb(expected_frames,2,NODES)v6=estimate_kb(expected_frames,6,NODES)ok2=abs(v6/v2-3.0)<=0.10print('硬判据 2:%s'%('通过'ifok2else'变量数与体积不成线性'))# 硬判据 3:降频 5 倍必须把帧数降到 1/5 量级(加初始帧)f1=frames_by_frequency(1,INCREMENTS)f5=frames_by_frequency(5,INCREMENTS)ok3=abs(f1/f5-5.0)<=0.3print('硬判据 3:%s(帧数 %d vs %d)'%('通过'ifok3else'频率语义与脚本假设不符',f1,f5))ok=expected_frames==41andok2andok3print('结论:%s'%('通过'ifokelse'输出请求的计数语义与脚本假设不符'))sys.exit(0ifokelse1)要点:
- 这段刻意不调用任何 Abaqus 接口:它把“帧数 = 1 + 增量数 ÷ frequency”这条语义写成断言。真正的增量数要从
.sta/.msg读取(第 10 篇),但脚本层必须先有一个可断言的模型,否则 ODB 读到 41 帧还是 4 帧时你无法判断是请求生效还是求解失败。 - 硬判据 3 是排查“输出请求没生效”的最便宜方法:如果实际帧数与
1 + 增量数 ÷ frequency不符,说明请求写在别的步上、或频率被默认请求覆盖。 - 【最佳实践】把输出预算写进脚本头部注释(本例 41 帧、2 变量、125 节点),这样后处理脚本在 ODB 里读到的帧数可以直接与注释比对,而不是靠肉眼。
2.3 验证“没请求就读不到”:可运行的对照脚本
# request_vs_read.py —— 用 ODB 实际内容证明“未请求的变量读不到”(【铁律】第 9 条)fromabaqusimportmdb,sessionfromabaqusConstantsimport*importodbAccessimportsys JOB='Cantilever'STEP='LoadStep'WANT_FIELD=('U','S')# 已在 2.1 中请求WANT_HISTORY=('ALLIE','ALLHLL')# 未显式请求时,这一项常常读不到odb=odbAccess.openOdb(path=JOB+'.odb',readOnly=True)try:step=odb.steps[STEP]frames=step.framesprint('实际帧数 = %d(应与 2.2 的预算一致)'%len(frames))# 场输出:打印这一帧实际有哪些变量——这是“请求是否生效”的唯一证据available=sorted(odb.rootAssembly.nodeOutputs.keys())print('ODB 场输出变量(节点级):%s'%available)missing_field=[vforvinWANT_FIELDifvnotinavailable]print('判定 A:未请求/未写入的场变量 = %s'%(missing_fieldifmissing_fieldelse'无'))# 历史输出:能量类只存在于 histories,不在 fieldOutputs 里(1.3 节)hist_vars=[]ifhasattr(odb.rootAssembly,'namedQueryKeys'):hist_vars=sorted(odb.rootAssembly.namedQueryKeys)try:hist_keys=sorted(odb.rootAssembly.historyRegions.keys())print('历史区域:%s'%hist_keys)region=odb.rootAssembly.historyRegions[hist_keys[0]]got=sorted(region.historyOutputs.keys())print('历史变量:%s'%got)missing_hist=[vforvinWANT_HISTORYifvnotingot]print('判定 B:未请求的历史变量 = %s'%(missing_histifmissing_histelse'无'))exceptExceptionase:print('历史区域接口形态(%s)以官方文档为准'%type(e).__name__)ok=(notmissing_field)andbool(hist_vars)print('结论:%s'%('请求与读取一致'ifokelse'需要补输出请求'))finally:odb.close()#【铁律】第 9 条:显式关闭 ODBsys.exit(0)要点:
- 判定 A 与判定 B 是两条不同的证据链:场输出看
nodeOutputs,历史输出看historyRegions。两者不共享变量集合——这就是 1.3 节第 2 条的可执行版本。 - 接口形态(
namedQueryKeys、historyRegions的键)在不同版本可能不同,脚本用hasattr/try包住并明确“以官方文档为准”,而不是照抄某版本的报错文本。 - 这段的判定标准是可观察输出:判定 A 必须打印“无”,判定 B 必须打印出至少一个能量变量;若判定 B 打印“无”,说明请求阶段漏写历史输出(回到 2.1 补
HistoryOutputRequest)。
三、常见报错与排查
现象:后处理读不到某个变量(例如能量或反力),脚本没有报错。
根因:该变量属于历史输出而脚本只请求了场输出;或请求写在另一个步上(作用域错配)。
解法:跑 2.3 的判定 A/B;把HistoryOutputRequest显式写全createStepName与变量名;用 2.2 的帧数断言确认请求生效。现象:ODB 帧数与脚本预期的
frequency不符(例如请求frequency=1却只有 4 帧)。
根因:实际增量数少于假设值(求解器把步内合并成少数增量),或输出请求被默认请求覆盖,或请求建在Initial上。
解法:从.sta/.msg读实际增量数(第 10 篇),再按“帧数 = 1 + 增量数 ÷ frequency”判定;把请求名与createStepName打印出来核对。现象:隐式静力在第一个增量就失败、增量不断收缩(现象文本以官方文档为准)。
根因:载荷为阶跃、接触初始状态未建立,或增量控制参数被默认值限制。
解法:用幅值曲线(TabularAmplitude)把载荷改渐变;显式写initialIncrement与maxNumIncrements;先建立接触再加载(分步)。默认值一律以官方文档为准。现象:
KeyError: 'LoadStep'或提交时引用了不存在的步。
根因:createStepName存的是字符串,步被改名或删除后引用不会自动更新(第 5 篇)。
解法:脚本入口把步名当参数;开头打印sorted(model.steps.keys());校验脚本检查“每个 BC/载荷的createStepName存在于 steps”。现象:显式长时分析跑完或中断后无法续算,只能从头重跑。
根因:没有请求重启动(restart),输入文件里没有*RESTART, WRITE;显式默认行为不保证给你重启动文件。
解法:在脚本里显式写重启动请求(关键字块或对应方法,方法名与参数以官方文档为准),并在提交后检查产物里是否存在重启动文件;读取时用*RESTART READ或对应接口(以官方文档为准)。
四、动手练习
帧数断言:在 2.1 的模型上提交作业,然后运行 2.3,并把
frequency从 1 改成 5 再提交一次。
判定标准:第一次 ODB 帧数必须等于1 + 实际增量数;第二次必须约为第一次的 1/5(加初始帧)。两次数字与 2.2 的预算表逐行对照,偏差 > 30% 即为请求未生效。历史输出补请求:先只请求场输出,跑 2.3 的判定 B;再补上
HistoryOutputRequest重跑。
判定标准:第一次判定 B 必须打印“无”或空清单、退出码 1;第二次判定 B 必须至少列出 1 个能量变量、退出码 0。这是本篇唯一的“请求层”验证。作用域对照(思考题,附验证要点):解释同一个位移边界条件写在
Initial与写在LoadStep的差别。
验证要点清单:① 打印model.boundaryConditions['FixedInitial'].createStepName必须为Initial;② 把同一个 BC 改到LoadStep后提交,比较两次输入文件里该边界条件出现的段落(第二次出现在步内);③ 删除LoadStep后运行校验脚本,必须报“引用了不存在的步”。三条都要有输出记录。
五、小结与下一篇预告
本篇的机制核心是三层分离:数据挂在 Model 仓库、作用域靠createStepName字符串、输出靠独立的请求仓库。这解释了三类“不报错的错误”:读不到变量(未请求或作用域错配)、帧数与预期不符(频率计数语义与实际增量数不一致)、引用失效(改名后字符串不更新)。可判定标准是三条硬判据:帧数 = 1 + 增量数 ÷frequency、同帧数下体积与变量数成线性、历史变量必须显式请求才读得到。第 9 篇把这些变量从 ODB 导出成 CSV——那里getSubset与sets的键空间陷阱会直接接上本篇的判定 A/B;第 10 篇讨论作业提交与abaqus_v6.env,实际增量数与并行核数从哪里读、又从哪里被覆盖。第 6 篇的沙漏 1% 阈值能否判定,取决于本篇是否请求了历史输出——这是本系列里最典型的一处跨篇依赖。
本篇认知问题回显(FAQ)
Q1:Abaqus 脚本里createStepName='Initial'的边界条件与写在分析步里的边界条件有什么不同?
A:Initial上的边界条件描述初始状态,分析步里的边界条件描述该步内相对初始状态的变化量,两者在输入文件里的写法与作用域不同。
Q2:为什么 Abaqus 的隐式静力分析常在第一个增量就失败,而加密网格没用?
A:常见根因是载荷阶跃或接触初始状态未建立导致增量收缩到下限;用幅值曲线(TabularAmplitude)把载荷改渐变比加密网格更有效。
Q3:FieldOutputRequest与HistoryOutputRequest为什么必须分开请求?
A:两者是 Model 上的两个独立仓库,场输出给空间上的变量集合、历史输出给指定区域随时间的标量序列;能量类变量只在历史输出里,不请求就读不到。
Q4:Abaqus 输出请求的frequency是什么语义,如何判定它生效了?
A:frequency是计数语义(每frequency个增量写一帧),判定式为 ODB 帧数 = 1 + 实际增量数 ÷frequency,不符即为请求写在别的步上或被默认请求覆盖。
Q5:为什么 Abaqus/Explicit 的长时分析要显式请求重启动(restart)?它与*RESTART什么关系?
A:重启动靠输入文件里的*RESTART, WRITE产生重启动文件,显式默认行为不保证生成;没有该文件中断后只能从头算,读取时用*RESTART READ(以官方文档为准)。