🗓️ 2026-02-06 🎖️ thermal transport 🗂️ DFT

基于QHA计算各向异性的热膨胀系数

目前基于QHA的各向异性热膨胀计算方法有三种,第一种是QHA拟合法,第二种是轴格林艾森法,第三种则是ZSISA及其变体。

这三种算法各有特色,这里汇总介绍一下。

轴格林艾森法

这个方法一般适用于立方,四方和正交体系(晶胞自由度≤3且不涉及角度)。

尽管低对称性晶体也应有相应算法可以求出热膨胀张量,但至少我目前尚未发现现成的脚本或者程序支持计算,需要一定的物理和编程基础。

不推荐材料人对以上三种晶体外的体系使用轴格林艾森法。

Important

在高温区,轴格林艾森法会有饱和现象,预测的热膨胀系数会逐渐趋于定值。

理论部分

格林艾森参数的定义如下:

可以看到,格林艾森参数,是描述微小应变引起的声子频率变化关系的参数。

所谓轴格林艾森参数,即是应变沿着轴时,计算声子得到的格林艾森参数,在计算上与普通的格林艾森参数并无区别。

但是轴格林艾森参数可以通过一个公式,和轴热膨胀系数联系起来,如下:

公式里符号的含义可以在 这篇文献 中找到。

可以看出,想要计算一个轴热膨胀系数,需要以下数据:

这里有个非常有意思的变换,就是宏观格林艾森参数的定义,宏观格林艾森参数 x 宏观热容 = (模式热容 x 模式格林艾森参数)的求和

代码实现

代码源自 Github 的无名客,我进行了翻新,让它可以使用python3来运行。

import numpy as np
from numpy import linalg as LA
import yaml

# -----------------------------
# 读取 grun_tec.in 文件
# -----------------------------
with open('grun_tec.in', 'r') as f:
    lines = [line.strip() for line in f.readlines()]

# 解析参数
An = lines[0].split('=')[1].split(',')[0].strip()  # 'y' 或 'n'
Dm = float(lines[1].split('=')[1].split(',')[0].strip())  # 维度 2/3
IL = int(lines[2].split('=')[1].split(',')[0].strip())  # 独立晶格常数数量 1/2/3
B = int(lines[3].split('=')[1].split(',')[0].strip())   # 声子支数
dd = float(lines[4].split('=')[1].split(',')[0].strip()) # Strain
bi = float(lines[5].split('=')[1].split(',')[0].strip()) # 未使用
fn0 = lines[6].split('=')[1].split(',')[0].strip()
fn1 = lines[7].split('=')[1].split(',')[0].strip()
fn2 = lines[8].split('=')[1].split(',')[0].strip()
fn3 = lines[9].split('=')[1].split(',')[0].strip()
fn4 = lines[10].split('=')[1].split(',')[0].strip()
fn5 = lines[11].split('=')[1].split(',')[0].strip()
fn6 = lines[12].split('=')[1].split(',')[0].strip()
c11 = float(lines[13].split('=')[1].split(',')[0].strip())
c22 = float(lines[14].split('=')[1].split(',')[0].strip())
c33 = float(lines[15].split('=')[1].split(',')[0].strip())
c12 = float(lines[16].split('=')[1].split(',')[0].strip())
c13 = float(lines[17].split('=')[1].split(',')[0].strip())
c23 = float(lines[18].split('=')[1].split(',')[0].strip())
Tm = int(lines[19].split('=')[1].split(',')[0].strip())
v0 = float(lines[20].split('=')[1].split(',')[0].strip()) * 1e-30  # m^3

# -----------------------------
# 常量
# -----------------------------
pi = np.pi
kb = 1.381e-23   # J/K
hb = 1.055e-34   # J*s

