基于Python的硅晶体各向异性3D可视化:<100>、<110>、<111>晶向原子排列对比
2026/9/20 13:52:41 网站建设 项目流程

硅晶体的各向异性是个很反直觉的东西。你拿一块单晶硅,沿着不同晶向去刻蚀、去解理、去外延生长,结果能差出好几倍——<111>面刻蚀慢得让人抓狂,<100>面却快得飞起;<110>面解理出来平整得像镜子,<111>面却容易崩边。很多做半导体工艺、MEMS器件或者晶体材料研究的朋友,脑子里知道"各向异性"这四个字,但真要在三维空间里把<100>、<110>、<111>这三个方向的原子排布差异想象清楚,光靠二维晶格图是远远不够的。

这篇内容就是来解决这个问题的。我用Python搭了一套3D可视化方案,把硅的金刚石立方晶格结构建出来,然后分别沿着<100>、<110>、<111>三个晶向切面,直观展示原子在空间中的排列差异。整套代码基于numpy做晶格计算、matplotlib做3D渲染,不依赖任何商业软件,跑一遍就能生成可交互旋转的3D图。适合做半导体工艺、晶体生长、MEMS设计的朋友参考,也适合刚接触晶体学、想用代码把抽象概念具象化的同学。

下面我会从晶格建模的底层逻辑讲起,把每个参数怎么来的、为什么这么选说清楚,然后逐个拆解三个晶向的切面构建方法,最后分享我在调试过程中踩过的几个坑。

1. 为什么硅的晶向差异值得用3D来看

1.1 金刚石立方结构不是"两套面心立方"那么简单

硅的晶体结构是金刚石立方(diamond cubic),空间群Fd-3m,每个晶胞里有8个原子。很多人第一次学的时候会被告知"就是两个面心立方沿体对角线偏移1/4",这话没错,但真要在代码里把原子坐标生成出来,光记住这句话是不够的。

金刚石立方的基元是这样的:一个原子在(0,0,0),另一个在(1/4,1/4,1/4)。然后面心立方的格点位置有四个:(0,0,0)、(0,1/2,1/2)、(1/2,0,1/2)、(1/2,1/2,0)。把基元的两个原子分别放到这四个格点上,就得到8个原子:

  • (0, 0, 0) 和 (1/4, 1/4, 1/4)
  • (0, 1/2, 1/2) 和 (1/4, 3/4, 3/4)
  • (1/2, 0, 1/2) 和 (3/4, 1/4, 3/4)
  • (1/2, 1/2, 0) 和 (3/4, 3/4, 1/4)

这8个原子在晶胞里的空间分布,决定了不同晶向上原子面的密度和间距完全不同。比如<111>方向上的原子面是密排面,面间距最大,原子面密度也最高;而<100>方向的原子面相对稀疏。这个差异直接导致了刻蚀速率、氧化速率、外延生长质量的不同。

我一开始图省事,只画了面心立方的4个格点,结果<111>方向的原子排列看起来完全不对——因为金刚石结构里那额外的4个原子才是让<111>面呈现ABCABC堆垛的关键。所以第一步,晶胞原子坐标必须老老实实按8个来。

1.2 三个晶向在工艺里的实际意义

在半导体制造里,这三个晶向的选用是有明确场景的:

晶向典型用途关键特性
<100>主流CMOS衬底界面态密度低,电子迁移率高
<110>MEMS结构、解理面解理平整,适合做悬臂梁
<111>外延、特殊器件原子面密度最高,刻蚀最慢

<100>面是现在绝大多数集成电路用的衬底方向,因为Si/SiO2界面态密度最低。但到了MEMS领域,<110>晶向的硅片因为能沿着{111}面解理出非常平整的侧壁,做加速度计、陀螺仪的时候特别有用。<111>面则是刻蚀速率最慢的面,在KOH各向异性刻蚀里经常被用作自停止面。

