蒙特卡罗模拟晶粒长大:Potts模型原理与Python实现
2026/9/15 3:51:31 网站建设 项目流程

简介:采用蒙特卡罗方法(Q-state Potts模型)模拟固态相变过程中晶粒长大的MATLAB程序,面向材料科学、冶金工程等领域的研究人员与学生,可用于金属再结晶、晶粒生长演化的仿真与教学演示。程序支持用户自定义三维网格尺寸、蒙特卡罗步数等关键参数,并整合了从初始组织赋值、能量计算、边界处理到微观组织绘制的完整流程。压缩包共30个文件,其中23个m脚本覆盖主程序及各项功能模块,6张jpg图展示不同时间步的晶粒形貌变化,txt文件提供使用说明,整体仅1.63MB,轻量且便于部署。目前已有450人学习浏览,适合需要快速上手三维晶粒长大模拟或在此基础上进行二次开发的读者。该代码结构清晰、模块划分明确,可直接运行或修改参数以适配不同相变体系,配合可视化结果可更直观地理解蒙特卡罗方法在材料模拟中的应用机理。

1. 从一块多晶看蒙特卡罗方法能解决什么

做材料模拟的人第一次接触晶粒长大,往往先被「蒙特卡罗」这个名头绕晕:它到底是随机抽样还是物理模型?实际上,在固态相变和再结晶模拟里,蒙特卡罗方法通常指用随机取向的离散格点逼近多晶组织,再通过能量最小化让晶界逐步迁移。你不需要求解曲率驱动方程,只需要定义好界面能,然后用 Metropolis 接受准则决定每个原子位置是否翻转取向,就能看到晶粒尺寸分布从一堆碎块慢慢变成六边形拼图。

这套方法很适合做两件事:一是快速观察晶粒长大的拓扑演化过程,看异常晶粒怎么吃掉邻居;二是作为相场模型的低成本替代,在 2D 和 3D 晶格上跑参数敏感性分析。它的代价是时间步没有直接物理单位,需要通过生长指数和实验数据标定。对做相变动力学、焊接热影响区组织预测甚至电池电极烧结研究的人来说,这是最容易上手的组织演化工具之一。

2. Potts 模型下的晶粒长大能量与蒙特卡罗步

2.1 离散化晶格与取向变量

蒙特卡罗模拟晶粒长大的标准做法是把连续组织离散成规则网格,每个格点对应一个晶粒取向。常用的是 Q-State Potts 模型:每个格点 i 被赋予一个取向取整数值 s_i ∈ [1, Q],在同一晶粒内部的格点取向相同,取向不同的相邻格点之间形成晶界。

三维时用立方晶格,二维时用正方形或六边形晶格。正方形晶格实现最简单,但各向异性较强;六边形晶格更接近真实组织,编程要处理偏移坐标。我刚上手时建议先用二维正方形晶格,把矩形区域的长度和宽度设为 Lx、Ly,初始化时给每个格点随机赋一个 1 到 Q 的整数。Q 一般取 24 或 32,太小时模拟后期会出现两个相邻晶粒因为取向碰撞而合并的假象,太大则初始化熵太高,晶粒细碎,消耗更多 MCS 才能进入稳态长大。

2.2 哈密顿量与界面能

Potts 模型的哈密顿量只统计近邻格点的取向是否一致。对格点 i,能量贡献为:

E_i = -J * sum( delta(s_i, s_j) - 1, j in neighbors )

J 是近邻耦合常数,通常设为单位 1;delta 是克罗内克函数,取向相同为 1,否则为 0。相邻取向相同时该项为 0,取向不同时贡献 +J,所以界面能是正的,晶界长度减少会让系统能量降低。这也是晶粒长大能自发发生的原因:小晶粒收缩,大晶粒吞并邻居,总晶界长度下降。

模拟时不需要计算整个系统的哈密顿量,只需要算单个格点取向翻转前后的能量差 dE。如果你用四近邻,一个格点最多只有四条边贡献界面能,所以 dE 只有几种离散值,可以用查表法加速,不用每次循环都调 np.sum。