# -----------------------------
# 计算宏观格林艾森参数和比热
# -----------------------------
def macrogrun(f0, f1, f2, w, dd, T, jm):
    """
    f0: 平衡频率 rad/s
    f1: 拉伸频率 rad/s
    f2: 压缩频率 rad/s
    w: 声子权重
    dd: Strain
    T: 温度
    jm: 声子支数
    """
    mode_g = -1/f0 * (f2 - f1) / (2*dd)   # 模格林艾森参数
    c1 = hb * f0 / (kb * T)
    c2 = np.exp(-c1)
    cv = w * kb * c1**2 * c2 / (1 + c2**2 - 2*c2)
    Cv = cv.sum() / w.sum() * jm
    I = cv * mode_g
    MG = I.sum() / cv.sum()
    return MG, Cv

# -----------------------------
# 解析 phonopy mesh.yaml 文件
# -----------------------------
def extract_mesh_yaml(fn, num_branches):
    """
    从 phonopy mesh.yaml 提取 mode_index, weight, frequency
    支持新旧 phonopy YAML 格式
    输出 numpy 数组 shape=(num_modes, 3) -> [mode_index, weight, frequency(THz)]
    """
    with open(fn, 'r') as f:
        data = yaml.safe_load(f)

    if "phonon" not in data:
        raise ValueError(f"No 'phonon' section found in {fn}")

    result = []
    for qpoint in data["phonon"]:
        weight = qpoint.get("weight", 1)
        bands = qpoint.get("band", [])

        # 兼容旧 phonopy YAML
        if bands and isinstance(bands[0], dict) and "frequency" not in bands[0] and "frequencies" in bands[0]:
            # 旧格式 frequencies 列表
            freqs = bands[0]["frequencies"]
            for idx, f in enumerate(freqs, start=1):
                result.append([idx, weight, f])
        else:
            for idx, band in enumerate(bands, start=1):
                freq = band.get("frequency", 0.0)
                result.append([idx, weight, freq])
    return np.array(result)

# -----------------------------
# 提取频率数据
# -----------------------------
t0 = extract_mesh_yaml(fn0, B)
t1 = extract_mesh_yaml(fn1, B)
t2 = extract_mesh_yaml(fn2, B)
f0 = t0[:,2] * 1e12 * 2 * pi
f1 = t1[:,2] * 1e12 * 2 * pi
f2 = t2[:,2] * 1e12 * 2 * pi
w  = t0[:,1]

Tem = np.arange(1, Tm)  # 温度数组
X = len(Tem)

# -----------------------------
# 初始化输出矩阵
# -----------------------------
tec = None
MG_arr = None

# -----------------------------
# 各向同性或各向异性计算逻辑
# -----------------------------
if An == 'n':
    # 各向同性
    tec = np.zeros((1,X))
    MG_arr = np.ones((1,X))
    if Dm == 2:
        ec = np.array([[c11,c12],[c12,c11]])*1e9
        sc = LA.inv(ec)
        for i,T in enumerate(Tem):
            mg, cv = macrogrun(f0,f1,f2,w,dd,T,B)
            mg /= Dm
            MG_arr[0,i] = mg
            tec[0,i] = mg*(sc[0,0]+sc[0,1])*cv/v0
    else:
        ec = np.array([[c11,c12,c12],[c12,c11,c12],[c12,c12,c11]])*1e9
        sc = LA.inv(ec)
        for i,T in enumerate(Tem):
            mg, cv = macrogrun(f0,f1,f2,w,dd,T,B)
            mg /= Dm
            MG_arr[0,i] = mg
            tec[0,i] = mg*(sc[0,0]+sc[0,1]+sc[0,2])*cv/v0
