简介:这是一套基于蒙特卡罗方法实现固态相变晶粒长大过程模拟的Matlab程序包,面向材料科学、金属再结晶及微观组织演化的研究者与学习者。代码采用Q-state Potts模型在3D方形晶格上运行,支持自由设定三维网格尺寸与蒙特卡罗步数等关键参数,便于针对不同初始组织状态开展模拟实验,直观观察晶界迁移与晶粒粗化行为。压缩包共30个文件,以23个m源文件为主,涵盖主程序、晶粒状态初始化、能量计算、边界处理、微观结构绘图等完整功能模块;另附6张JPG过程效果图和1个说明文档,可快速对照检查模拟结果。包体大小约1.63MB,轻量易用。目前已有450人浏览学习,适合需要开展相变模拟或再结晶过程仿真的Matlab用户参考借鉴。
1. 蒙特卡罗方法模拟固态相变晶粒长大的直接起点
用蒙特卡罗方法模拟固态相变中的晶粒长大,最直接的做法是把微观组织离散成三维网格,每个格子保存一个取向态,然后用随机翻转驱动界面迁移。这套基于Q-state Potts模型的MATLAB代码正是这样实现的:用户在Procure_Input_Values_3D_QPOTTS.m里填入3D网格尺寸、蒙特卡罗步数、取向数Q等参数,运行MAIN.m就能看到晶粒在三维方形格子上逐渐吞并、长大的全过程。它解决的典型问题是金属再结晶过程中的晶粒长大模拟,以及为后续加形核项、储能项提供基础框架。对刚开始接触微观组织演化的工程师,可以把它当做一个可以复现的起点;对需要二次开发的人,代码里把能量计算、边界处理、可视化分成了独立模块,很容易针对自己的模型修改。
2. 3D Potts模型的哈密顿量与Metropolis状态更新规则
2.1 从取向态集合到晶界能
Potts模型把每个格点看作一个晶粒单元,赋予一个离散取向态q,q的取值范围是1到Q。相邻两个格点的取向态不同,就认为它们之间存在晶界,贡献一段界面能;取向态相同则不贡献能量。对三维方形格子,哈密顿量写成:
E = -J Σ_{<i,j>} δ(q_i, q_j)
其中J是晶界能系数,δ是克罗内克函数。这个式子意味着系统倾向于让取向相同的邻居尽量多,也就是让大晶粒吞掉小晶粒来降低总晶界面积。金属再结晶模拟里,J通常等于单位晶界能的折算系数,大小决定了曲率驱动力的量级。实际代码中不会直接对整个三维数组求和,因为每次翻转格点只需要局部能量变化,所以才有了Energy1、Energy2和EnergyChange这三个文件的分工。
2.2 Energy1、Energy2与EnergyChange:索引矩阵下的局部能
从文件名看,Energy1_3D_QPOTTS.m负责计算某格点的当前局部能,Energy2_3D_QPOTTS.m负责计算翻转试演后的局部能,EnergyChange_3D_QPOTTS.m再求差。这是一种很稳的写法:如果直接改状态再回滚,容易出错;先计算两个状态的能量,再决定是否接受,可以保持状态矩阵在每次试演前后不变。
一个典型的逐邻居累加实现如下:
function E = Energy1_3D_QPOTTS(state, x, y, z, J, Lx, Ly, Lz) % 计算格点(x,y,z)的局部晶界能 % state: 三维取向矩阵,取值1..Q % 六个最近邻的偏移量 off = [-1 0 0; 1 0 0; 0 -1 0; 0 1 0; 0 0 -1; 0 0 1]; E = 0; for k = 1:6 nx = mod(x-1+off(k,1), Lx) + 1; ny = mod(y-1+off(k,2), Ly) + 1; nz = mod(z-1+off(k,3), Lz) + 1; if state(nx,ny,nz) ~= state(x,y,z) E = E + J; % 取向不同,贡献一个J end end end参数说明:state是三维网格取向矩阵;x、y、z是目标格点坐标;Lx、Ly、Lz三个维度边长。此逻辑把“取向相同”时的贡献记为零,取向不同时加一个J,所以边界能越高越不稳定。实际代码里为了速度,会预先用EdgeBoundaryWrap把邻居坐标算好,循环内不再做mod,因为对于大尺寸网格,mod的代价不可忽略。EnergyChange则是比较新旧两个局部能:
ΔE = E_new - E_old
注意翻转前后只是中心格点的取向变了,邻居的状态不变,所以这个差别的计算成本是固定的6次比较,跟网格总尺寸无关。
2.3 Metropolis判据与状态翻转的可接受概率
得到ΔE后,用Metropolis规则决定是否接受。这个规则从统计力学出发,保证系统趋向平衡态。对晶粒长大来说,它允许界面有一定热扰动,避免总是停在亚稳态。
if dE <= 0 state(x,y,z) = newSpin; % 能量下降,必然接受 else if rand < exp(-dE / kT) state(x,y,z) = newSpin; end end参数说明:dE是局部能量差;kT是玻尔兹曼常数乘温度,单位必须与J一致。rand产生0到1之间的均匀随机数,用于决定是否接受一个能量升高的翻转。温度越高,exp(-ΔE/kT)越大,界面越粗糙,晶粒形状越不规则;温度很低时,系统接近零温动力学,偶尔的高能翻转也能帮助系统跳出局部极小,但界面会趋向平直。典型取值见表:
| 参数 | 含义 | 典型范围(J=1时) |
|---|---|---|
| Q | 取向态总数 | 30~100 |
| kT | 温度能量比 | 0.5~1.0 |
| MCS | 蒙特卡罗步总数 | 500~5000 |
| 网格尺寸 | LxLyLz | 50^3以上 |
需要说明的是,MCS被定义为一个平均等待时间:在每个MCS中,系统尝试足够多次翻转,使每个格点平均被抽到一次。因此网格越大,单个MCS内部循环次数越多。
3. MAIN.m主流程与蒙特卡罗步的实现细节
3.1 输入参数与初始化矩阵
MAIN.m是整个模拟的入口,不建议把参数散落在各个函数里。原始代码用Procure_Input_Values_3D_QPOTTS.m统一收集输入,用InitialThings和InitializeMatrices完成前处理。这样做的理由很实际:当你要跑100组参数做对比时,只需要改一个文件,而不用在各个函数里翻找。
初始化部分通常包括四个步骤:读入边长Lx、Ly、Lz,写取向数Q,写蒙特卡罗步数totalMCS和温度kT;调用InitializeMatrices分配三维矩阵;调用AssignRandomInitialStateMatrix为每个格点赋一个1到Q之间的随机整数,作为初始随机取向;再用PlotInitialStateMatrix输出第0步的显微组织,方便和后面的结果对比。
% MAIN.m 3D Q-state Potts模型晶粒长大主循环 clear; close all; Procure_Input_Values_3D_QPOTTS; % 读入Lx Ly Lz Q kT totalMCS InitialThings_3D_QPOTTS; % 初始化随机种子、时间轴等 state = InitializeMatrices_3D_QPOTTS(Lx, Ly, Lz); state = AssignRandomInitialStateMatrix_3D_QPOTTS(state, Q);这里每个函数名都和压缩包内真实文件名对应,方便读者对照代码。InitializeMatrices返回的state是一个Lx乘Ly乘Lz的uint16数组,用uint16而不是double的原因是两个方向共用同一个取向态时,内存可以降到原来的1/4。对100×100×100的网格,double类型需要8MB,uint16只要2MB,而且访问cache更友好。
值得注意的是,AssignRandomInitialStateMatrix的随机性受rand影响。如果希望结果可复现,最好在InitialThings里固定rng,但在做统计平均时又需要不同随机种子,所以建议把随机种子作为输入参数之一。
3.2 MCS循环里的两层试演结构
主循环的核心是同时做两件事:按MCS推进时间,按空间遍历或者随机抽点推进微观结构更新。很多初学者会把蒙特卡罗步和格点循环弄混:一个MCS不是一次翻转,而是整个系统平均覆盖一次的集合。所以代码结构通常是:
for mcs = 1:totalMCS for i = 1:Lx*Ly*Lz [x,y,z] = PickRandomLatticeSite_3D_QPOTTS(Lx, Ly, Lz); newSpin = KDel_3D_QPOTTS(state, Q); % 从Q个取向里挑一个新取向 dE = EnergyChange_3D_QPOTTS(state, x, y, z, newSpin); if dE <= 0 || rand < exp(-dE/kT) state(x,y,z) = newSpin; end end DisplayCurrentMcs_3D_QPOTTS(mcs, totalMCS); if mod(mcs, 20) == 0 PlotMicrostructure_3D_QPOTTS(state, mcs); end end参数说明:PickRandomLatticeSite返回的是随机选取的格点坐标,内循环次数等于总格点数,所以总尝试次数是totalMCS乘以格点总数。KDel负责从1到Q中等概率抽取新取向,注意它不应该返回当前取向,否则一次试演完全无效,但即便如此偶尔抽到相同取向也不会破坏模拟的马尔可夫性质,只是浪费计算;经验上Q越大这种浪费越少,Q=30时大概每次试演有3%概率抽到原取向,可接受。
第二个值得注意的点是,内循环里每次都调用rand会产生大量随机数。对500步MCS、网格80×80×80,大约是2.56亿次随机数生成。这个包没有做特别优化,但对于学习目的已经足够。如果你打算做更长时间模拟,可以提前用randstream一次性生成一个巨大的随机数组,或用矢量化方式一次性处理一批格点,不过后者会改变Metropolis的顺序,需要验证结果是否一致。
3.3 输出控制与中间结果保存
压缩包里的100.jpg、80.jpg、60.jpg等文件就是这种mod输出机制生成的不同MCS步数的显微组织快照。保存时用有序数字命名,比用时间戳更好,因为后面的mcs_year_achievements.m之类文件可以直接对号入座地做长大曲线统计。DisplayCurrentMcs只输出进度,不落盘;真正落盘的是PlotMicrostructure。
一套让我比较舒服的输出策略是每个MCS都只更新内存中的“平均晶粒半径”变量,每20个MCS写一次完整快照,最后用ExtractQ_XYZQ_3D_POTTS导出所有格点的坐标与取向,给后续统计使用。这样不会因为硬盘IO拖慢主循环,也不会丢失中间结构。
4. 3D方形格子的邻居索引与周期性边界
4.1 用EdgeBoundaryWrap避免重复mod
三维方形格子的边界处理是模拟中最容易出错的地方。最简单的方法是mod取模,如第2章代码所示,但它在内层循环里对每个邻居都做一次mod,会让CPU在整数除法上用掉不少时间。原始资源里专门有EdgeBoundaryWrap_3D_QPOTTS.m,说明作者倾向于提前把边界索引处理好。
常见的做法是把坐标偏移和取模运算组合成一张查找表:对x方向,预先算好一张长度为Lx+2的表wrapX,其中wrapX(1)=Lx,wrapX(Lx+1)=1,这样任何x-1、x+1偏移都能直接查表,而不是做mod。类似地生成wrapY、wrapZ。这样在能量计算里,六个邻居坐标可以写成:
function E = EnergyFast(state, x, y, z, wrapX, wrapY, wrapZ) Lx = numel(wrapX) - 2; Ly = numel(wrapY) - 2; Lz = numel(wrapZ) - 2; % 六个邻居的坐标全部通过wrap表映射 nx1 = wrapX(x-1); nx2 = wrapX(x+1); ny1 = wrapY(y-1); ny2 = wrapY(y+1); nz1 = wrapZ(z-1); nz2 = wrapZ(z+1); E = ... (state(nx1,y,z) ~= state(x,y,z)) + ... (state(nx2,y,z) ~= state(x,y,z)) + ... (state(x,ny1,z) ~= state(x,y,z)) + ... (state(x,ny2,z) ~= state(x,y,z)) + ... (state(x,y,nz1) ~= state(x,y,z)) + ... (state(x,y,nz2) ~= state(x,y,z)); E = E * J; end这里没有mod,只有索引引用,性能提升在三维网格上相当明显。wrapX的生成方式也很直接:
wrapX = zeros(1, Lx+2); wrapX(1) = Lx; wrapX(2:Lx+1) = 1:Lx; wrapX(Lx+2) = 1;参数说明:wrapX长度为Lx+2,把x-1映射到Lx,把x+1映射到1。这种实现简单且不容易出错,缺点是每个维度都要维护一张表;对更大规模的模拟,也可以把三维邻居预计算成六个独立的索引数组,但那需要更多内存,对显式调用更加方便。
4.2 PickRandomLatticeSite与随机访问的缓存问题
随机选点函数如果用randi([1, totalSites]),然后通过ind2sub转换成三维坐标,没问题,但要小心ind2sub的开销。更快的做法是直接在1D索引上操作,让Fortran风格的访问模式减少:
function [x,y,z] = PickRandomLatticeSite_3D_QPOTTS(Lx, Ly, Lz) linIdx = randi(Lx*Ly*Lz); z = ceil(linIdx / (Lx*Ly)); y = ceil(mod(linIdx - 1, Lx*Ly) / Lx) + 1; x = mod(linIdx - 1, Lx) + 1; end实际上在MATLAB中直接让state保持三维矩阵,用x,y,z索引即可。真正影响性能的是访问邻居时,x方向邻居在内存中是连续的,y方向会有跨度,z方向跨度更大。因此如果格子很大,建议将坐标循环顺序写成x最内层,并让rng产生随机坐标时尽量一次生成一组向量,再做批量更新。但批量更新会破坏严格的Metropolis顺序,只推荐在验证后使用。
说明参数:Lx、Ly、Lz分别是三个维度的尺寸;randi返回均匀随机整数;ceil和mod是取整操作。这个文件的作用是让内层循环每次得到合法坐标,不需要额外判断边界。
4.3 网格尺寸、Q值和内存占用
这里给一个经验表,让你在开始模拟前对内存有数:
| 网格尺寸 | uint16状态矩阵 | 6个邻居索引表(int32) | 每MCS尝试次数 |
|---|---|---|---|
| 50^3 | 0.25 MB | 约 3 MB | 125000 |
| 100^3 | 2 MB | 约 24 MB | 1e6 |
| 200^3 | 16 MB | 约 192 MB | 8e6 |
从表可以看出,200^3网格仅邻居索引表就会到192MB,所以原始代码选择每步即时计算mod也有它的道理:省内存,耗CPU。理解这个权衡后,你再决定要不要改成查找表。
一些用户在超大网格上直接使用查找表导致内存交换,模拟速度反而变慢。解决办法是只对两个维度做查找表,第三个维度仍用mod,让内存占用减半。这种折中在三维相场模拟里很常见,Potts模型同样适用。
5. 微观结构可视化与晶粒尺寸的量化输出
5.1 用PlotMicrostructure与PlotBoxEdges绘制三维晶粒
压缩包里的100.jpg、80.jpg等图片,我推测是每20或40步调用PlotMicrostructure_3D_QPOTTS后保存的。这个函数大概率用isosurface或者patch把相同取向的格点区域显示出来。要画出三维体积感,一般做法是:
function PlotMicrostructure_3D_QPOTTS(state, mcs) % 将三维取向矩阵转成label矩阵,每个取向一种颜色 p = patch(isosurface(state, 0.5)); isonormals(state, p); set(p, 'FaceColor', 'flat', 'EdgeColor', 'none'); camlight; lighting gouraud; title(sprintf('MCS = %d', mcs)); end但Potts模型的state是离散整数,直接画等值面会有问题。更稳妥的做法是先把每个取向用一个平滑的随机RGB颜色编码,然后使用volshow或者slice显示。PlotBoxEdges则是绘制三维背景框,好让读者感受到晶粒在立方体中的位置。两者配合起来才能看清楚晶界在三维空间如何运动。
实际使用中,我最常看的是MCS=1、20、60、100这几个时间点的图。如果从初始随机取向出发,MCS=1时还是大量细碎晶粒,MCS=60时已经开始出现明显的晶粒竞争,MCS=100时等轴晶的几何特征基本成型。如果是用来做金属再结晶演示,这个时间序列已经足够说明问题。
5.2 用ExtractQ_XYZQ_3D_POTTS导出坐标与取向数据
这个文件的作用是导出每个格点的坐标和取向,格式每行可能是x,y,z,q。得到这个数据之后,你就不再依赖图形,而是用数值统计判断晶粒是否在正常长大:
data = dlmread('output_xyzq.txt'); q = data(:,4); Lx = max(data(:,1)); Ly = max(data(:,2)); Lz = max(data(:,3)); state = reshape(q, [Lx Ly Lz]); % 连通域分析:把7邻接或26邻接看成同一晶粒 % 注意周期性边界,需要先把state扩展成3x3x3再标号说明该代码的要点:先是读取导出文件,reshape成三维矩阵;然后为了正确处理周期性边界,将状态在三个方向都各复制一次,再用bwlabeln做26邻接连通域标记,最后只取中间区域的结果,删除扩展部分的重复统计。这里最大的坑是周期性边界:如果两个晶粒在左边界和右边界处实际连通,而你没有扩展矩阵,它们会被统计成两个晶粒。
对晶粒尺寸分布,可以从连通域体积得到等效半径R=(3V/4π)^(1/3),然后画log-log曲线看是否满足正常长大。这是验证程序正确性最直接的手段。
5.3 导出数据与图形输出之间的时间戳对应
压缩包里的文件名如100.jpg、80.jpg、60.jpg,是MCS步数,这样在写mcs_year_achievements.m这类后处理脚本时就能直接按文件名索引,避免再读一遍参数字段。如果你自己改文件名格式,建议保留数字前缀,否则后期自动化处理会非常难受。
一种更工程化的做法是除了图形,再保存一个mat文件,里面包含mcs、平均晶粒半径、每个格点的取向,这样即使没有原始状态矩阵,也能后悔后重画。Save时候用v7.3格式,避免超过2GB保存失败。
6. 参数调优与常见坑:MCS步数、Q值和温度怎么选
6.1 先检查长大指数
常见的失败模式是:运行完,晶粒尺寸看着在长大,但平均半径随MCS的曲线不光滑,甚至停滞。第一个要检查的是Q值是否太小。Q=2或Q=4时,不同晶粒有很高概率在演化的某个时刻被赋成同一个取向,而拓扑上它们又相隔很远的晶界。所以当两个本来就分离的晶粒因为取向相同而“合并”时,系统的晶界能出现假性下降,晶粒尺寸统计完全失真。我一般取Q=30以上,如果要研究准三维薄膜,Q=50~100会更稳。
第二个坑是温度设定。kT接近于零时,程序几乎只接受能量降低的翻转,晶界会沿着能量梯度移动,这时候晶粒长大过程会非常慢,而且界面会倾向于变成低指数面,形成非真实的多边形状。kT过高时,exp(-ΔE/kT)的接受率接近1,系统变成随机噪声,晶界变厚、微观组织看上去像“熔融”。从经验看,J=1、kT=0.6~1.0是常见区间。
6.2 用MCS步数验证正常长大
很多初学者只跑几十个MCS,看到晶粒刚开始长大就认为算法有问题。正确做法是先观测平均晶粒半径随时间的幂律关系。正常长大理论给出d^m - d0^m = K * (MCS),m≈2。你可以在每个MCS间隔记录一次平均半径,然后拟合指数:
mcsVec = [20 40 80 160 320 640]; R = [5.2 7.1 9.8 13.5 18.4 25.9]; p = polyfit(log(mcsVec), log(R), 1); n = p(1); % 对应d ~ t^n中的n,正常长大接近0.5参数说明:这里用log-log线性拟合来估计长大指数,n接近0.5说明模拟进入了正常长大阶段,n远小于0.5可能说明晶粒被钉扎或者Q值不够,n偏大则要检查是否出现了异常晶粒长大。实际使用中不要把R固定为最大晶粒半径,而应该用体积加权平均半径。
还有一个很实际的问题:随机种子。如果你不固定rng,同一组参数两次运行的结果差别可能很大,尤其是在小网格上。做参数扫描时,建议对每个参数取5个不同的随机种子,把平均半径取平均再拟合,这样长大指数会更稳定。在代码里可以这样写:
for seed = 1:5 rng(seed); % 完整跑一次模拟 R_all(seed, :) = computeR(mcsVec); end R_mean = mean(R_all);我一般会在KDel里加一个判断,如果新取向等于当前取向就重新抽样,避免一次无效翻转。这会让计算量略微增加,但长远看更值得。程序运行中如果发现MATLAB卡死,首先要看是不是内存溢出,把网格尺寸降一半试试;其次看是不是输出图像频率太高,把mod条件从10改成100,通常都能继续跑。
本文还有配套的精品资源,点击获取