这些工艺差异的根源,全在原子尺度的排列方式上。用3D图把三个方向的切面画出来,你一眼就能看出为什么<111>面原子排得那么密、为什么<110>面容易解理。

1.3 为什么选Python而不是专业晶体软件

市面上有VESTA、Materials Studio这类专业工具,画晶体结构确实方便。但它们的问题是:第一,授权费用不低;第二,想批量生成不同晶向的切面图,操作起来很繁琐;第三,你没法把可视化逻辑嵌到自己的分析流程里。

用Python的好处是,整个晶格生成、切面计算、渲染逻辑都是你自己可控的。想改晶格常数、想换切面位置、想加原子颜色映射,改几行代码就行。而且numpy的向量化运算处理几千个原子的坐标变换毫无压力,matplotlib的3D模块虽然渲染质量比不上专业软件,但做教学演示和快速分析完全够用。

我实测下来,一个包含5x5x5个晶胞的超胞(1000个原子),从生成坐标到渲染出图,整个过程不到3秒。这个效率对于需要反复调整参数看效果的场景来说,非常舒服。

2. 用numpy搭建金刚石立方晶格

2.1 晶胞原子坐标的生成逻辑

先把最核心的晶胞坐标生成写出来。硅的晶格常数a=5.431埃(室温下),但我们在代码里可以先用归一化坐标,最后再乘以晶格常数。

import numpy as np def generate_diamond_cubic_basis(): """ 生成金刚石立方结构的基元原子坐标(归一化到晶胞边长) 返回8个原子的分数坐标 """ # 面心立方的四个格点 fcc_sites = np.array([ [0.0, 0.0, 0.0], [0.0, 0.5, 0.5], [0.5, 0.0, 0.5], [0.5, 0.5, 0.0] ]) # 基元偏移量 basis_offset = np.array([0.25, 0.25, 0.25]) # 每个格点放两个原子 atoms = [] for site in fcc_sites: atoms.append(site) atoms.append(site + basis_offset) return np.array(atoms) basis = generate_diamond_cubic_basis() print(f"晶胞内原子数: {len(basis)}") print(basis)

这段代码跑出来就是前面说的8个原子坐标。注意basis_offset是加在分数坐标上的,不是笛卡尔坐标。因为金刚石结构的基元偏移是沿着体对角线方向的1/4,在分数坐标下就是每个分量加0.25。

这里有个容易搞错的地方:偏移量是(1/4, 1/4, 1/4),不是(1/4, 0, 0)或者别的组合。我见过有人写成(0.25, 0.25, 0)的,那样生成出来的是闪锌矿结构的一个变体,不是金刚石立方。金刚石立方的两个基元原子必须沿体对角线偏移。

2.2 超胞扩展与原子坐标的笛卡尔转换

单个晶胞只有8个原子,画出来太稀疏,看不出晶面的原子排列规律。我们需要把晶胞在三个方向上重复扩展,构建一个超胞(supercell)。

def build_supercell(basis, nx, ny, nz, a=5.431): """ 构建超胞 basis: 晶胞内原子的分数坐标 nx, ny, nz: 三个方向的重复次数 a: 晶格常数(埃) 返回所有原子的笛卡尔坐标 """ all_atoms = [] for i in range(nx): for j in range(ny): for k in range(nz): # 平移向量 translation = np.array([i, j, k]) # 当前晶胞内的原子坐标 cell_atoms = basis + translation all_atoms.append(cell_atoms) all_atoms = np.vstack(all_atoms) # 转换到笛卡尔坐标 cartesian = all_atoms * a return cartesian atoms = build_supercell(basis, 3, 3, 3) print(f"超胞原子总数: {len(atoms)}") print(f"坐标范围: {atoms.min(axis=0)} 到 {atoms.max(axis=0)}")

3x3x3的超胞有216个原子,坐标范围从0到3a。这个规模用来做可视化刚好,原子不会太密也不会太稀。

