1. 为什么要把和Ansys绑在一起
仿真工程师在日常的工作时间里, 有很大一部分时间并不是花在“想问题”上, 而是花的功夫主要是“点鼠标”。
改一个尺寸, 接着跑一次求解, 然后等结果出现, 再导出云图展示情况, 最后再去改下一个尺寸, 这套动作重复几十次甚至上百次的情况下, 人很容易陷入麻木的状态, 并且也特别容易在某个具体的环节中出现操作错误的情况。
自动化来驱动这个Ansys仿真工作, 其实说白了这件事的最终目的就是把重复性比较高的劳动都交由脚本来进行, 从而使得工程师能够将更多的时间和精力放在判断和决策上面。
在我所接触的那些仿真团队里面, 凡是可以把参数化扫描、批量后处理、报告生成这些环节都跑顺的, 交付的节奏都明显更加稳定。原因其实也不复杂, 因为脚本是不会疲劳的。它不会因为到了下午就犯困, 从而把边界条件设置有错误, 也不会漏掉某一个具体的工况。
它所要解决的问题是很具体的, 也就是重复点击、参数组合产生爆炸式增长、结果整理需要耗费时间、人为操作存在不一致性这些问题。那么究竟适合哪一些人来作为参考呢。
对于已经有了一定Ansys操作基础, 会写一点代码, 并且被重复劳动折磨过的仿真从业者来说是最为合适的;即便是完全没碰过Ansys的人也能看懂相关的思路, 但是还是建议先把软件的基本操作流程走一遍再开始上手编写脚本。
此处必须先将一个基础前提交代明白。因为安西斯这个家族的产品线长度是非常可观的, 并且不同模块内部的脚本接口存在着不小的差异之处。所以本文正在展开讨论的自动化思考路径是严格围绕着参数化建模、批量求解以及结果提取这一条主要脉络来进行的。
当具体涉及到相关接口的时候, 会对适用的场景做出明确的说明, 而不会去绑定任何一个特定的软件版本。毕竟你手中所持有的软件版本有可能和使用本文作者的版本是不一样的现象发生, 不过支撑这些操作的底层逻辑在本质上是处于相通状态的。
2. 整体设计思路和方案选择, 得先弄清楚自动化哪些事情, 然后再去想怎么实现自动化。
现在的大多数人都喜欢一开始就直接去寻找接口文档, 可是他们在写完了一大堆代码之后才发现自己的方向走错了。
我平时养成的习惯是先绘制一张关于动作的清单, 具体做法是把一次完整的仿真过程拆解成建模、赋值材料、划分网格、设定边界条件、进行求解、后期处理以及生成报告这些不同的阶段, 然后再针对每一个阶段去标注清楚它属于固定的环节还是属于变化的环节, 对于固定不变的部分, 比较适合封装成函数以便重复使用, 而对于那些会发生变化的部分, 才有必要采取参数化处理的策略。
因为这个顺序是很关键的, 之所以这样觉得, 是因为自动化方面的价值主要是集中在变化上的, 如果连那种固定的动作都想要塞进脚本里面去的话, 那么代码就会变得特别冗杂, 而且很难去维护。
现在给大家举一个具体的实例。有一个团队需要针对一批支架结构开展强度验证, 其中这些支架的几何外形是确定不变的, 仅仅是在板件的厚度以及载荷作用的坐标位置上存在变化。
针对这种情况, 自动化工作的重心就应当集中在四个方面, 即修改板壳厚度、变更载荷的位置坐标、进行大规模的迭代计算以及提取其中的最大应力值, 至于网格划分的具体策略则可以保持固定而不必调整。
反观另一种情形, 如果所处理的几何对象在每次任务中都不相同, 那么工作的核心就需要向参数化建模以及几何模型清理等前期环节转移。只有先将这两个方面的重要程度区分清楚, 在后期的工具选择过程中才不会出现方向性的错误。
2.2 三条主流技术路线怎么选
Ansys的自动化接口, 从大的划分角度来看, 大致可以归为三种类型, 每一种类型都具有一定的特性差异, 如果在选择的时候出现失误, 就会导致在实践中遭受诸多不必要的波折与阻碍。
路线 典型方式 适合场景 主要代价
脚本录制回放
软件自带脚本录制,改吧改吧就用
快速验证、单次任务
录出来的代码冗长,参数化改造费劲
原生脚本接口
各模块自带的/命令接口
参数化建模、批量求解
需要熟悉接口对象模型
外部流程编排
调起求解器、读写结果文件
多工况调度、跨工具串联
要处理进程、路径、异常
我的这个建议就是, 让新手先从录制回放这个地方开始入门, 等到摸清了对象的内部构造之后, 再转向使用原生接口, 最后利用外部编排工具把整条操作链路全都串联起来, 这样才是最稳妥的办法。
所谓的外部编排, 其核心在于把前面两种手段所产生的各个脚本当作是一些独立的零部件来进行使用。然后, 再通过一个统一的调度机制来对这些零件进行整体的调度和安排。
千万别一开始就想着搞什么全流程一站式搞定的事儿, 应当先选一个具体的环节来试着推进, 让这个环节能够顺利跑起来并且保持稳定状态之后, 然后再去连接后面的步骤, 这样的做法是在出现问题的时候, 用来进行问题定位和排查的范围可以变得非常小。
2.3 为什么用做“胶水”
强Ansys强这个软件里面不同模块自己带的脚本语言不是同一种回事, 有的更像是命令流的样子, 有的则更像对象的接口。它的优势在于它其实差不多能够跟所有的模块进行通话或者交流, 并且同时还可以很方便地去处理文件、表格、日志还有图表这些东西。
你可以把它理解成一种叫做“总调度”的东西, 意思是建模用的脚本、求解运行的脚本以及后处理的脚本各自分别负责其中的一段工作路段, 这些脚本负责按照规定的先后顺序去把每一个步骤调用出来, 传递参数给下一个环节, 接收产生的结果数据, 记录下整个过程的工作日志信息。
这种分层设计的好处在于, 假设某一特定环节出了差错的情况下, 并不会对其余环节的重复使用性造成负面影响, 因此, 当更换到一个新的项目环境当中时, 仅仅需要去改动其中一个片段, 就可以顺利地实现后续的继续使用操作。
3. 这里面的核心细节咱们来好好解释解释, 同时把实操当中该注意的要点点出来, 第三部分的第一小节讲的是参数化建模, 这个做法就是把“尺寸”给转换成“变量”。
做参数化工作的时候, 第一步绝对不可以直接去写代码, 而是要给模型里面那些关键性的尺寸找到并且起一个规范的名称。
我曾经见到过非常多的案例, 里面的模型使用的尺寸名称全部都是系统自动生成的默认名字, 像是D1、D2、D3这样的组合, 这种处理方式带来的问题就是, 哪怕只过了短短两天的时间, 你自己都可能忘记D1代表的是板厚还是其他具体的某个部位。
使用规范化的命名方法(例如使用有意义的描述性单词等有效方式)能够显著地提升脚本文件的可阅读和可理解程度, 同时也为后续将当前模型与外部的参数表格进行准确的对应匹配带来极大的便利。
建模脚本的核心结构通常是由以下步骤组成的, 分别是清空旧模型、创建几何、按照参数表格进行赋值以及更新模型。在这个过程中, 有一个非常容易被忽视的坑存在, 那就是在更新失败的时候不会报错。
当某些几何元素的参数超出了合理的范围时, 它可能就会发生静默式的失败, 这会导致模型仍然保持着旧有的状态, 而脚本却自以为已经修改成功了。
针对这个情况, 我的解决办法是在每一次执行更新操作之后添加一项校验工作, 具体做法是读取某一个关键的尺寸实际值, 并将其与期望值进行比对, 如果发现两者不一致的情况, 就直接抛出异常。这个校验的部分, 请你去多写三行代码, 这样能够把后面几小时的排查工作给省掉。
# 伪代码示意:参数赋值后的校验思路set_parameter("plate_thickness", 8.0)
update_model()
actual = get_parameter("plate_thickness")
if abs(actual - 8.0) > 1e-6:
raise RuntimeError(f"参数更新失败,期望8.0,实际{actual}")
3.2 批量求解:进程管理和资源分配是重头戏
在进行批量运行工况的时候, 最容易出现问题的部分通常不是求解计算这个环节本身, 而是关于进程和资源的管理问题。因为如果同时开启过多的求解实例, 就会把计算机的内存全部占用光, 而如果开启的数量太少, 又会白白浪费很多时间。
根据我个人的经验建议是, 应该首先去测试单个工况在运行时的峰值内存占用情况以及所消耗的时间长度, 然后再根据机器当前可用的资源总量来进行计算, 从而确定合适的并发数量, 并且一定要预留出百分之二十的余量空间。
举个例子来说假如单个工况的峰值内存占用了8GB, 而机器总共有的内存是64GB, 那么在这种情况下开启6个并发是比较稳妥的做法, 千万不要贪图多开而尝试开7个。
另一个细节是对于求解失败这种情况的判定, 因为当求解器返回完成这个状态的时候并不一定代表所得的结果是可信的, 所以我们需要查看收敛性以及检查有没有警告信息这些内容, 所以在脚本里面应该要去抓取那些出现在求解日志中的关键字, 这样就可以把那种带有警告但仍然收敛的情况和那种正常的收敛情况给区分开来, 并且对于那些带有警告的情况我们要把它们标记出来以便安排人员进行人工复核, 如果不执行这一步操作的话, 那么在批量运行结束之后就拿出一堆可疑的结果来, 这样返工的成本就会变得更高了。
3.3 结果提取:别让后处理成为新瓶颈
很多人把自动化做到仅仅只是求出解的那个阶段就停止了, 结果后处理这一步还是要靠手动去一张一张地看图像数据。其实从效果来看, 结果提取这部分才是能够带来最大收益的地方之一。
具体的思路是可以在求解完成之后, 让脚本来自动读取相应的结果文件, 然后按照预先设置好的探针位置或者是表达式去把这些数值给提取出来, 比如说提取最大应力、最大的变形量或者是特定节点的值, 最后把这些数据写到一张汇总表里面, 这样一来, 几十个工况所得到的结果就变得一目了然非常清晰了, 后续进行排序操作或者是筛选操作甚至是绘制趋势图都会变得相当方便起来。
这里需要特别留意单位的一致性这个事项, 考虑到不同模块或者不同结果类型在默认情况下其采用的标准单位可能是有所区别的, 因此在编写脚本的时候建议在代码中明确地指定所要使用的具体单位, 而不要盲目地去依赖那些默认设定的数值, 我曾经就因此上过当受过教训, 有一批数据结果里面混杂着两种不同的单位形式, 导致最终汇总成的表格所计算得出的变化趋势出现了完全相反的错位现象, 后来经过很长时间仔细排查之后才定位到根本原因其实是由于没有正确处理单位这一方面的问题所导致的。
3.4 日志与可追溯:自动化的“黑匣子”
我们脚本来跑批量任务的时候, 最怕的情况就是任务已经跑完了, 但是根本不知道是哪一步出了问题。我的做法是给每一个工况都单独搞一个日志文件, 然后在里面开始记录开始时间、参数的具体数值、各个阶段耗时一共是多少, 还包括那些警告信息和错误信息。
这个日志文件的名字, 我会把它加上工况编号和时间戳这样的内容, 这样一来如果出了什么问题, 就能非常快且准确地找到源头定位问题。这套东西写起来其实并不是很复杂, 但是它是非常关键的一个点, 它直接决定了自动化能不能够长期地稳定使用下去。
要知道一个没有日志支持的自动化系统, 在本质上等于把黑盒的操作换个位置而已。
4. 在这五个处于实际运作状态的场景里面, 关于它们被实施并且最终产生实际效果的整个过程, 其中第四点第一小部分所讲到的就是第一个场景的具体内容, 那么这个被称为参数化扫描的批量求解。
这是在现在人看来, 最典型而且也是最实用的场景设定。不妨假设说, 当前正在研究某个具体结构在不同板块厚度以及不同载荷情况下面所产生的整体响应结果。其中涉及的参数组合数量, 会达到几十种之多。
如果采用人工手动去计算的方法的话, 那就需要重复进行几十次的独立操作过程。但是如果使用编写好的脚本来执行此项任务的话, 其实也就是由两层循环结构外加一次求解程序的调用操作所构成的简化方案而已。
我的实操流程一般是这样组织的, 先把参数组合生成一张表, 这可以用简单的列表推导来实现, 也可以从表格文件里读取数据, 然后逐个调用建模脚本去修改参数, 接着调用求解脚本去跑计算, 再调用提取脚本收结果, 最后把所有东西汇总成一张总表, 关键点在于每个工况之间要进行彻底的状态清理, 以此避免上一个工况的残留影响下一个。
由清理工作不彻底所引发的“幽灵结果”问题, 是存在非常难度的排查工作的。因此, 我习惯使用的做法, 是在每一次运行计算工况的开始阶段之前, 去显式地对模型以及求解设置方面进行彻底的重新复位操作。
# 伪代码:参数扫描主循环骨架results = []
for thickness in thickness_list:
for load in load_list:
reset_model()
build_geometry(thickness)
apply_load(load)
status = solve()
if status == "ok":
results.append(extract_results(thickness, load))
else:
log_failure(thickness, load, status)
write_summary(results)
这个场景的收益是非常直观的, 也就是原来需要花费一两天时间才能完成的工作, 通过脚本来运行仅仅只需要一个晚上就能产出结果, 但是在这里需要特别注意的是, 一定要采取先小批量试跑的策略, 就是拿着两三个组合去验证一下脚本输出的结果是否与人工做的结果保持一致, 在这个前提得到满足之后再把范围放开来进行全量数据的处理, 假如是直接就使用全量数据来进行操作的话, 一旦发生脚本存在逻辑错误的意外情况,就会白白浪费掉整整一个夜晚的机器计算时间成本。
4.2 场景二:多工况结果自动汇总与对比
在完成了批量求解操作之后, 实际得到的那些结果文件分散在各个不同的位置, 想要依靠人工来进行整理工作, 这真是一件令人感到十分痛苦的事情, 针对这个特定的场景, 其核心要点在于必须要将结果数据进行有效结构化, 我所采取的具体做法是首先定义出一份统一的结果信息模板, 例如里面要明确列出包括“工况编号”在内以及诸如“参数1”、“参数2”这样具体的数据项, 还要有“最大应力”、“最大变形”以及“安全系数”等关键指标, 当每一个工况的相关数据提取完毕之后就直接填入到表格的对应行中, 最终把这些零散的数据统一输出成一个标准的表格文件, 正因为获得了这样一张清晰明了的数据汇总表, 所以接下来如果需要对这些数据进行对比分析、绘制相应的曲线图或者去寻找最优的参数组合, 这些任务都可以通过简单地编写几行代码轻松搞定。
这里有个实用技巧, 把参数值和结果放在同一张表里, 而不是分开存, 这样后续做敏感性分析时直接对表做分组统计就行, 不用再去关联两套数据, 另外汇总表里建议保留一列数据来源文件路径, 方便回溯到原始结果。
4.3 场景三:几何变体的自动重建与网格更新
有一些项目所涉及的内容, 并不是仅仅对尺寸进行修改, 而是把几何的构建形态给换掉, 例如说是选择不同的开孔方案, 或者是选择不同种类的加强筋排列布局方式。在这样的这类场景里面, 它所存在的困难之处, 主要是在于几何重建过程的稳定性方面。
基于我个人的工作经验, 我建议要把这个几何构件的搭建过程拆解成为由基础体加上特征所组成的一个模块化结构体系, 对于每一个特征设置一个单独的函数, 当需要更换方案时, 只需要去调用不同的特征组合就行了。
通过这种方法去操作的话, 其稳定程度会比每次都重新编写几何脚本要高得多, 而且在后续进行维护工作时会更加容易处理。
网格更新这回事, 简直就是另一个大坑。只要几何形状发生了变动, 原先那套网格里面的设置, 很有可能就不再管用了。因此, 在代码脚本里头, 非得包含一个叫做“网格质量检查”的这样的步骤不可。具体来说, 就是去核查那一堆单元到底有多少个数量, 还有那个指标最差的那个质量标准是多少。
万一这些数据超出了事先定好的那些界限数值, 那就赶紧发出警报。至于指望你的代码能够自动把所有关于网格的问题都修好, 那是根本别想做到的事情。能够在出了问题的时候发出警报, 然后叫人过来人工处理一下, 这样子的作用就已经非常之大非常之有价值了。
4.4 场景四:求解状态的自动监控与异常处理
对于时间很长的批量任务来说, 最怕的事情就是它在中途的地方给卡住了不动弹。对于这个场景的解决思路来讲, 可以给求解过程加上一个心跳检查的功能。具体的做法是脚本去定期检查看那个求解的进程还在不在那里, 还有日志文件是不是也在持续地变得更大一点。
如果说是超过了预定设好的时间的范围依旧没有取得任何进展的话, 那么就可以判定它是处于卡死的状态。一旦这么判定了就需要杀掉这个进程, 同时还要把这个现场的状况给记录下来, 接着就跳转到下一个要处理的工况上去。通过这一套机制能够让无人值守进行的批量任务变得更加可靠一些。
异常处理的原则是, 能够进行跳过的就执行跳过操作, 无法进行跳过的就必须做好记录工作, 个别工况出现失败的情况, 不应该导致整个批次流程因此被拖垮停下来, 但是在发生故障的时候必须把导致失败的具体原因给清清楚楚地记录下来, 通常情况下我都会把所发现的失败情况明确划分为“可以进行重试”这一类和“完全不适宜进行重试”这一类这两个大的种类范围, 对于那些归并为第一类的故障实例会自动启动一次重新执行的操作机制, 而对于那些归并为第二类的故障现象则会直接采用先记录在案随后直接绕过的方式进行后续业务流转, 等到当前整个批次的所有任务全部彻底结束完毕之后, 再进行统一的并由人工来介入查看相关的详情资料。
4.5 场景五:一键生成标准化报告
结果出来了之后, 报告往往还需要花很多时间来排版。现在这个场景, 是把汇总表、关键云图还有结论文字, 按照模板自动拼接起来变成一份报告。在导出云图的时候, 要注意分辨率和视角保持一致。
需要在脚本里把视角参数固定住, 这样才能保证不同工况的图能够直接进行对比。大家建议在制作报告模板的时候, 可以先从简单的文本或者表格格式开始用起, 千万别一开始就去做太复杂的排版动作, 只有先把“数据被自动填进去”这个事情给跑通, 才是正经事。
注意的事项是, 自动生成的那些报告, 一定要保留“人工复核”这一个环节。因为脚本可以保证数据填写到正确的位置里面去, 可是最终的结论到底是不是合理的, 还是需要人员亲自来看一看。
我个人的经历当中, 曾经见过自动生成的报告, 把那些异常值当作是正常结果这样给写进去了。所以, 这个复核的这个步骤, 是完全不能省掉的。
5. 在常见问题与排查技巧实录的第五点一章节里, 当你的脚本运行成功了但是得出来的结果不正确的时候, 大家先去检查这三个地方。
如果出现的结果不对劲, 我通常会依照特定的次序来展开查找和诊断的工作, 首先是去确认关键的设置项是否真的已经发生了作用, 其次是去看那些极端的或边缘的情况有没有被纳入到测试的范围里面去, 最后是看各个数值所采用的计量标准是不是保持了一致的状态, 像上面列出的这三个方面, 是导致各种麻烦的大部分原因所在, 比如当发现某个设定没有发挥作用的时候, 往往是因为缺少了用来检查和验证的更新步骤, 再比如, 当碰到边缘情况处理出错的问题时候, 经常是因为好几个人编写的程序代码都在使用同一个模型文件, 所以产生了相互干扰的情形, 至于单位不统一这个老问题嘛, 因为之前说过了, 所以现在就不再重复啰嗦了。
我们建议在脚本之中增加一个关于“关键设置回读”的步骤, 去把那个实际上已经生效的参数还有边界情况打印进入日志里面去, 等到出现问题的时候, 人们一眼就能够看出来。
5.2 批量任务中途失败,怎么快速定位
首先, 请先查看日志的时间戳, 定位到最后一次成功完成的工况, 那么问题大概率就出现在紧接着它后面的那个环节。接着,请把那个出问题的工况单独拿出来重新运行一次, 然后仔细看看出现的报错信息到底是什么样子。
万一这个报错信息写得非常含糊不清, 你就应该把这个具体工况所需要的所有输入参数以及生成的中间文件全部单独提取出来, 你自己再动手把整个处理流程老老实实手动再走一遍并进行对比分析。
根据我从前的实际经验, 批量环境下面所遇到的失败情况, 在绝大多数时候其实都能在单个工况的手动复现过程中被找到原因, 只要能成功把它复现出来, 后面的排查和查找问题就会变得相对容易许多了。
5.3 常见问题速查表现象 可能原因 排查动作
参数改了但模型没变
更新失败未校验
加回读校验,比对期望值
批量结果互相污染
工况间状态未清理
每工况前显式重置
求解卡死不退出
无超时监控
加心跳检测和超时杀进程
汇总表数值异常
单位不一致
显式指定单位并核对
云图无法对比
视角/分辨率不统一
固定导出参数
脚本换机器就报错
路径硬编码
用相对路径或配置项
5.4 几条踩坑换来的经验
第一, 千万不要把路径给硬编码了, 如果你在你的脚本里面把这个绝对的长路给写死了, 那么等到你把东西搬到另外一台机器上的时候, 整个程序就完全没法用了, 最好的办法呢, 是使用配置文件或者是使用相对的这路来指代, 这样做的话将来要是需要进行迁移的话所付出的成本就会低很多。
第二, 一定要先去用小小的这种样本去验证一下然后再去做那种全量的处理, 这个说法虽然在前面的时候就已经说过了, 但是现在再强调一遍也是完全没有关系的原因是这条建议真的可以帮助你们省时间而不是一点作用都没有还在那里浪费时间。
第三, 在记录日志的时候最好是多多益善绝对不能够少, 因为现在硬盘的这种存储空间其实是非常便宜的并不值钱反而是如果在后面你要去查找那个出了什么问题的地方却发现自己没有任何的日志可以查看这才是最要命的事情。第四点, 要求脚本具备重复执行的能力。
具体来说, 就是使用同一个脚本来运行两次, 这时候得到的结果应该是保持一致的。如果发现结果不一致了, 这说明在之前运行的时候, 有隐藏的残留状态没有被清理干净。
6. 我个人的几点体会
关于这套东西, 我断断续续用了好些年头了, 心里最大的感受就是那个意思。自动化的难点, 根本不在写代码这件事儿上, 而是在于得先把流程给想清楚。流程要想清楚了, 那代码仅仅就是一个翻译的过程而已。
要是流程没弄清楚, 那你代码写得再漂亮, 那也是白搭, 一点作用没有。还有另一个体会呢, 就是别总想着一步就能到位, 这个事儿急不得。你得先挑出最让人头疼的那个环节, 把它给自动化掉。等你是尝到了那个甜的滋味儿之后, 再去考虑扩展的事情, 这是更稳妥的做法。
我看过了许多团队, 他们在开头的时候, 就一心想要去弄一个全流程的平台, 结果搞了半年也没有把它搞定, 这反倒不如先拿出两天的时间来, 把批量求解的过程给理顺跑通, 这样的做法显得更加实在。
最后再分享一个小习惯, 就是每写完一段自动化脚本之后, 我都会故意去制造一个错误, 比方说传个超范围参数之类的, 然后仔细去看, 这个脚本能不能正确地报错, 能正常报错的脚本, 才是那种能够长期使用的脚本, 那种静默失败的脚本, 其危险性甚至比手动操作还要厉害, 因为这个习惯帮助我提前发现过好几次潜在的、可能出错的问题, 所以你也完全可以去试试看。