简介:面向拓扑优化与各向异性网格自适应研究场景的MATLAB/Octave开源工具包,重点解决二维/三维稳态导热路径设计、线弹性结构最小柔度两类经典问题,适合力学、热设计及结构优化方向的科研人员与工程师使用。压缩包共5个文件,包含3个M函数脚本、1个Markdown说明文档与1个RST格式技术说明,各算例均设有独立调用入口,整体仅173KB,轻量便携。脚本支持按设定参数复现WCSMO12会议论文中的典型算例,导热问题可控制体积约束、散热器位置与网格细化指标,弹性问题可调节载荷工况、惩罚因子与多约束配置,输出可直接用于结果分析与可视化。已有323人下载学习,对于希望快速上手各向异性网格自适应与拓扑优化算法的MATLAB用户,是一份既有理论出处又有可运行代码的实用参考。
1. Trullekrul这套MATLAB/Octave脚本:让拓扑优化不再被固定网格绑架
做拓扑优化的人大多卡在同一个尴尬上:设计域网格一粗,结果就糊出锯齿边界,密度云图像马赛克;网格加密倒是清晰了,但收敛慢、内存高,一次参数扫描要跑一整天。Trullekrul 是一套系统开源的 MATLAB/Octave 脚本,把各向异性网格自适应、拓扑优化和有限元求解串成同一条流水线。它的核心思路不是无差别加密,而是让单元沿结构边界拉伸:边界法向加密、切向放宽,用远少于各向同性网格的单元数保住同样的分辨率。适合被固定网格精度和计算成本两头挤压的拓扑优化研究者,也适合想搞懂各向异性网格自适应落地细节的新手,前提是你对 MATLAB 的基本语法和有限元稀疏求解不陌生。
2. 先拆底层逻辑:度量张量、误差估计和各向异性网格的三角关系
2.1 各向异性网格和“均匀加密”的本质差别
先明确一个容易被忽略的事实:传统自适应网格做的是各向同性加密,凡是误差大的区域都切成更小的等边三角形。这在有限元理论里站得住脚,但对拓扑优化这类问题很浪费。结构边界是一条接近一维的曲线,沿切向你根本不需要那么高的分辨率,需要细化的只有法向。真正高效的做法是把单元沿边界法向压扁、沿切向拉长,一个长宽比 20:1 的三角形就能顶二十个等边三角形,计算量省下一个量级。
各向异性网格就是这个“定向细化”的产物。两点之间的“距离”不再由欧氏度量决定,而是由节点处的一个对称正定矩阵——度量张量——重新标定。单元在该度量下的目标边长由特征值的倒数决定,拉伸方向就是特征向量方向,特征值差越大单元越扁。这个做法在计算流体力学边界层问题里已经用了很多年,丢进拓扑优化里是同一套逻辑,只是要拉长的对象从流体边界层变成了材料边界。
在一套工具里同时出现网格自适应和拓扑优化,看起来跨度大,实际是同一件事的两面:拓扑优化每一步都要在网格上求解位移场,位移场梯度大的地方基本就是结构边界所在;把网格往梯度方向自适应,等于把计算资源精确投向优化最关心的区域。这就是 Trullekrul 把三类脚本放在一起的根本原因,它们共享同一个度量张量模型。
2.2 度量张量怎么构造:梯度恢复、特征截断与平滑
度量张量的构造流程,常见做法分四步走。第一步在单元上算解梯度;第二步用一个面积加权投影把它恢复成连续的节点梯度,这一步比直接取单元平均值稳定得多;第三步对节点梯度再数值求一次导,得到节点处的 Hessian 近似;第四步对 Hessian 做特征分解,把特征值截断到目标尺寸对应的闭区间,并按设定的最大各向异性比上限做限制,最后对度量场做一次平滑处理,避免相邻单元的尺寸跳变太夸张。
参数上,hmin 和 hmax 直接决定网格的下上限尺寸;aniso_max 控制单元能有多扁,常见给 10 到 50;gradation 平滑系数管过渡半径,取 0.4 到 0.6 之间比较稳。这三个参数几乎主导了整套自适应效果:hmin 太小会把所有边界都细化到底,hmax 太大会让远离边界的地方粗得离谱,aniso_max 太小就退化成各向同性自适应,等于白折腾。
如果想亲眼看到度量张量长什么样,可以把它画成椭圆:特征向量是主轴方向,特征值的倒数是轴长,画出来之后拉伸方向一目了然。
% 绘制节点度量张量的椭圆,直观检查各向异性方向和强度 [V, D] = eig(M_node); % M_node: 2x2 对称正定矩阵 lambda = sort(diag(D)); % 特征值排序,防止主轴互换 theta = atan2(V(2,1), V(1,1)); % 主轴倾角 a = 1 / sqrt(lambda(2)); % 长半轴,对应拉伸方向 b = 1 / sqrt(lambda(1)); % 短半轴,对应加密方向 draw_ellipse(x_node, y_node, a, b, theta); % 自绘辅助函数逻辑说明:这段代码把「看不见」的度量张量转成可视化椭圆,a 和 b 分别代表单元在拉伸和加密两个方向上的目标尺寸。注意 eig 返回的特征向量列序和特征值顺序不对应,直接算轴长很容易把长短轴搞反,所以我先 sort 了 lambda,再单独取 theta。参数说明:M_node 是脚本内部算完度量后导出的节点数据,不同版本的导出变量名可能不同,建议在断点里用 whos 确认;draw_ellipse 是我补的绘图函数,只影响可视化,不参与主流程计算。
2.3 主循环:求解、估误差、重建网格,三步一个周期
Trullekrul 的主流程适合拆成三拍。第一拍,在当前网格上做一次有限元求解,拿到位移场和目标函数;第二拍,基于位移场或密度场做误差估计,构造节点度量张量;第三拍,用这个度量张量驱动网格重生成,再把解从旧网格插值到新网格,进入下一轮。整个流程循环到收敛,网格在这个过程中是“长”出来的,而不是一开始给定死的。
这一步最常见的误会是「自适应到底多久做一次」。常见做法不是每轮优化迭代都重建网格,那会让目标函数曲线抖成心电图,优化器的历史信息全部失效。一般是先在一套较粗的网格上预热若干轮,之后每隔 N 轮才重建一次网格,N 常见取 10 到 30,中间的轮次只做密度更新。这个错拍节奏在第四章会给出可直接跑的示例代码。
3. 把Trullekrul跑起来:路径配置、demo复现和输出解读
3.1 先解决两件小事:递归加路径和统一入口
下载解压后第一件事不是双击主脚本,而是先看目录结构。我一般会在终端里列一下:根目录下通常有 mesh 子目录负责网格生成与重建,fem 子目录负责单元刚度与求解,topopt 子目录负责灵敏度与优化更新,外加一堆 demo 开头的入口脚本。入口脚本才是真正该读的东西,它们把三块串起来并给出算例参数。
MATLAB 和 Octave 都能跑,核心代码是纯 m 文件,稀疏线性求解用的是内建反斜杠,不太挑版本。Octave 下如果碰到 mex 相关报错,多半是脚本里的加速钩子没编译成功,把配置里的 use_mex 开关关掉、退回纯 m 版本就能继续。另一个高频问题是文件名大小写:Linux 上 MATLAB 区分文件名大小写,Windows 上跑得好好的代码换到 Linux 就报函数未定义。解决办法是统一小写命名,或者用 which 函数逐个排查。
配好路径后,标准的入场动作是下面这段包装脚本:
% run_demo.m —— 我写的 Trullekrul 统一入口包装 addpath(genpath('./trullekrul_root')); % 递归加入全部子目录 cfg = trullekrul_defaults('beam'); % 加载两端支撑梁算例配置 cfg.hmin = 0.02; % 最小网格尺寸 cfg.hmax = 0.5; % 最大网格尺寸 cfg.aniso_max = 20; % 最大单元长宽比 cfg.volfrac = 0.4; % 材料体积约束 cfg.maxiter = 200; % 优化总迭代轮数 [mesh, dens, history] = trullekrul_run(cfg); % 运行主循环逻辑说明:trullekrul_defaults 负责把算例的默认参数读进来,trullekrul_run 负责跑主循环并返回网格、密度场和收敛历史。仓库原始入口名可能带 demo 前缀或直接叫 run_trullekrul,包装前先用 which trullekrul_run 确认实际名字,别照抄一半发现函数不存在。参数说明:hmin 与 hmax 控制目标尺寸上下限,aniso_max 控制单元最扁程度,volfrac 是材料用量约束,maxiter 是总轮数。第一次复现建议只动 hmin 和 maxiter,其余保持默认。
第一次跑时可以参考这张参数表,按设计域比例换算:
| 参数 | 作用 | 第一次复现建议 |
|---|---|---|
| hmin | 最小目标尺寸,决定边界细分上限 | 设计域宽度的 1/50 左右 |
| hmax | 最大目标尺寸,决定远离边界处粗化上限 | 设计域宽度的 1/2 左右 |
| aniso_max | 单元长宽比上限,越大单元越扁 | 10~20 |
| volfrac | 体积约束,材料占比 | 0.4 |
| maxiter | 优化总轮数 | 200 |
| adapt_interval | 相邻两次网格重建的间隔轮数 | 10~30 |
3.2 跑通第一个算例:从配置到三张检查图
复现第一步我建议先跑两端支撑梁算例,设计域取 2:1,中点施加垂直载荷,这是拓扑优化最经典的验算用例。跑完主循环后至少要看三张图——密度云图、网格图、收敛曲线——才能判断这轮复现是否成功。下面这段代码一次导出三张图:
% 一次性输出密度云图、自适应网格、收敛曲线 subplot(1,3,1); patch('Faces', mesh.elem, 'Vertices', mesh.node, ... 'FaceVertexCData', dens, 'FaceColor', 'interp'); axis equal; title('Density field'); subplot(1,3,2); triplot(mesh.elem, mesh.node(:,1), mesh.node(:,2)); axis equal; title('Adaptive mesh'); subplot(1,3,3); plot(history.iter, history.comp); title('Compliance history');逻辑说明:三张图对应“结果对不对、网格长什么样、收敛顺不顺”三个检查点。patch 用节点密度做颜色插值,直接看边界是否干净;triplot 画重建后的网格,边界区细长三角形一目了然;收敛曲线是判断优化稳定性的直接证据。参数说明:FaceVertexCData 的行数必须和顶点数一致,如果脚本返回的是单元中心密度,要先把 Mx1 的数据用重心平均插值回 Nx1,否则 patch 会报维度不匹配。
如果密度云图里灰色区域一大片,先别怀疑求解器,去查密度过滤半径和灵敏度过滤,这块在第五章展开。
3.3 输出数据怎么读:密度场维度与网格体检
主循环返回的三个数据各有各的坑。mesh 是标准节点-单元结构,node 是 Nx2 坐标,elem 是三角形连接。dens 的维度要特别注意:Trullekrul 习惯把密度放在网格节点上,得到 Nx1 向量,但部分子函数会临时把它放到单元中心变成 Mx1,混用会让后处理结果变得莫名其妙。拿不准时用 size(dens) 验证一次,再检查取值是否落在 [0,1] 区间。
网格重建后的体检同样关键。自适应干得好不好,不能只看目标函数,要看单元长宽比分布和总数变化。长宽比中位数能到 5 以上、最大值按设置接近 aniso_max,说明各向异性真的生效了;如果中位数还在 1.2 附近,基本是度量张量没构造对,或者 hmin 太保守,把单元全限制成近等边形态。
% 统计重建后的网格长宽比,验证各向异性是否生效 ar = compute_aspect_ratio(mesh.elem, mesh.node); % 自写辅助函数 fprintf('elements=%d median=%.2f max=%.2f\n', ... size(mesh.elem,1), median(ar), max(ar));逻辑说明:compute_aspect_ratio 对每个三角形算最长边与对应高度的比值,中位数代表普遍有多扁,最大值对照 aniso_max 看是否被硬截断。参数说明:这个辅助函数仓库未必自带,没有的话可以用网格质量函数替代,效果类似,只是输出做了 [0,1] 归一化,看分布更直观。
4. 把拓扑优化接进去:SIMP更新与网格自适应的错拍配合
4.1 固定网格把拓扑优化坑在哪
先回顾密度法拓扑优化的老问题:设计变量放在网格节点或单元上,材料边界只能在单元之间折线式移动,结果天然带锯齿。锯齿区域应力集中,优化器为了消掉局部峰值会放任边界糊成灰色,灰单元越积越多。棋盘格更是固定网格的经典病灶,相邻单元密度交替为 0 和 1,材料呈棋盘式分布,直觉上不构成结构,目标函数上却经常是局部更优。各向同性加密能缓解,但加密四倍换来的清晰度,计算量涨得比分辨率还快。
自适应网格解决这个问题的方式很直接:让材料边界附近的单元法向变小、切向拉长,边界变成一条干净的折线而不是一排骨牌。更关键的是,同样的目标分辨率下,自适应网格的单元总数往往只有各向同性网格的十分之一到五分之一。我复现时最直接的感受是,同一台机器上固定网格跑到三百轮开始卡顿,自适应配置跑到五百轮还很从容。
4.2 推荐的错拍节奏:预热轮、重建间隔与冷静期
自适应网格不是越频繁越好。优化迭代的前几十轮,密度场还在大范围重排,这时重建网格只会白费计算并破坏历史信息。我一般这样定节奏:前 30 轮完全不碰网格,只做 SIMP 更新;之后每 15 轮重建一次,重建完再多跑 5 轮“冷静期”,冷静期迭代不计入收敛判断,让密度场在新网格上重新站稳。代码骨架如下:
for iter = 1:cfg.maxiter [dens, comp] = simp_update(mesh, dens, cfg); % SIMP 更新 if iter > cfg.warmup && ... mod(iter - cfg.warmup, cfg.adapt_interval) == 0 mesh_new = adapt_mesh(mesh, dens, cfg); % 重建网格 dens = project_density(mesh, mesh_new, dens); mesh = mesh_new; end history.comp(iter) = comp; end逻辑说明:simp_update 每轮都执行,做有限元求解、灵敏度计算和密度更新;adapt_mesh 只在满足周期条件时触发;project_density 负责把旧网格节点密度插值到新网格。我把三个函数都做了薄封装,方便在关键位置加日志验证网格规模变化。参数说明:warmup 建议取 20 到 30,adapt_interval 取 10 到 30,两者都太小会造成网格抖动。project_density 内部做线性插值后必须把密度裁到 [0,1],否则下一轮灵敏度里会出现非法负密度。
注意:SIMP 更新里的密度过滤和灵敏度过滤不是一回事。跨网格投影后,旧网格的灵敏度如果不同步插值,下一轮更新方向就是错的。
4.3 密度场跨网格投影:最容易翻车的环节
网格重建后,旧网格的节点密度不能直接丢弃,必须插值到新网格节点上。这个投影看似简单,实际暗藏三个坑。
第一,新网格边界和旧网格不完全重合,投影后体积分数会漂移,需要在插值后重新归一化回到 volfrac。第二,密度是节点值,线性插值是自然选择,但插值会抹掉边界附近 0/1 的锐利程度,导致下一轮优化把刚拉直的边界又糊回去。第三,投影后灵敏度场必须同步投影,只搬密度不搬灵敏度,优化方向就变成了一个令人抓狂的黑匣子。
针对第二点,常见补救是投影后再做一次密度锐化,用双曲正切类函数把接近 0 和 1 的值向两端推,压缩中间灰色区。这个锐化强度是个旋钮:推得太猛容易在边界产生振铃,推得太轻等于没做。我一般先不锐化跑一轮,量一下边界灰色带宽度再决定要不要加。第五章的收敛曲线回弹问题,根源基本都在这个投影环节。
5. 避坑指南:五个绕不开的坑与排查路径
5.1 环境与路径相关的两个坑
坑一:addpath 之后仍然报函数未定义。现象:运行 run_demo.m 第一行就抛 Undefined function,用 which 查不到。原因:只 addpath 了根目录,子目录 mesh、fem、topopt 都没进搜索路径;另一个隐形原因是 Linux 下文件名大小写敏感,脚本里引用的是 MeshAdapt,实际文件名是 meshadapt。解决:把 addpath 改成 addpath(genpath('根目录')),再用 which -all 确认实际函数名,顺手用 dir 检查子目录文件命名是否统一。
坑二:Octave 下运行到一半崩在加速钩子上。现象:报错包含 mex not found 或 shared library failed to load,同一份代码在 MATLAB 下正常。原因:部分辅助函数带 C 写的加速钩子,Octave 的 mex 兼容层要么没编译要么库不匹配。解决:在配置结构体里找 use_mex 或 enable_fast 开关,置为 false 强制走纯 m 路径;想保留加速就用 mkoctfile 重新编译源码,编译前确认编译器版本对得上。
5.2 优化迭代阶段的三个坑
坑三:结果出现大片棋盘格。现象:密度云图像国际象棋棋盘,0/1 交替铺满设计域。原因:密度过滤半径 rmin 过小或压根没开过滤,灵敏度没有空间耦合,相邻单元各干各的。解决:把 rmin 调到至少覆盖两到三圈单元,比如设计域宽度的 1/20,或者改用灵敏度过滤并在每轮后做密度平滑。改完后棋盘格变成连续带,边界略模糊一点,这是正常折中。
坑四:单元总数越涨越多,中途内存告急。现象:日志里的 elements 列单调上涨,某轮开始内存翻倍,求解时间陡增。原因:hmin 太小加上度量平滑不足,局部误差信号把整片区域都拉到了最细档。解决:检查三处——hmax 是否大到能让远离边界的区域真正粗化,gradation 平滑系数是否在 0.4 以上,目标单元总数有没有设上限。我给配置加过 nTarget 字段,超出上限时把度量整体放大,等价于所有目标尺寸放大,网格规模立刻被按住。
坑五:自适应重建后收敛曲线回弹。现象:每次网格重建后 compliance 突然跳高 5% 到 10%,然后缓慢回落,收敛历史出现周期性锯齿,严重时最终拓扑都和固定网格跑出的不一样。原因:密度投影改变了体积分数,插值又抹平了边界锐度,目标函数在新网格上重新评估自然偏大;如果回弹幅度一次比一次大,说明投影在累积误差。解决:投影后做体积校正,把整体密度乘一个系数让体积分数回到约束值;冷静期设成 adapt_interval 的三分之一,回弹期间的迭代不参与收敛判断;回弹仍大于 10% 就回 4.2 节检查 warmup。有趣的是,同样的配置 warmup=30 时回弹约 3%,warmup=10 时回弹到 12%,优化历史在网格剧烈变化时并不可靠,这不是玄学,而是插值误差的必然结果。
6. 进阶验证:用网格无关性测试戳破自适应的玄学错觉
有人会把自适应网格优化结果的不稳定归咎于玄学,其实大多数情况是缺了验证环节。网格无关性测试是最值得做的一项:固定所有优化参数,只把目标单元总数分别设为 2000、4000、8000 三档,各跑一遍完整流程,对比最终密度拓扑、边界形态和 compliance 值。三档结果拓扑一致、compliance 相差在 5% 以内,说明结果对网格尺度不敏感,可以放心去做参数扫描;如果拓扑明显不同,问题多半出在过滤半径和度量平滑上,网格只是一面照妖镜。
% 三档网格规模下的网格无关性快速验证 for nTarget = [2000, 4000, 8000] cfg.nTarget = nTarget; [~, ~, hist] = trullekrul_run(cfg); fprintf('n=%-5d comp=%.4f\n', nTarget, hist.comp(end)); end逻辑说明:循环复用统一入口,只改 nTarget,其他参数完全一致,差异只能来自网格尺度,这是对照实验的基本前提。参数说明:三档测试时不要同时动 hmin,否则分不清是哪个参数导致差异;打印用 %-5d 对齐,方便直接贴进表格对比。
验证完网格无关性,收敛判据也要升级。我建议不要看单轮次的 compliance 变化,而是看相邻两次网格重建之后冷静期结束时的 compliance 对比,两者相差小于 1% 才算真收敛。理由很简单:单轮变化在自适应网格上噪声太大,一次重建就能制造 3% 的波动,拿它当停止条件只会让程序提前退出或永远跑不完。
最后补一个习惯。从那以后我每次复现这类带网格自适应的优化代码,都强制先走一遍三档网格规模的无关性扫描,再谈调参;扫描通过之后,去动 hmin、aniso_max、gradation 这些旋钮才有意义。这套 Trullekrul 脚本下载下来之后,建议你也按这个顺序来——先跑通 demo,再做无关性验证,最后才碰参数。希望帮到你。
本文还有配套的精品资源,点击获取