2.3 蒙特卡罗步(MCS)和时间标度

蒙特卡罗步是这里的核心时间单位。一个 MCS 被定义为对所有 N 个格点平均进行一次翻转尝试。具体算法叫 Metropolis 单自旋翻转:

  1. 随机选取一个格点 i。
  2. 随机给出一个新取向 s_new,在 [1, Q] 中均匀抽取。
  3. 计算 dE = E_new - E_old。
  4. 如果 dE ≤ 0,接受新取向;否则以概率 exp(-dE / (k_B T)) 接受。

重复以上过程 N 次,记为一个 MCS。注意每个 MCS 里每个格点不一定只被选一次,因为随机抽样可能有重复和遗漏,这是正常的。想严格控制每个格点都尝试一次,可以做带置换的随机扫过,但那会引入一点点空间相关性,对研究动力学临界指数不利。我倾向于在观测温度和晶格下用随机抽样,统计噪声反而更均匀。

电位上的坑:温度 T 是模型温度,不是摄氏温度。真实物理温度和模型温度之间没有直接换算关系,需要把模拟结果和实验晶粒尺寸随时间变化曲线拟合来确定有效温度。另一个坑是 k_B 通常并入 T 里,所以直接写 exp(-dE/T)。

2.4 代码实现最小框架

下面的 Python 代码只展示 Potts 模型单步核心逻辑,后续还会展开完整模拟流程。这里先看能量差怎么算:

import numpy as np def energy(grid, i, j, orientation, Lx, Ly): """计算格点(i,j)在当前取向下的局部能量,计入周期性边界""" e = 0 for di, dj in [(1,0), (-1,0), (0,1), (0,-1)]: ni, nj = (i + di) % Lx, (j + dj) % Ly if grid[ni, nj] != orientation: e += 1 return e def try_flip(grid, i, j, Q, T, Lx, Ly): """Metropolis翻转尝试,返回是否接受""" old_orient = grid[i, j] e_old = energy(grid, i, j, old_orient, Lx, Ly) new_orient = np.random.randint(1, Q + 1) e_new = energy(grid, i, j, new_orient, Lx, Ly) dE = e_new - e_old if dE <= 0: grid[i, j] = new_orient return True if np.random.rand() < np.exp(-dE / T): grid[i, j] = new_orient return True return False

这段代码里,grid 是二维数组,Lx 和 Ly 是行列数。用取模实现周期性边界,避免晶界在边缘被冻结。Q 是取向总数,T 是模型温度。新取向直接用均匀随机抽样,不考虑邻域已有取向,这种做法的好处是能模拟随机形核,坏处是翻转接受率低,计算浪费大。

后面我会改用更高效的替代方案:新取向不从全体 Q 中抽取,而是从邻域取向中抽样。这样晶粒内部翻转被直接禁止,晶界处的新取向总来自邻居,接受率高很多,模拟出来的晶界更光滑。

3. 用 Python 实现晶粒长大模拟的完整流程

3.1 初始化随机取向的晶格

开始写完整模拟器时,先把数据结构和初始化定好。我用 numpy 的 int16 存放取向,避免用 int64 浪费内存。初始取向随机赋值,可以有两种方式:一种叫 uniform random,每个格点独立从 [1, Q] 中抽取值,得到的初始组织非常碎,需要先进行一段快速弛豫;另一种叫 seeded,预先把几个晶核种在固定位置,让晶粒从核点长出来,适合模拟再结晶初期。

以下代码给出两种初始化和一个简单的输出函数:

import numpy as np def init_uniform(Lx, Ly, Q, seed): """随机初始取向,模拟过冷液体或快速形核初期""" rng = np.random.default_rng(seed) return rng.integers(1, Q + 1, size=(Lx, Ly)) def init_seeded(Lx, Ly, Q, seed, n_grains=16): """预置n_grains个方形晶核,每个内核取向不同""" rng = np.random.default_rng(seed) grid = np.zeros((Lx, Ly), dtype=np.int16) block_x = Lx // int(np.sqrt(n_grains)) block_y = Ly // int(np.sqrt(n_grains)) orientations = rng.choice(Q, size=n_grains, replace=False) + 1 idx = 0 for bx in range(int(np.sqrt(n_grains))): for by in range(int(np.sqrt(n_grains))): x0, x1 = bx*block_x, (bx+1)*block_x y0, y1 = by*block_y, (by+1)*block_y grid[x0:x1, y0:y1] = orientations[idx] idx += 1 return grid