这里我用的是分数坐标转笛卡尔坐标的简单乘法,因为金刚石立方的晶格矢量就是正交的(立方晶系),所以直接乘以晶格常数就行。如果是六方晶系或者三斜晶系,就需要用晶格矩阵来转换了。硅是立方晶系,这一步可以偷懒。

2.3 原子颜色的映射策略

为了让3D图更有信息量,我给原子加了颜色映射。思路是这样的:根据原子在某个方向上的坐标值来着色,这样能直观看出原子面的分层。

def get_atom_colors(atoms, direction): """ 根据原子在指定方向上的投影值生成颜色 direction: 单位向量 """ projections = atoms @ direction # 归一化到0-1 norm = (projections - projections.min()) / (projections.max() - projections.min()) return norm

这个投影值其实就是原子在指定晶向上的"深度"。同一层的原子投影值相同,颜色也相同,这样画出来就能看到一层一层的原子面。对于<111>方向,这个分层会特别明显,因为密排面的间距大。

我试过用不同的colormap,'viridis'和'coolwarm'效果都不错。'coolwarm'的冷暖对比更适合展示层状结构,'viridis'在打印成黑白的时候区分度更好。这个看个人喜好。

3. 三个晶向切面的构建方法

3.1 <100>方向:最直观的层状结构

<100>方向就是沿着晶胞的棱方向,也是最容易理解的一个。切面垂直于x轴(或者y轴、z轴,对称等价)。

def slice_along_100(atoms, a=5.431, tolerance=0.1): """ 沿<100>方向切面,提取靠近x=0平面的原子 """ # 归一化x坐标到晶格常数 x_frac = atoms[:, 0] / a # 找到接近整数的x坐标(即原子面位置) mask = np.abs(x_frac - np.round(x_frac)) < tolerance return atoms[mask] slice_100 = slice_along_100(atoms) print(f"<100>切面原子数: {len(slice_100)}")

<100>面的原子排列是正方形网格,每个原子面内的原子间距是a/2(因为面心立方格点的贡献)。相邻原子面之间的距离是a/4。这个结构相对简单,画出来是一层一层的方格。

但要注意,金刚石结构里<100>方向的原子面并不是等间距的。由于基元偏移的存在,原子面会出现"双层"结构——两个靠得很近的原子面,然后隔一段距离再出现下一组。这个细节在二维图上很难表现,但在3D图里旋转一下就能看得很清楚。

3.2 <110>方向:解理面的原子排布

<110>方向是面对角线方向,这个方向的原子排列比<100>复杂一些,但它是硅最常见的解理方向。

def slice_along_110(atoms, a=5.431, tolerance=0.1): """ 沿<110>方向切面 <110>方向的单位向量是(1,1,0)/sqrt(2) """ direction = np.array([1, 1, 0]) / np.sqrt(2) projections = atoms @ direction # 投影值的周期是a/sqrt(2) period = a / np.sqrt(2) proj_frac = projections / period mask = np.abs(proj_frac - np.round(proj_frac)) < tolerance return atoms[mask] slice_110 = slice_along_110(atoms) print(f"<110>切面原子数: {len(slice_110)}")

<110>面的原子排列是矩形网格,而且面上原子的密度比<100>面高。这就是为什么<110>硅片容易解理——原子面间距相对较大,面内结合强,层间结合弱,受力时容易沿这个面分开。

在3D可视化里,<110>切面看起来是一个个矩形排列的原子,而且能看到明显的"锯齿"状边缘。这个锯齿结构是<110>方向原子层堆垛的特征,也是为什么<110>面刻蚀后会形成垂直侧壁的原因。

3.3 <111>方向:密排面的ABC堆垛

<111>方向是体对角线方向,也是最复杂的一个。这个方向的原子面是密排面,原子排列成六角形图案。

