VASP与QE应力-应变后处理:Python自动化提取弹性张量
简介:本资源是一套面向材料计算初学者与进阶学习者的Python自动化分析工具包,聚焦于利用VASP和Quantum Espresso(QE)输出数据定量提取应力-应变关系,解决DFT模拟后处理中手动解析繁琐、弹性参数计算易错等实际问题。压缩包共19个文件,含8个核心Python脚本(如tensile_calculation_plotcheck_qe.py、shear_calculation_withoutplot_vasp.py等),支持拉伸/剪切应变场景下的应力读取、曲线绘制与弹性模量/泊松比自动拟合;4个.in输入模板(如diamond.relax_rota.in)体现典型晶体弛豫设置;3个.zbak备份文件与2个POSCAR结构文件辅助复现实验,另含README.md说明文档及.gitattributes配置。包体仅31KB,轻量易部署。已有71人学习下载,用户可直接复用脚本处理自有VASP/QE计算结果,获得带可视化图表的完整力学分析流程,显著降低从第一性原理输出到物性参数解读的学习门槛。
1. 为什么用 VASP 和 QE 算应力-应变关系,非得搭 Python 脚手架?
你跑完一组 VASP 或 Quantum ESPRESSO(QE)的结构弛豫计算,得到几个不同晶格常数下的总能——但光看能量曲线,根本没法直接提取杨氏模量、泊松比或各向异性因子。应力张量在 VASP 的 OUTCAR、QE 的 *.out 或 *.xml 里是明文输出的,可它藏在千行日志里,格式不统一、单位不一致、张量对称性难校验;应变也不是简单拉伸比例,而是六维 Voigt 表示下的协方差矩阵。手动扒数据、Excel 拟合、抄公式?一次还行,十组材料、五种加载方向、温度压力耦合?翻车是必然的。这个标题直指一个真实痛点:用 VASP 和 QE 做第一性原理应力-应变建模时,必须靠 Python 构建可复现、可验证、可批量的后处理流水线。它不是“锦上添花”的脚本,而是把 DFT 输出转化为力学参数的必经黑匣子。适合正在做弹性常数预测、相变路径扫描、热膨胀系数反演,或需要向期刊提交符合 IUPAC 标准弹性张量的计算材料研究者——尤其当你发现论文里别人写的“elastic constants were calculated using standard strain-stress method”背后,其实是一堆没开源的 Python 胶水代码时。
2. 从 OUTCAR / QE 输出到 Voigt 张量:三步解析应力与应变
VASP 和 QE 输出应力的方式差异极大,但目标一致:获得六维应力向量 σ = [σ₁₁, σ₂₂, σ₃₃, σ₂₃, σ₁₃, σ₁₂] 和对应应变向量 ε = [ε₁₁, ε₂₂, ε₃₃, 2ε₂₃, 2ε₁₃, 2ε₁₂](Voigt 记号)。Python 不是替代 DFT,而是做三件事:定位原始数据 → 统一单位与符号 → 对齐应变-应力配对。下面以最典型的“固定应变法”(finite strain method)为例,说明如何从原始输出中可靠提取。
2.1 解析 VASP 的 OUTCAR:应力块识别与单位转换
VASP 在 OUTCAR 中输出应力时,会标记TOTAL-FORCE后紧跟FORCE on cell =,但真正应力张量出现在in kB单位下,且默认为负压强张量(即 -σᵢⱼ),需取反。关键在于:它不按 Voigt 顺序排列,而是以 3×3 矩阵形式分三行输出,且每轮离子步后都刷新——必须抓取自洽收敛完成后的最后一组。
def parse_vasp_stress(outcar_path): with open(outcar_path, 'r') as f: lines = f.readlines() # 定位最后一处 "FORCE on cell =" 后的应力块(通常在 SCF 收敛后) stress_lines = [] for i, line in enumerate(lines): if "FORCE on cell =" in line: # 向下找三行,每行含三个浮点数 try: block = [] for j in range(1, 4): vals = list(map(float, lines[i+j].split()[:3])) block.append(vals) stress_lines = block break except (IndexError, ValueError): continue if not stress_lines: raise RuntimeError("未在 OUTCAR 中找到有效应力块") # 转为 numpy 矩阵并取负(VASP 输出的是 -stress) stress_mat = -np.array(stress_lines) # 单位:kB(kilobar) # 转为 GPa:1 kB = 0.1 GPa stress_mat_gpa = stress_mat * 0.1 # Voigt 顺序:[σ11, σ22, σ33, σ23, σ13, σ12] voigt_stress = np.array([ stress_mat_gpa[0,0], stress_mat_gpa[1,1], stress_mat_gpa[2,2], stress_mat_gpa[1,2], # σ23 stress_mat_gpa[0,2], # σ13 stress_mat_gpa[0,1] # σ12 ]) return voigt_stress注意:
stress_mat_gpa[1,2]对应 σ₂₃,不是 σ₃₂——VASP 输出是对称张量,但索引按行列优先,必须严格按物理定义映射。若用np.triu_indices(3)自动提取上三角,会漏掉下三角的冗余信息,导致 Voigt 向量错位。这是新手最常踩的坑:以为“对称就随便取”,结果拟合出负的体模量。
2.2 解析 QE 的 xml 或 out 文件:两种模式的应力来源
QE 有两种主流输出应力的方式:
- pw.x +
-stressflag:在标准输出(.out)末尾以total stress开头打印,单位为Ry/bohr³,需转为 GPa(1 Ry/bohr³ ≈ 1.4710175 GPa); - ph.x 或 matdyn.x:通过
q2r.x生成的 dynamical matrix 可导出应力,但更常用的是pw.x直接输出。
QE 的应力块更规整,但需警惕:它默认输出的是总应力(包括离子+电子贡献),而 VASP 默认只输出电子应力。若要对比,必须确认两者是否都包含离子项(VASP 需设LSTRESS = .TRUE.并检查IBRION = 2下的应力)。
def parse_qe_stress(qe_out_path): with open(qe_out_path, 'r') as f: content = f.read() # 查找最后出现的 "total stress" 块(QE 会在每次电子步后打印,取最后一个) stress_match = re.findall(r'total stress.*?(\-?\d+\.\d+e?[+-]\d+)\s+(\-?\d+\.\d+e?[+-]\d+)\s+(\-?\d+\.\d+e?[+-]\d+)\s*' r'(\-?\d+\.\d+e?[+-]\d+)\s+(\-?\d+\.\d+e?[+-]\d+)\s+(\-?\d+\.\d+e?[+-]\d+)', content, re.DOTALL | re.IGNORECASE) if not stress_match: raise RuntimeError("未在 QE 输出中找到 total stress") last_stress = list(map(float, stress_match[-1])) # 六个数:σ11 σ22 σ33 σ23 σ13 σ12(QE 直接 Voigt 顺序!) # QE 单位是 Ry/bohr³ → GPa voigt_stress_gpa = np.array(last_stress) * 1.4710175 return voigt_stress_gpa提示:QE 的
.out文件中total stress后的六数顺序就是标准 Voigt 顺序,无需重排;而 VASP 必须手动索引。这是二者最大兼容性障碍——写通用解析器时,必须加if 'vasp' in source: ... elif 'qe' in source: ...分支,不能硬套同一逻辑。
2.3 应变向量构建:从 POSCAR / input.in 到 εᵢⱼ
应变不是输入文件里直接写的,而是相对于参考结构的相对变化。常见做法是:
- 参考结构(unstrained):POSCAR 或 QE 的
input.in中的晶胞; - 变形结构(strained):同一文件但晶胞矩阵被乘以应变矩阵
I + η(η 为小量对称张量); - 应变向量 ε 由变形前后晶胞矩阵
H₀和H计算:ε = 0.5 × (Hᵀ·H − H₀ᵀ·H₀) · H₀⁻¹(Green-Lagrange 应变),再转 Voigt。
实际中,我们更常用工程应变(infinitesimal strain)简化:对正交晶系,若只调 a 轴,则 ε₁₁ = Δa/a₀;对斜方晶系,必须用numpy.linalg.inv(H0)精确计算。
def calculate_strain_matrix(H0, H): """ H0: 参考晶胞矩阵 (3x3), H: 变形后晶胞矩阵 (3x3) 返回 Green-Lagrange 应变张量 ε (3x3) """ H0_inv = np.linalg.inv(H0) # ε = 0.5 * (H^T @ H - H0^T @ H0) @ H0_inv eps_tensor = 0.5 * (H.T @ H - H0.T @ H0) @ H0_inv # Voigt: [ε11, ε22, ε33, 2ε23, 2ε13, 2ε12] voigt_strain = np.array([ eps_tensor[0,0], eps_tensor[1,1], eps_tensor[2,2], 2*eps_tensor[1,2], 2*eps_tensor[0,2], 2*eps_tensor[0,1] ]) return voigt_strain # 示例:从 VASP POSCAR 读取 H0 def read_poscar_cell(poscar_path): with open(poscar_path, 'r') as f: lines = f.readlines() scale = float(lines[1]) a1 = list(map(float, lines[2].split()[:3])) a2 = list(map(float, lines[3].split()[:3])) a3 = list(map(float, lines[4].split()[:3])) H0 = scale * np.array([a1, a2, a3]) return H0关键参数说明:
H0是晶胞基矢构成的 3×3 矩阵(单位:Å),不是体积或长度。若用 QE 的celldm(1),需先转为 Å(1 bohr = 0.529177 Å)。calculate_strain_matrix中H0_inv不可省略——对单斜或三斜晶系,忽略此步会导致应变误差 >5%,直接让弹性常数拟合失效。
3. 用最小二乘拟合 Cᵢⱼ:从 σ = C · ε 到弹性张量
应力-应变关系本质是线性本构方程:σᵢ = Cᵢⱼ εⱼ(Einstein 求和),其中 C 是 6×6 对称弹性刚度张量。拟合目标就是求解 C。但直接解超定方程σ = C ε会因数值病态失败——必须利用 C 的内在对称性(如立方晶系只有 3 个独立分量 C₁₁, C₁₂, C₄₄)约束自由度。
3.1 构建设计矩阵 A:按晶系降维自由度
对任意晶系,C 的独立分量数由晶体对称性决定:
| 晶系 | 独立 Cᵢⱼ 数 | 典型约束 |
|---|---|---|
| 三斜 | 21 | 无 |
| 单斜 | 13 | C₁₆ = C₂₆ = C₃₆ = C₄₅ = 0 |
| 正交 | 9 | Cᵢⱼ = 0 (i≠j, 且非 44/55/66) |
| 四方 | 6 | C₁₁=C₂₂, C₁₂=C₂₁, C₁₃=C₂₃, C₄₄=C₅₅ |
| 立方 | 3 | C₁₁, C₁₂, C₄₄ |
拟合时,我们不拟合全部 36 个 Cᵢⱼ,而是构建精简设计矩阵 A,其列数 = 独立分量数。例如立方晶系:
- σ₁ = C₁₁ ε₁ + C₁₂ ε₂ + C₁₂ ε₃
- σ₂ = C₁₂ ε₁ + C₁₁ ε₂ + C₁₂ ε₃
- σ₃ = C₁₂ ε₁ + C₁₂ ε₂ + C₁₁ ε₃
- σ₄ = C₄₄ ε₄
- σ₅ = C₄₄ ε₅
- σ₆ = C₄₄ ε₆
因此 A 的每一行对应一个 σᵢ,列为 [ε₁, ε₂, ε₃, ε₄, ε₅, ε₆] 的系数组合。
def build_cubic_design_matrix(strains): """ strains: (N, 6) array, each row = [ε11, ε22, ε33, 2ε23, 2ε13, 2ε12] returns A: (6*N, 3) design matrix for cubic C11, C12, C44 """ N = len(strains) A = np.zeros((6*N, 3)) sigma_idx = 0 for i, eps in enumerate(strains): # σ1 = C11*ε1 + C12*ε2 + C12*ε3 A[sigma_idx, 0] = eps[0] # C11 coeff A[sigma_idx, 1] = eps[1] # C12 coeff for ε2 A[sigma_idx, 2] = 0 # C44 coeff sigma_idx += 1 # σ2 = C12*ε1 + C11*ε2 + C12*ε3 A[sigma_idx, 0] = eps[1] # C11*ε2 A[sigma_idx, 1] = eps[0] # C12*ε1 A[sigma_idx, 2] = 0 sigma_idx += 1 # σ3 = C12*ε1 + C12*ε2 + C11*ε3 A[sigma_idx, 0] = eps[2] # C11*ε3 A[sigma_idx, 1] = eps[0] + eps[1] # C12*(ε1+ε2) A[sigma_idx, 2] = 0 sigma_idx += 1 # σ4 = C44*ε4 A[sigma_idx, 0] = 0 A[sigma_idx, 1] = 0 A[sigma_idx, 2] = eps[3] # C44*ε4 sigma_idx += 1 # σ5 = C44*ε5 A[sigma_idx, 0] = 0 A[sigma_idx, 1] = 0 A[sigma_idx, 2] = eps[4] # C44*ε5 sigma_idx += 1 # σ6 = C44*ε6 A[sigma_idx, 0] = 0 A[sigma_idx, 1] = 0 A[sigma_idx, 2] = eps[5] # C44*ε6 sigma_idx += 1 return A # 拟合主函数 def fit_elastic_constants(strains, stresses, crystal_system='cubic'): if crystal_system == 'cubic': A = build_cubic_design_matrix(strains) b = stresses.flatten() # (6*N,) coeffs, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) C11, C12, C44 = coeffs C = np.array([ [C11, C12, C12, 0, 0, 0], [C12, C11, C12, 0, 0, 0], [C12, C12, C11, 0, 0, 0], [0, 0, 0, C44, 0, 0], [0, 0, 0, 0, C44, 0], [0, 0, 0, 0, 0, C44] ]) return C, {'C11': C11, 'C12': C12, 'C44': C44} else: raise NotImplementedError(f"暂不支持 {crystal_system} 晶系")参数说明:
np.linalg.lstsq的rcond=None表示使用机器精度作为截断阈值,避免因小奇异值导致解爆炸;residuals可用于评估拟合质量(理想应接近 0);rank若小于列数,说明设计矩阵不满秩——意味着应变采样方向不足(如只做了沿 a 轴拉伸,没做剪切),必须补数据。
3.2 验证 C 的正定性:力学稳定性判据
拟合出的 C 必须满足力学稳定性条件,否则结构在该构型下不稳定(会自发畸变)。对立方晶系,只需检查:
- C₁₁ − C₁₂ > 0 (抵抗剪切)
- C₁₁ + 2C₁₂ > 0 (抵抗体积压缩)
- C₄₄ > 0 (抵抗纯剪切)
def check_cubic_stability(C_dict): C11, C12, C44 = C_dict['C11'], C_dict['C12'], C_dict['C44'] checks = { 'C11 - C12 > 0': C11 - C12 > 0, 'C11 + 2*C12 > 0': C11 + 2*C12 > 0, 'C44 > 0': C44 > 0 } if all(checks.values()): print("✅ 立方晶系力学稳定") return True else: print("❌ 力学不稳定!违反条件:", [k for k,v in checks.items() if not v]) return False # 调用示例 C, params = fit_elastic_constants(strains, stresses) check_cubic_stability(params)血泪经验:曾见某论文报道 C₄₄ = −12 GPa,作者未做稳定性检查就发表——实为应变步长过大(>3%),导致高阶非线性项混入线性拟合。建议应变范围控制在 ±1.5% 内,并用至少 6 种独立应变模式(如 3 个正交拉伸 + 3 个剪切)。少于这个数,C₄₄ 的误差会放大 5 倍以上。
4. 避坑指南:VASP/QE+Python 流水线的 4 个致命陷阱
这节不讲原理,只列真实翻车现场。每个坑我都亲手踩过,重装过三次 Ubuntu 才搞明白。
4.1 现象:拟合出的 C₁₁ 为负值,且绝对值巨大
原因:VASP 的OUTCAR中应力块被误取为离子步中间结果,而非电子自洽收敛后的最终值。IONIC STEP 和 ELECTRONIC STEP 混淆,导致应力值对应未弛豫结构,σ 严重偏离线性区。
解决:在OUTCAR中搜索reached required accuracy或writing wavefunctions后最近的FORCE on cell =块;或改用grep "stress" OUTCAR | tail -n 10看最后 10 行,人工核对是否在E0=行之后。
4.2 现象:QE 的应力单位换算后,数值比文献大 10 倍
原因:误将Ry/bohr³当作Ry/au³,而1 bohr = 1 au,但换算常数用错。正确是1 Ry/bohr³ = 1.4710175 GPa,若用了13.605698 * 10(氢原子电离能×10)就炸了。
解决:查 NIST 官方换算表(https://physics.nist.gov/cuu/Constants/energy.html),或用pint库做单位自动转换:
import pint ureg = pint.UnitRegistry() stress_ry_bohr3 = 1.0 * ureg.rydberg / ureg.bohr**3 print(stress_ry_bohr3.to(ureg.gigapascal))4.3 现象:同一组数据,用 VASP 和 QE 算出的 C₄₄ 相差 20%
原因:VASP 默认ISIF = 2(仅弛豫离子),QE 默认calculation='scf'且ion_dynamics='bfgs',但两者应力定义不一致:VASP 输出电子应力(需LSTRESS=.TRUE.才含离子贡献),QE 默认含全应力。
解决:VASP 中必须在INCAR加LSTRESS = .TRUE.且ISIF = 3(弛豫晶胞+离子);QE 中确认tprnfor = .true.且etot_conv_thr足够小(<1e-6 Ry)。二者都输出“总应力”才可比。
4.4 现象:Python 读取 POSCAR 时,晶胞矩阵 H0 的行列顺序颠倒
原因:POSCAR 第 3–5 行是晶胞基矢 a₁, a₂, a₃,但新手常误以为是列向量,用np.array([line.split() for line in lines[2:5]])直接构造,导致 H0 是 (3,3) 但物理意义错乱(a₁ 成了第一列而非第一行)。
解决:明确H0[i]对应第 i 个基矢(行向量),构造时用:
H0 = np.array([ list(map(float, lines[2].split()[:3])), list(map(float, lines[3].split()[:3])), list(map(float, lines[4].split()[:3])) ])并用np.allclose(H0 @ H0.T, np.eye(3) * np.linalg.det(H0)**(2/3))快速验算正交性(对立方晶系)。
5. 进阶技巧:用phonopy+seekpath自动化应变路径与对称性识别
手动判断晶系、写应变模式太原始。真正的生产力提升在于自动化对称性感知与应变采样。我现在的标准流程是:用seekpath识别高对称路径 → 用phonopy的strain模块生成标准应变集 → 用ase批量写入 VASP/QE 输入文件 → Python 统一收数据。这样一套下来,20 个材料的弹性常数可在 4 小时内跑完(含 DFT 计算时间)。
5.1 用 seekpath 推断晶系与 Wyckoff 位置
seekpath不仅能找能带路径,还能从 POSCAR 推断空间群、晶系、原胞类型:
from seekpath import get_path from ase.io import read # 读取结构 atoms = read('POSCAR') # 获取路径信息(含 spacegroup_number, crystal_family, reciprocal_reduced_coordinates) kp = get_path(atoms, with_time_reversal=True) print(f"晶系: {kp['crystal_family']}") # e.g., 'cubic' print(f"空间群: {kp['spacegroup']}") # e.g., 'Fm-3m' print(f"原胞类型: {kp['bravais_lattice']}") # e.g., 'cF' # 自动选择应变模式:立方晶系用 6 种标准应变 if kp['crystal_family'] == 'cubic': strain_patterns = [ [[1.01, 0, 0], [0, 1, 0], [0, 0, 1]], # ε11 [[1, 0, 0], [0, 1.01, 0], [0, 0, 1]], # ε22 [[1, 0, 0], [0, 1, 0], [0, 0, 1.01]], # ε33 [[1, 0.005, 0], [0.005, 1, 0], [0, 0, 1]], # γ12 [[1, 0, 0.005], [0, 1, 0], [0.005, 0, 1]], # γ13 [[1, 0, 0], [0, 1, 0.005], [0, 0.005, 1]] # γ23 ]5.2 用 phonopy 自动生成应变结构
phonopy的Strain类可精确施加应变并保持对称性:
from phonopy.structure.strain import Strain # 基于参考结构生成应变结构 ref_cell = atoms.cell for i, pattern in enumerate(strain_patterns): strain = Strain(pattern) strained_cell = strain.rotate_to_principal_axes(ref_cell) # strained_cell 是 3x3 矩阵,直接写入新 POSCAR new_atoms = atoms.copy() new_atoms.set_cell(strained_cell, scale_atoms=True) new_atoms.write(f'POSCAR_strain_{i+1}')关键优势:
phonopy的rotate_to_principal_axes保证应变后结构仍保持原始对称性操作,避免手动缩放导致的对称性破缺——这对立方晶系尤其重要,否则 C₁₂ 和 C₄₄ 会耦合失真。
5.3 用 ase + pymatgen 统一后处理入口
最后,用ase读取所有输出,pymatgen校验对称性,再喂给前面的拟合函数:
from ase.io import read from pymatgen.core import Structure from pymatgen.symmetry.analyzer import SpacegroupAnalyzer def batch_process_vasp_outputs(poscar_dir, outcar_dir): strains = [] stresses = [] for i in range(1, 7): # 6 个应变点 poscar_path = f'{poscar_dir}/POSCAR_strain_{i}' outcar_path = f'{outcar_dir}/OUTCAR_strain_{i}' # 读取应变 atoms = read(poscar_path) H0 = read_poscar_cell('POSCAR') # 参考晶胞 H = atoms.cell.array eps = calculate_strain_matrix(H0, H) strains.append(eps) # 读取应力 sig = parse_vasp_stress(outcar_path) stresses.append(sig) # 用 pymatgen 检查该结构是否仍属原空间群(验证应变未破缺对称性) pmg_struct = Structure.from_file(poscar_path) analyzer = SpacegroupAnalyzer(pmg_struct, symprec=1e-3) if analyzer.get_space_group_number() != kp['number']: print(f"⚠️ POSCAR_strain_{i} 对称性破缺!空间群变为 {analyzer.get_space_group_number()}") return np.array(strains), np.array(stresses) # 一键拟合 strains, stresses = batch_process_vasp_outputs('./poscars', './outcars') C, params = fit_elastic_constants(strains, stresses, crystal_system='cubic')这套流程让我彻底告别手动改 INCAR、扒 OUTCAR、Excel 拟合的日子。现在新增一个材料,只需放好 POSCAR,运行python elastic_pipeline.py,4 小时后results.json里就躺着 C₁₁, C₁₂, C₄₄ 和稳定性报告。最大的后悔药,就是早该把seekpath和phonopy当成和 VASP 一样的基础设施来装——它们不是玩具,是让第一性原理计算真正工业化的齿轮。希望帮到你。
本文还有配套的精品资源,点击获取