这个初始化的关键是「内聚取向」:种子法生成的初始晶粒没有碎晶,直接进入长大阶段。如果你要研究稳态长大指数,建议用 uniform 跑 100 个 MCS 让组织熟悉化后再开始记数据,否则前几十个 MCS 包含大量形核效应,会污染指数拟合。

3.2 单次翻转尝试与能量差计算

上一章的最小框架直接用全局均匀抽样,效率偏低。实际生产中更常用的做法是:从当前格点周围随机抽取一个近邻取向作为 s_new,然后计算 dE。这样,晶粒内部取向不变,只有晶界处格点才可能翻转,翻转接受率能提高一个数量级。

def attempt_flip(grid, i, j, T, Lx, Ly): """从近邻取向中随机选一个作为新取向,Metropolis接受""" old = grid[i, j] neigh = [] for di, dj in [(1,0), (-1,0), (0,1), (0,-1)]: ni, nj = (i + di) % Lx, (j + dj) % Ly neigh.append(grid[ni, nj]) candidate = neigh[np.random.randint(4)] if candidate == old: return False dE = 0 for di, dj in [(1,0), (-1,0), (0,1), (0,-1)]: ni, nj = (i + di) % Lx, (j + dj) % Ly if grid[ni, nj] != candidate: dE += 1 if grid[ni, nj] != old: dE -= 1 if dE <= 0 or np.random.rand() < np.exp(-dE / T): grid[i, j] = candidate return True return False

注意 dE 的计算逻辑:把当前格点的旧取向记为 old,候选取向为 candidate,局部能量从旧状态换到新状态,只需统计四个近邻里和 candidate 的失配数减去和 old 的失配数。不需要再调用统一 energy 函数,直接做差值更省时间,这也是中等规模网格上跑百万步的常用手法。

参数上,T 的值决定了晶界粗糙度。T 设置太低,晶界很直,模拟容易出现晶界钉扎在 45° 对称位置的现象;T 太高,晶界会反复扰动,晶粒可能被热噪声打碎。常见的经验值范围是 0.1 到 1.0(以 J=1 计),很多论文用 T = 0.3 或 T = 0.6 跑 2D Potts 晶粒长大。

3.3 循环执行 MCS 并记录微观结构

一个 MCS 的驱动逻辑是对网格内随机位点执行 N 次翻转尝试,N = Lx * Ly。为了记录平均晶粒尺寸,需要在每个 MCS 结束时统计晶粒数。统计晶粒数用连通域标记:

from scipy import ndimage def count_grains(grid): """对相同取向的连通区域做标记,返回晶粒数和尺寸列表""" labeled, n = ndimage.label(grid) # 默认四连通 sizes = ndimage.sum(grid, labeled, range(1, n+1)) return n, sizes

注意 ndimage.label 会把空间上离散但同一取值的两处区域识别为同一 label 吗?不会,label 是按连通性分开标记的,相同取向的分离区域会得到不同 label。唯一的问题是六边形晶格不能直接用 ndimage 的默认结构,需要自定义 connectivity 参数。对我们二维方格子,默认四连通就够了。

主循环建议每 10 或 20 个 MCS 采样一次,同时保存原始 grid 的压缩快照,方便后续可视化。保存快照用 np.savez_compressed,比直接保存 npy 更省空间。一个 512x512 的网格,每 10 MCS 存一份,跑 10000 MCS 会产生约 200 MB 压缩文件,注意磁盘预算。

3.4 可视化与数据导出

可视化晶粒组织一般用 matplotlib 的 imshow,并加上不同取向对应不同伪彩色。我喜欢额外画出晶界:取梯度,边界处灰度突变,把梯度二值化后叠加在图像上,就能得到清晰的多晶图。批量生成图片可以用 ffmpeg 合成动画。