def slice_along_111(atoms, a=5.431, tolerance=0.1): """ 沿<111>方向切面 <111>方向的单位向量是(1,1,1)/sqrt(3) """ direction = np.array([1, 1, 1]) / np.sqrt(3) projections = atoms @ direction # 投影值的周期是a/sqrt(3) period = a / np.sqrt(3) proj_frac = projections / period mask = np.abs(proj_frac - np.round(proj_frac)) < tolerance return atoms[mask] slice_111 = slice_along_111(atoms) print(f"<111>切面原子数: {len(slice_111)}")

<111>面的原子排列是六角密排结构,每个原子周围有6个最近邻。这个面的原子面密度最高,面间距也最大(相对于面内原子间距而言)。在KOH刻蚀里,<111>面因为原子密度高、悬挂键少,刻蚀速率最慢,经常被用作刻蚀自停止面。

在3D图里,<111>切面看起来最"密",原子几乎挤在一起。而且由于ABCABC的堆垛顺序,不同层的原子位置有偏移,旋转到侧面看的时候能明显看到三层一个周期的重复。

3.4 三个切面的对比可视化

把三个切面放在同一张图里对比,差异会非常直观。我用matplotlib的subplot来实现:

import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D fig = plt.figure(figsize=(18, 6)) directions = [ ('<100>', np.array([1, 0, 0])), ('<110>', np.array([1, 1, 0]) / np.sqrt(2)), ('<111>', np.array([1, 1, 1]) / np.sqrt(3)) ] for idx, (label, direction) in enumerate(directions): ax = fig.add_subplot(1, 3, idx + 1, projection='3d') # 计算投影并着色 projections = atoms @ direction norm = (projections - projections.min()) / (projections.max() - projections.min()) # 绘制所有原子(半透明) ax.scatter(atoms[:, 0], atoms[:, 1], atoms[:, 2], c='lightgray', s=5, alpha=0.1) # 高亮切面原子 period = a / np.linalg.norm(direction) proj_frac = projections / period mask = np.abs(proj_frac - np.round(proj_frac)) < 0.1 ax.scatter(atoms[mask, 0], atoms[mask, 1], atoms[mask, 2], c=norm[mask], cmap='coolwarm', s=30, alpha=0.9) ax.set_title(f'{label} 晶向切面', fontsize=14) ax.set_xlabel('X (Å)') ax.set_ylabel('Y (Å)') ax.set_zlabel('Z (Å)') plt.tight_layout() plt.savefig('silicon_crystal_orientations.png', dpi=150, bbox_inches='tight') plt.show()

这段代码会生成一张三联图,左边是<100>,中间是<110>,右边是<111>。每个子图里,灰色半透明的点是整个超胞的原子,彩色的点是落在切面附近的原子。颜色从蓝到红表示原子在晶向上的深度。

跑出来之后你会发现,<100>切面的原子排列最稀疏,<110>次之,<111>最密。这个视觉上的密度差异,直接对应了三个晶向在刻蚀速率、氧化速率上的差异。

4. 渲染优化与交互体验

4.1 matplotlib 3D的性能瓶颈与应对

matplotlib的3D scatter在原子数超过2000之后会明显变卡,旋转操作会有延迟。我试过用5x5x5的超胞(1000个原子),旋转还算流畅,但到了8x8x8(4096个原子)就开始卡了。

应对方法有几个:第一,只绘制切面附近的原子,而不是整个超胞。因为我们的重点是展示切面结构,超胞里远离切面的原子对理解帮助不大。第二,降低非切面原子的透明度,让它们几乎不可见,减少渲染负担。第三,如果确实需要展示大超胞,可以考虑用plotly或者pyvista替代matplotlib,它们的WebGL渲染性能好很多。

我个人的做法是:用3x3x3的超胞做交互探索,用5x5x5的超胞出最终的高质量图。这样兼顾了流畅度和视觉效果。

4.2 用plotly做可交互的3D图

如果想让别人也能旋转、缩放你的3D图,plotly是更好的选择。它生成的HTML文件可以直接在浏览器里打开,交互体验比matplotlib好一个档次。