else:
    # 各向异性
    t3 = extract_mesh_yaml(fn3, B)
    t4 = extract_mesh_yaml(fn4, B)
    f3 = t3[:,2]*1e12*2*pi
    f4 = t4[:,2]*1e12*2*pi

    MG_arr = np.ones((IL,X))
    tec = np.zeros((IL,X))

    # 如果 IL==3,需要 fn5, fn6
    if IL == 3:
        t5 = extract_mesh_yaml(fn5, B)
        t6 = extract_mesh_yaml(fn6, B)
        f5 = t5[:,2]*1e12*2*pi
        f6 = t6[:,2]*1e12*2*pi
        print("3D anisotropic material with 3 independent lattice constants")
    else:
        print("3D anisotropic material with 2 independent lattice constants (e.g. tetragonal)")

    # 构建弹性常数矩阵
    ec = np.array([[c11,c12,c13],[c12,c22,c23],[c13,c23,c33]])*1e9
    sc = LA.inv(ec)

    for i,T in enumerate(Tem):
        mg0, cv = macrogrun(f0,f1,f2,w,dd,T,B)
        mg1, cv = macrogrun(f0,f3,f4,w,dd,T,B)
        if IL==3:
            mg2, cv = macrogrun(f0,f5,f6,w,dd,T,B)
            MG_arr[0,i] = mg0
            MG_arr[1,i] = mg1
            MG_arr[2,i] = mg2
            tec[0,i] = (mg0*sc[0,0] + mg1*sc[0,1] + mg2*sc[0,2]) * cv/v0
            tec[1,i] = (mg0*sc[1,0] + mg1*sc[1,1] + mg2*sc[1,2]) * cv/v0
            tec[2,i] = (mg0*sc[2,0] + mg1*sc[2,1] + mg2*sc[2,2]) * cv/v0
        else:  # IL==2
            mg0 /= 2
            MG_arr[0,i] = mg0
            MG_arr[1,i] = mg1
            tec[0,i] = (mg0*sc[0,0] + mg0*sc[0,1] + mg1*sc[0,2]) * cv/v0
            tec[1,i] = (mg0*sc[2,0] + mg0*sc[2,1] + mg1*sc[2,2]) * cv/v0

# -----------------------------
# 输出 LTEC.dat
# -----------------------------
ltec = np.vstack((Tem, MG_arr, tec)).T
Y = ltec.shape[1]

with open('LTEC.dat', 'w') as fd:
    fd.write(f"{'T (K)':<6}")
    YY = (Y-1)//2
    for _ in range(YY):
        fd.write(f"{'Macro Gruneisen':^25}")
    for _ in range(YY):
        fd.write(f"{'LTEC (K-1)':^25}")
    fd.write('\n')

    for row in ltec:
        fd.write(f"{int(row[0]):<6}")
        fd.write(f"{row[1]: 20.15e}")
        for val in row[2:]:
            fd.write(f"{val: 25.15e}")
        fd.write('\n')

print("----------- Successful! ------------")

需要一个准备文件,grun_tec.in

声子的计算由phonopy完成。

如下,

An=y,               !Anisotropic (y) or not (n) 
Dim=3,              !2(D) or 3(D)
Independ=2,         !Independent lattice constants
B=12,               !Branches of phonons
Delta=0.01,         !Strain 
#strain=1,          !This parameter is not of use now. (^.^)
fname0=mesh0.yaml,  !Phonon filenames, 0 is for the equilibrium volume
fname1=mesh1.yaml,  !1-6 are phonon filenames under strains. 1 and 2 are compressed and stretched, along the same direction.
fname2=mesh2.yaml,
fname3=mesh3.yaml,
fname4=mesh4.yaml,
fname5=t5.dat,
fname6=t6.dat,
C11=1090.12069,     !Elastic constants, in GPa.
C22=1090.11933,
C33=47.17890,
C12=20.186738,
C13=-12.47374,
C23=-12.47373,
Tmax=800,           !Maximum temperature, in K
v0=33.57,           !Equilibrium volume, in A^3

小结

对声子积分有了深入的认识。

所谓波矢q和支数j,就是mesh.yaml中的q-point和band。

从头加到尾,也就是对声子在布里渊区内积分的近似。

计算模式热容时,注意把phonopy中的频率转化成角频率。

Comment