import matplotlib.pyplot as plt def snapshot(grid, filename, show_boundary=True): plt.figure(figsize=(6, 6)) plt.imshow(grid, cmap='nipy_spectral', origin='lower', interpolation='nearest') if show_boundary: gy, gx = np.gradient(grid.astype(float)) boundary_mask = (np.abs(gx) > 0.5) | (np.abs(gy) > 0.5) plt.imshow(boundary_mask, cmap='gray', alpha=0.3, interpolation='nearest') plt.axis('off') plt.savefig(filename, dpi=150, bbox_inches='tight') plt.close()

导出数据时,除了平均晶粒面积,最好把晶粒尺寸直方图也保存下来。晶粒长大模拟的经典输出是平均等效半径 R vs MCS,取对数后拟合直线,斜率为生长指数 n。后面会展开讲拟合时的坑,包括早期瞬态和有限尺寸饱和。

最容易被忽略的数据是晶粒面积分布的形状。模拟走到后期,如果发现大晶粒占比过高,几乎肯定是初始化时 Q 太小导致两个不相邻晶粒碰撞后取向一致又合并,这时应该增大 Q 或改用从邻域选取候选取向的方式,从根源上降低合并概率。

4. 蒙特卡罗参数怎么调:温度、晶格尺寸与邻域选取

4.1 温度 T 与热起伏的取舍

模型温度 T 是蒙特卡罗模拟最需要细致的参数。它不像 Ising 模型里的临界温度那么显眼,但在晶粒长大中有两个作用:一是决定晶界迁移是否会被钉扎,二是控制晶界热激活起伏。T 接近 0 时,只要翻转会升高能量就不会接受,系统会迅速进入一种被晶界拓扑锁死的亚稳态,晶粒尺寸不再增长,这是初学者最常见的错误。

T 过高时,晶界会一边犁地一边后退,翻转尝试的接受率很高,但晶粒增长的曲率驱动被热噪声掩盖,最终可能出现非物理的晶粒碎化。我见过有人拿 T = 5 去模拟,结果晶界像一个随机游走的水母,晶粒尺寸和 MCS 指数的关系完全散掉。建议从 T = 0.3 开始扫描,固定晶格尺寸,跑 500 MCS,看平均面积的 log-log 曲线是否呈直线。如果早期凹陷,说明 T 太低;如果噪声极大,说明 T 太高。

一个可供参考的调参矩阵:

参数推荐范围对结果影响需要观察的指标
T0.1 - 1.0温度越低,晶界越平直,接受率越低生长指数 n、晶界粗糙度
Q24 - 64过小导致晶粒碰撞合并后期晶粒合并频率
Lx, Ly128 - 1024过小导致有限尺寸饱和最大晶粒尺寸占比
邻域类型4近邻 / 8近邻4近邻各向异性大晶界形态各向异性
MCS采样间隔10 - 50间隔大则平滑,小则涨落大log-log 曲线线性段

4.2 晶格尺寸与周期性边界

晶格尺寸直接决定你能观察到的最大晶粒和长时间行为。128x128 的网格在 1000 MCS 内通常能跑到平均晶粒尺寸接近边长的一半,之后有限尺寸效应开始主导,生长指数会突然变小,好像组织不再长大,其实是晶粒被周期性边界隔断了。

我通常的做法是固定温度,先用 128 和 256 两种尺寸各自跑短时间,确认 log-log 曲线在小时间范围内重叠;如果重叠良好,再放大到 512 以上进行正式模拟。周期边界条件要写对:处理边缘格点时坐标取模,不要用镜像,因为镜像边界会把两个对称晶粒硬缝在一起,产生非物理的晶界。

4.3 邻域类型与各向异性

二维正方形格子在四近邻下,晶界能量具有明显的各向异性:45°方向的晶界倾向于沿对角线对折,导致晶粒呈菱形而非等轴状。换用八近邻虽然增加了计算量,但能显著减轻这种各向异性,让晶粒更接近圆润的等轴晶。三维模拟则常用 26 近邻或 6 近邻的组合,不同空间方向上的晶界曲率表现差异很大。