import plotly.graph_objects as go def create_interactive_3d(atoms, direction, label): projections = atoms @ direction norm = (projections - projections.min()) / (projections.max() - projections.min()) period = a / np.linalg.norm(direction) proj_frac = projections / period mask = np.abs(proj_frac - np.round(proj_frac)) < 0.1 fig = go.Figure() # 背景原子 fig.add_trace(go.Scatter3d( x=atoms[~mask, 0], y=atoms[~mask, 1], z=atoms[~mask, 2], mode='markers', marker=dict(size=2, color='lightgray', opacity=0.1), name='背景原子' )) # 切面原子 fig.add_trace(go.Scatter3d( x=atoms[mask, 0], y=atoms[mask, 1], z=atoms[mask, 2], mode='markers', marker=dict(size=5, color=norm[mask], colorscale='Viridis', opacity=0.9), name=f'{label}切面' )) fig.update_layout( title=f'硅晶体 {label} 晶向原子排列', scene=dict( xaxis_title='X (Å)', yaxis_title='Y (Å)', zaxis_title='Z (Å)' ) ) fig.write_html(f'silicon_{label.replace("<", "").replace(">", "")}.html') return fig fig = create_interactive_3d(atoms, np.array([1,1,1])/np.sqrt(3), '<111>') fig.show()

plotly生成的HTML文件大概2-3MB,包含所有原子坐标和渲染逻辑。发给别人直接打开就能看,不需要装Python环境。这个对于做汇报或者写技术文档特别方便。

4.3 原子半径的视觉缩放

在3D图里,原子如果按真实半径画,会互相重叠,看起来一团糊。我一般把原子半径缩放到真实值的20%-30%,这样既能看出原子位置,又不会完全遮挡。

硅的共价半径是111pm,原子间距在晶体里是2.35埃左右。如果按真实半径画,相邻原子会严重重叠。缩放到0.3倍之后,原子之间有空隙,排列结构看得更清楚。

这个缩放比例不是固定的,取决于你的超胞大小和视角。超胞越大,原子可以画得相对大一点;超胞小的话,原子要画小一点避免重叠。我一般先用0.3倍试,看效果再调。

5. 调试过程中踩过的坑

5.1 切面容差设太小导致原子丢失

最开始我用tolerance=0.01来筛选切面原子,结果发现<111>切面上只有零星几个原子,完全看不出密排结构。排查了半天才意识到,浮点数计算累积误差导致很多原子的投影值偏离整数超过了0.01。

金刚石立方的原子坐标里有1/4、3/4这些分数,乘以晶格常数再投影,浮点误差会累积。特别是<111>方向,涉及除以sqrt(3),误差更大。后来我把容差调到0.1,切面原子数量就正常了。

提示:容差不是越小越好。太小会漏掉原子,太大会把相邻层的原子也框进来。对于硅的晶格常数5.431埃,容差在0.05到0.15之间比较合适。可以用切面原子数随容差变化的曲线来确定最佳值。

5.2 超胞边界原子不完整

用3x3x3超胞的时候,边界上的晶胞只有部分原子在计算范围内。比如x方向从0到3a,但原子坐标可能到3a+0.25a(因为基元偏移)。如果直接用np.arange生成平移向量,边界原子会被截断。

我的处理方式是:生成超胞的时候多扩展一圈,然后在可视化的时候用坐标范围来裁剪。这样保证边界上的原子面是完整的,不会出现"缺角"的情况。

def build_supercell_with_margin(basis, nx, ny, nz, a=5.431, margin=1): """ 带边距的超胞构建,确保边界原子完整 """ all_atoms = [] for i in range(-margin, nx + margin): for j in range(-margin, ny + margin): for k in range(-margin, nz + margin): translation = np.array([i, j, k]) cell_atoms = basis + translation all_atoms.append(cell_atoms) all_atoms = np.vstack(all_atoms) cartesian = all_atoms * a # 裁剪到目标范围 mask = np.all((cartesian >= -0.1) & (cartesian <= np.array([nx, ny, nz]) * a + 0.1), axis=1) return cartesian[mask]

这个margin参数设1就够了,多生成一圈原子再裁掉,保证边界完整。

5.3 3D视角的默认设置不好看

matplotlib的3D图默认视角是俯视,对于展示晶面结构来说角度不太理想。我一般会手动设置视角:

ax.view_init(elev=20, azim=45)

elev是仰角,azim是方位角。对于<100>切面,elev=20、azim=45能同时看到切面和层状结构。对于<111>切面,elev=30、azim=60能更好地展示六角排列。

这个没有标准答案,多试几个角度,找到最能体现结构特征的那个。我一般会生成一组不同角度的图,然后挑最好的。

5.4 颜色映射的归一化问题

用投影值做颜色映射的时候,如果直接用原始投影值,不同晶向的数值范围不一样,颜色对比度会差很多。比如<100>方向的投影范围是0到3a,<111>方向是0到3a*sqrt(3),差了1.7倍。

解决方法是每个晶向单独做归一化,把投影值映射到0-1区间。这样三个图的颜色对比度一致,放在一起对比的时候不会因为数值范围不同而产生误导。

norm = (projections - projections.min()) / (projections.max() - projections.min())

这行代码看着简单,但少了它,三个晶向的图放一起就没法直接对比颜色了。

6. 从3D图里能读出什么工艺信息

6.1 原子面密度与刻蚀速率的对应关系

把三个晶向的切面图并排放在一起,最直观的差异就是原子面密度。<111>面的原子排列最密,<110>次之,<100>最疏。这个密度差异直接对应了KOH刻蚀里的速率关系:<100>面刻蚀最快,<110>次之,<111>最慢。

背后的原理是:刻蚀反应发生在表面原子上,表面原子密度越低,每个原子暴露的悬挂键越多,反应活性越高。所以<100>面刻蚀快,<111>面刻蚀慢。这个逻辑在3D图里看得非常清楚——<100>面的原子之间空隙大,刻蚀液容易接触到下面的原子层;<111>面原子挤得密,刻蚀液很难渗透。

6.2 解理方向与原子层间距

<110>方向之所以容易解理,是因为这个方向的原子层间距相对较大,层间结合力弱。在3D图里旋转到侧面看,<110>方向的原子层之间有明显的"间隙",而<111>方向的原子层几乎紧贴在一起。

这个视觉上的间隙差异,对应的是层间结合能的不同。硅的解理面是{111}面,但解理方向是<110>。这是因为{111}面是密排面,面内结合强,但面间结合弱,受力时容易沿{111}面分开,而分开的方向就是<110>。

6.3 外延生长的晶面选择

做外延生长的时候,衬底晶面的选择直接影响外延层的质量。<111>面因为原子面密度高、表面能低,外延生长时容易形成平整的界面。但<111>面的表面悬挂键方向是倾斜的,外延层可能会有孪晶缺陷。<100>面虽然原子密度低,但表面悬挂键方向垂直,外延层质量更可控。

这些差异在3D图里都能找到对应的结构特征。比如<111>面的六角排列,每个原子有3个悬挂键指向面外,方向是倾斜的;<100>面的正方排列,每个原子有2个悬挂键垂直指向面外。悬挂键的方向和数量,决定了外延生长的初始成核行为。

7. 代码封装与复用建议

7.1 把晶格生成封装成类

如果经常需要做不同晶向的可视化,建议把代码封装成一个类,避免每次重复写参数。