需要明确一点:Potts 模型的晶界能本身是各向同性的,各向异性来自网格离散化方式和近邻选取。所以改变邻域类型不是引入物理各向异性,而是在试着削弱数值伪影。如果你就是想去研究特定晶体学取向附近的各向异性晶界,需要额外引入取向差角相关的晶界能项,不是只改邻域能做到的。

4.4 初始化种子数对结果的影响

我见过很多人直接在同一个网格上重复多次实验,发现最终平均晶粒面积曲线几乎不重合,于是怀疑程序有 bug。其实问题出在初始化:uniform random 的方式在系统初期产生的晶粒分布严重依赖于随机种子的聚团情况。种子恰好聚在一侧时,早期大量小晶粒会快速合并,平均面积曲线前面几段很陡。

一个缓解办法是去掉前 5% 到 10% 的 MCS 数据点再做拟合。另一个更可靠的做法是采用 Voronoi 初始化:用少量随机核点生成 Voronoi 图,把每个 Voronoi 单元内格点设为同一取向。这样初始组织的晶粒尺寸分布更接近稳态分布,能缩短达到动力学标度区的时间,也更容易在不同随机种子之间得到可重复的统计结果。

5. 验证模拟正确性:晶粒生长指数与尺寸分布

5.1 平均晶粒面积随 MCS 的变化

晶粒长大的经典判据是平均面积随时间的增长关系,在 Potts 模型中写作<A> = R^2 ~ (MCS)^n。理论上曲率驱动下的正常长大 n = 1(面积线性增长),但很多模拟只得到 n 在 0.6 到 1.0 之间,原因是有限尺寸、初始化方式、温度都影响拟合斜率。我不建议只靠斜率判断程序是否写对了,而是要看整条 log-log 曲线是否在足够长的时间区间内呈直线。

拟合时要注意几个时间窗口:早期(前 50 个 MCS)包含初始组织弛豫,不能计入;后期平均晶粒尺寸接近网格边长一半时进入饱和区,也要截掉。我一般取 MCS 从 100 到总步数的 20% 作为拟合区间,用最小二乘拟合得到斜率,并计算置信区间。多次运行取斜率均值,可以评估随机涨落对指数的影响。

5.2 运用 Aboav-Weaire 定律检查拓扑

晶粒长大的另一个经典标度关系是 Aboav-Weaire 定律:邻居数为 n 的晶粒,相邻晶粒的平均边数 m 与 n 满足m ~ a / n + b。在二维组织里,这个关系几乎是普适的,不管是 Potts 模型还是相场模拟。验证它可以发现程序里很隐蔽的 bug,比如晶粒计数错误或边界上晶粒被截断。

实现上先用连通域标记得到每个晶粒的面积和边数,再统计邻居关系。注意二维 Potts 模型里晶界可能不稳定,一个晶粒可能在某个瞬间被小扰动劈开,所以统计前最好先做一次形态学闭运算,把细微的晶界间隙填平。这个步骤虽然需要额外计算,却能极大减少异常晶粒数据点。

5.3 快速回归测试清单

我建议在修改代码或调参后跑一个固定脚本,输出下面几个指标来快速判断模拟是否正常:

  1. 平均晶粒数量是否单调下降,最终趋近于一个或几个晶粒。
  2. log(<A>)log(t)的早期直线段斜率是否在 0.5 - 1.0 之间。
  3. 晶粒面积分布是否随时间自相似,而不是出现两个峰。
  4. 总能量曲线是否持续下降,且后期下降速率变慢。

把这四项合在一起,比单独看生长指数可靠得多。因为晶粒长大模拟没有绝对正确的图像,只有自洽的动力学和拓扑统计。你的代码如果过不了这些检验,优先检查邻域索引、周期性边界取模、能量差符号,这三处写错一个,后面的结果基本都要推翻。跑通过这一套后,蒙特卡罗方法才真正成为你能信任的组织演化模拟工具。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询