class SiliconCrystal: def __init__(self, a=5.431): self.a = a self.basis = self._generate_basis() def _generate_basis(self): fcc_sites = np.array([ [0.0, 0.0, 0.0], [0.0, 0.5, 0.5], [0.5, 0.0, 0.5], [0.5, 0.5, 0.0] ]) basis_offset = np.array([0.25, 0.25, 0.25]) atoms = [] for site in fcc_sites: atoms.append(site) atoms.append(site + basis_offset) return np.array(atoms) def build_supercell(self, nx, ny, nz): # 实现略 pass def get_slice(self, direction, tolerance=0.1): # 实现略 pass def plot_3d(self, direction, ax=None): # 实现略 pass

这样用起来就清爽多了:

si = SiliconCrystal() atoms = si.build_supercell(3, 3, 3) si.plot_3d(np.array([1,1,1])/np.sqrt(3))

7.2 批量生成不同晶向的图

如果需要一次性生成多个晶向的图,可以写个循环:

directions = { '<100>': np.array([1, 0, 0]), '<110>': np.array([1, 1, 0]) / np.sqrt(2), '<111>': np.array([1, 1, 1]) / np.sqrt(3), '<210>': np.array([2, 1, 0]) / np.sqrt(5), '<211>': np.array([2, 1, 1]) / np.sqrt(6) } si = SiliconCrystal() atoms = si.build_supercell(3, 3, 3) for label, direction in directions.items(): fig = si.plot_3d(direction) fig.savefig(f'silicon_{label.strip("<>")}.png', dpi=150)

这样一次跑完,所有晶向的图都出来了。我一般会加上<210>和<211>这两个高阶晶向,它们在特殊器件里也有应用。

7.3 导出原子坐标供其他分析使用

可视化只是第一步,生成的原子坐标可以导出成CSV或者XYZ格式,供其他分析工具使用。

def export_to_xyz(atoms, filename, symbol='Si'): with open(filename, 'w') as f: f.write(f"{len(atoms)}\n") f.write(f"Silicon crystal, {len(atoms)} atoms\n") for atom in atoms: f.write(f"{symbol} {atom[0]:.4f} {atom[1]:.4f} {atom[2]:.4f}\n") export_to_xyz(atoms, 'silicon_supercell.xyz')

XYZ格式是晶体可视化领域的通用格式,VESTA、OVITO、Jmol都能直接打开。这样你可以用Python做初步分析,然后用专业软件做精细渲染,各取所长。

8. 几个实际应用场景的延伸

8.1 各向异性刻蚀的模拟预判

做MEMS加工的时候,经常需要预判KOH刻蚀后的形状。把<100>、<110>、<111>三个面的刻蚀速率比输进去,结合3D晶格图,可以大致判断刻蚀前沿的推进方向。

比如在<100>硅片上刻蚀一个方形窗口,侧壁会沿着{111}面形成54.7度的斜面。这个角度就是<100>面和<111>面的夹角,在3D图里量一下就能验证。我用这个方法给学生讲各向异性刻蚀的时候,比单纯画二维截面图直观多了。

8.2 晶圆键合的对准角度

晶圆键合的时候,两个硅片的晶向对准角度直接影响键合质量。<100>和<110>硅片键合时,如果晶向偏差超过0.5度,键合界面就会出现位错。用3D图把两个晶向的原子排列叠在一起,能直观看到偏差角度对原子匹配的影响。

这个应用我还没在代码里实现,但思路是:把两个不同晶向的切面原子坐标投影到同一个平面上,计算原子位置的重合度。重合度越高,键合质量越好。这个可以作为后续扩展的方向。

8.3 教学演示中的动态旋转

给本科生讲晶体学的时候,静态图很难让学生理解三维空间关系。用plotly生成的交互式HTML,学生可以自己旋转、缩放,从任意角度观察原子排列。我试过在课堂上用这个方式讲<111>面的ABC堆垛,学生的理解速度比看二维图快很多。

而且plotly的HTML文件很小,可以直接嵌到课件里,或者发给学生课后自己看。不需要装任何软件,浏览器打开就行。

9. 性能与精度的平衡取舍

9.1 超胞大小的选择依据

超胞越大,统计意义上的原子排列越有代表性,但渲染越慢。我一般根据用途来选:

用途推荐超胞原子数渲染时间
快速预览2x2x264<1秒
交互探索3x3x32161-2秒
出图发表5x5x510003-5秒
批量分析8x8x8409610-15秒

对于展示晶面原子排列,3x3x3其实就够了。因为切面附近的原子排列规律在2-3个周期内就能看出来,不需要太大的超胞。超胞太大反而会让图显得杂乱。

9.2 浮点精度的处理

硅的晶格常数是5.431埃,原子坐标计算涉及除以sqrt(2)和sqrt(3),浮点误差不可避免。在做切面筛选的时候,容差设得太小会漏原子,设得太大又会混入相邻层。

我的经验是:先用一个较宽松的容差(比如0.2)筛选,然后统计切面原子数随容差的变化。当容差从0.05增加到0.15时,原子数应该基本稳定;超过0.2之后原子数会突然增加(因为开始混入相邻层)。取这个稳定区间的中间值作为最终容差。

这个方法我称之为"容差扫描",对于任何晶向都适用。写成一个函数的话:

def find_optimal_tolerance(atoms, direction, tol_range=np.arange(0.02, 0.3, 0.02)): counts = [] for tol in tol_range: period = a / np.linalg.norm(direction) proj_frac = (atoms @ direction) / period mask = np.abs(proj_frac - np.round(proj_frac)) < tol counts.append(mask.sum()) # 找到计数稳定的区间 counts = np.array(counts) diff = np.abs(np.diff(counts)) stable_idx = np.where(diff < counts.mean() * 0.1)[0] return tol_range[stable_idx[0]] if len(stable_idx) > 0 else 0.1

这个函数会自动扫描容差范围,找到原子数稳定的区间,返回推荐的容差值。对于不同的晶向和超胞大小,都能自适应。

9.3 内存占用的优化

8x8x8的超胞有4096个原子,每个原子3个float64坐标,加上颜色映射的数组,内存占用大概几百KB,完全不是问题。但如果扩展到20x20x20(64000个原子),内存就会到几十MB,matplotlib渲染会非常慢。

对于超大超胞,建议用numpy的memmap或者分块处理。不过说实话,做晶向可视化不需要那么大的超胞。3-5个周期的重复就足够展示原子排列规律了。再大就是浪费计算资源。

10. 最后分享几个实用技巧

第一个技巧:如果你只想快速看某个晶向的原子排列,不需要写完整的类,直接用numpy的矩阵运算几行就能搞定。核心就是atoms @ direction这个投影操作,加上容差筛选。我经常在Jupyter Notebook里临时写几行做快速验证。

第二个技巧:matplotlib的3D图保存成PDF的时候,矢量格式会保留所有原子,文件可能很大。如果只是用于文档嵌入,建议保存成PNG,dpi设150-200就够了。如果要印刷出版,再用PDF格式。

第三个技巧:不同晶向的切面图放在一起对比时,记得把坐标轴范围设成一致。不然<111>方向的图因为投影长度大,会自动缩放,视觉上原子大小和<100>图不一致,对比起来会误导。用ax.set_xlimax.set_ylimax.set_zlim手动统一范围。

第四个技巧:如果你要展示的是原子面的二维排列,其实可以把3D切面上的原子投影到二维平面再画,这样更清晰。但3D图的价值在于展示层间关系,所以两种图各有用途。我一般会同时生成3D和2D两个版本,3D看整体结构,2D看面内排列。

这套代码我从最初写到现在,断断续续改了大半年。最开始只是想画个简单的晶格图,后来越加越多,变成了一个比较完整的晶向可视化工具。中间踩的坑主要集中在浮点精度和视角设置上,这两个问题解决之后,剩下的就是调参数和美化的事了。如果你也在做类似的可视化,希望这些经验能帮你少走点弯路。

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

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

立即咨询