VASP 磁性结构可视化:一键生成完美 VESTA / MCIF

在第一性原理计算中,特别是研究过渡金属硫族化合物(TMDs)、笼目(Kagome)晶格或其他复杂拓扑磁性材料时,直观地展示体系的自旋磁矩分布是不可或缺的一环。

然而从 VASP 的 OUTCAR 中手动提取各原子的磁矩非常抽象且难以可视化。

本文开发并开源了一个极简 Python 脚本:VASP_Mag_Visualizer。

它可以一键读取 VASP 的计算结果,自动生成物理严谨、格式完美兼容的 VESTA 和 MCIF 文件。

核心功能特性

  1. 同步输出包含自旋红蓝着色配置的 .vesta 文件,以及标准的磁性 CIF(.mcif)文件。

  2. 解析 INCAR(初始猜测)与 OUTCAR(最终收敛),生成两套可视化文件,方便评估弛豫过程。

  3. 完整扫描 OUTCAR 中的每一个离子优化步,将每一步的磁矩演化提取至独立的日志文件(OUTCAR_mag_steps.txt)。

运行环境

本脚本仅依赖 Python 3 标准库和 numpy,无需安装庞大的重型材料库。

操作流程

将计算完成的 VASP 文件(需包含 POSCAR, INCAR, OUTCAR)与脚本置于同一目录。在终端执行:

复制代码
python VASP2VESTA_MCIF.py

产出文件说明执行后,目录中将自动生成五件套:

  • VESTA_INCAR_mag.vesta:基于初始设置的三维结构。

  • VESTA_OUTCAR_mag.vesta:最终收敛状态(红正蓝负,直接双击预览)。

  • INCAR_mag.mcif / OUTCAR_mag.mcif:标准磁性 CIF 格式,保留未经视觉缩放的绝对物理数值。

  • OUTCAR_mag_steps.txt:磁矩演化历史日志。

自定义微调通过修改脚本顶部的配置参数

复制代码
# ================= 配置参数 =================MAG_THRESHOLD = 0.05  # 磁矩阈值:绝对值小于此值的感应磁矩不画箭头# ============================================

请直接复制下方源码保存为 VASP2VESTA_MCIF.py 即可使用。

复制代码
import osimport numpy as npimport re
# ================= 配置参数 =================MAG_THRESHOLD = 0.05  # 磁矩阈值:绝对值小于此值的感应磁矩不画箭头OUTPUT_VESTA_INCAR = "VESTA_INCAR_mag.vesta"    # INCAR 初始磁性 VESTAOUTPUT_VESTA_OUTCAR = "VESTA_OUTCAR_mag.vesta"  # OUTCAR 收敛磁性 VESTAOUTPUT_MCIF_INCAR = "INCAR_mag.mcif"            # INCAR 初始磁性 CIFOUTPUT_MCIF_OUTCAR = "OUTCAR_mag.mcif"          # OUTCAR 收敛磁性 CIFMAG_STEPS_FILE = "OUTCAR_mag_steps.txt"         # OUTCAR 步进历史文本# ============================================
def get_cell_params(cell):    """晶胞向量转换为晶格常数和角度"""    a = np.linalg.norm(cell[0])    b = np.linalg.norm(cell[1])    c = np.linalg.norm(cell[2])    def angle(v1, v2):        cos_t = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))        return np.degrees(np.arccos(np.clip(cos_t, -1.0, 1.0)))    alpha, beta, gamma = angle(cell[1], cell[2]), angle(cell[0], cell[2]), angle(cell[0], cell[1])    return a, b, c, alpha, beta, gamma
def read_poscar(filename="POSCAR"):    """读取 POSCAR 文件"""    with open(filename, 'r') as f:        lines = f.readlines()    scale = float(lines[1].strip())    cell = np.array([[float(x) for x in line.split()] for line in lines[2:5]]) * scale    line5 = lines[5].split()    if line5[0].isalpha():        elements = line5        counts = [int(x) for x in lines[6].split()]        start_idx = 7    else:        counts = [int(x) for x in line5]        elements = ["X"] * len(counts)        start_idx = 6    if lines[start_idx].strip().lower().startswith('s'): start_idx += 1    coord_type = lines[start_idx].strip().lower()    start_idx += 1    coords = np.array([[float(x) for x in line.split()[:3]] for line in lines[start_idx:start_idx+sum(counts)]])    if coord_type.startswith('c') or coord_type.startswith('k'):        coords = np.dot(coords, np.linalg.inv(cell))    atom_symbols = []    for el, count in zip(elements, counts): atom_symbols.extend([el]*count)    return cell, atom_symbols, coords
def read_incar_mag(filename="INCAR", num_atoms=0):    """提取 INCAR 磁性设置,兼容各种注释和乘号写法,共线磁矩强制映射到 X 轴"""    ifnot os.path.exists(filename): return None    with open(filename, 'r') as f:        lines = f.readlines()            clean_lines = []    for line in lines:        line = line.split('#')[0].split('!')[0]        clean_lines.append(line.strip())            text = " ".join(clean_lines).upper()    match = re.search(r'MAGMOM\s*=\s*([0-9\.\-\*\s]+)', text)    if not match: return None        magmoms = []    for token in match.group(1).strip().split():        if '*' in token:            try:                count, val = token.split('*')                magmoms.extend([float(val)] * int(count))            except: pass        else:            try: magmoms.append(float(token))            except: pass                if not magmoms: return None        mag_array = np.zeros((num_atoms, 3))    if len(magmoms) >= 3 * num_atoms:        for i in range(min(num_atoms, len(magmoms)//3)):            mag_array[i] = magmoms[3*i:3*i+3]    else:        # 共线磁矩全部映射到 x 轴,保证与 OUTCAR 的 magnetization (x) 严格对应        for i in range(min(num_atoms, len(magmoms))):            mag_array[i, 0] = magmoms[i]                return mag_array
def extract_outcar_mag(filename="OUTCAR", num_atoms=0):    """提取 OUTCAR 磁性历史,支持任意分量的自动拼装"""    if not os.path.exists(filename): return None    with open(filename, 'r') as f: lines = f.readlines()        history = []    current_step_mag = np.zeros((num_atoms, 3))    current_axis = None    in_mag_block = False    dash_count = 0    has_data = False        for line in lines:        if"magnetization (x, y, z)" in line:            if has_data and current_axis is not None:                history.append(current_step_mag.copy())            current_axis = "xyz"            in_mag_block = True            dash_count = 0            current_step_mag = np.zeros((num_atoms, 3))            has_data = False            continue                    elif "magnetization (x)" in line:            if (current_axis in [2, "xyz"]) or (current_axis == 0and has_data):                history.append(current_step_mag.copy())                current_step_mag = np.zeros((num_atoms, 3))            current_axis = 0            in_mag_block = True            dash_count = 0            has_data = False            continue                    elif "magnetization (y)" in line:            current_axis = 1            in_mag_block = True            dash_count = 0            has_data = False            continue                    elif "magnetization (z)" in line:            current_axis = 2            in_mag_block = True            dash_count = 0            has_data = False            continue                    if in_mag_block:            if"---" in line:                dash_count += 1                if dash_count == 2:                    in_mag_block = False            else:                parts = line.split()                if len(parts) >= 2and parts[0].isdigit():                    idx = int(parts[0]) - 1                    if idx < num_atoms:                        has_data = True                        if current_axis == "xyz":                            current_step_mag[idx, 0] = float(parts[1])                            current_step_mag[idx, 1] = float(parts[2])                            current_step_mag[idx, 2] = float(parts[3])                        else:                            current_step_mag[idx, current_axis] = float(parts[-1])                                if has_data:        history.append(current_step_mag.copy())            return history if history else None
def write_vesta_final(filename, cell, symbols, coords, mag_vectors):    """生成完全符合 VESTA 3.5.4 规范的文件,磁矩缩短 20% 防重叠,自带渲染勾选"""    if mag_vectors is None: return    a, b, c, alpha, beta, gamma = get_cell_params(cell)        with open(filename, 'w') as f:        f.write("#VESTA_FORMAT_VERSION 3.5.4\n\n")        f.write("CRYSTAL\n\nTITLE\ncreated_by_Script\n\n")                f.write("GROUP\n1 1 P 1\n")        f.write("SYMOP\n 0.000000  0.000000  0.000000  1  0  0   0  1  0   0  0  1   1\n")        f.write(" -1.0 -1.0 -1.0  0 0 0  0 0 0  0 0 0\n")        f.write("TRANM 0\n 0.000000  0.000000  0.000000  1  0  0   0  1  0   0  0  1\n")        f.write("LTRANSL\n -1\n 0.000000  0.000000  0.000000  0.000000  0.000000  0.000000\n")        f.write("LORIENT\n -1   0   0   0   0\n")        f.write(" 1.000000  0.000000  0.000000  1.000000  0.000000  0.000000\n")        f.write(" 0.000000  0.000000  1.000000  0.000000  0.000000  1.000000\n")        f.write("LMATRIX\n 1.000000  0.000000  0.000000  0.000000\n")        f.write(" 0.000000  1.000000  0.000000  0.000000\n")        f.write(" 0.000000  0.000000  1.000000  0.000000\n")        f.write(" 0.000000  0.000000  0.000000  1.000000\n")        f.write(" 0.000000  0.000000  0.000000\n")                f.write("CELLP\n")        f.write(f"  {a:.6f}   {b:.6f}   {c:.6f}   {alpha:.6f}   {beta:.6f}  {gamma:.6f}\n")        f.write("  0.000000   0.000000   0.000000   0.000000   0.000000   0.000000\n")                f.write("STRUC\n")        for i, (sym, coord) in enumerate(zip(symbols, coords)):            clean_sym = re.sub(r'[^A-Za-z]', '', sym)            f.write(f"  {i+1} {clean_sym}      {clean_sym}{i+1:03d}  1.0000   {coord[0]:.6f}   {coord[1]:.6f}   {coord[2]:.6f}    1a       1\n")            f.write("                            0.000000   0.000000   0.000000  0.00\n")        f.write("  0 0 0 0 0 0 0\n")                valid_vects = []        for i, mag in enumerate(mag_vectors):            if np.linalg.norm(mag) > MAG_THRESHOLD:                valid_vects.append((i+1, mag))                        if valid_vects:            f.write("VECTR\n")            for vect_idx, (atom_id, mag) in enumerate(valid_vects, 1):                # 磁矩乘以 0.8,防止箭头在晶体中穿模重叠                mag_scaled = mag * 0.8                f.write(f"   {vect_idx}    {mag_scaled[0]:.5f}    {mag_scaled[1]:.5f}    {mag_scaled[2]:.5f} 0\n")                f.write(f"    {atom_id}   0    0    0    0\n")                f.write(" 0 0 0 0 0\n")            f.write(" 0 0 0 0 0\n")                        f.write("VECTT\n")            for vect_idx, (atom_id, mag) in enumerate(valid_vects, 1):                # 判断显色逻辑:优先依据主要磁矩方向判断正负                if abs(mag[2]) > 1e-3:                    is_pos = mag[2] > 0                elif abs(mag[0]) > 1e-3:                    is_pos = mag[0] > 0                else:                    is_pos = mag[1] > 0                                r, g, b_color = (255, 0, 0) if is_pos else (0, 0, 255)                f.write(f"   {vect_idx}  0.500 {r:3d} {g:3d} {b_color:3d} 1\n")            f.write(" 0 0 0 0 0\n")                f.write("\nSTYLE\n")        f.write("DISPF 37753794\n")        f.write("MODEL   0  1  0\n")        f.write("SURFS   0  1  1\n")        f.write("SECTS  32  1\n")        f.write("FORMS   0  1\n")        f.write("ATOMS   0  0  1\n")        f.write("BONDS   1\n")        f.write("POLYS   1\n")        f.write("VECTS 1.0\n") # 确保双击文件后自动勾选显示 Vectors
def write_mcif(filename, cell, symbols, coords, mag_vectors):    """生成标准磁性 CIF (.mcif) 文件,保留物理真实长度"""    if mag_vectors is None: return    a, b, c, alpha, beta, gamma = get_cell_params(cell)    inv_cell = np.linalg.inv(cell)        with open(filename, 'w') as f:        f.write("data_magnetic_structure\n\n")        f.write(f"_cell_length_a       {a:.6f}\n")        f.write(f"_cell_length_b       {b:.6f}\n")        f.write(f"_cell_length_c       {c:.6f}\n")        f.write(f"_cell_angle_alpha    {alpha:.6f}\n")        f.write(f"_cell_angle_beta     {beta:.6f}\n")        f.write(f"_cell_angle_gamma    {gamma:.6f}\n\n")                f.write("_symmetry_space_group_name_H-M   'P 1'\n")        f.write("_symmetry_Int_Tables_number      1\n\n")                f.write("loop_\n_atom_site_label\n_atom_site_type_symbol\n_atom_site_fract_x\n_atom_site_fract_y\n_atom_site_fract_z\n")        for i, (sym, coord) in enumerate(zip(symbols, coords)):            clean_sym = re.sub(r'[^A-Za-z]', '', sym)            f.write(f"{clean_sym}{i+1:<4} {clean_sym:<3} {coord[0]:.6f} {coord[1]:.6f} {coord[2]:.6f}\n")                    f.write("\nloop_\n_atom_site_moment_label\n_atom_site_moment_crystalaxis_x\n_atom_site_moment_crystalaxis_y\n_atom_site_moment_crystalaxis_z\n")        for i, (sym, mag) in enumerate(zip(symbols, mag_vectors)):            if np.linalg.norm(mag) > MAG_THRESHOLD:                clean_sym = re.sub(r'[^A-Za-z]', '', sym)                mag_frac = np.dot(mag, inv_cell)                f.write(f"{clean_sym}{i+1:<4} {mag_frac[0]:.6f} {mag_frac[1]:.6f} {mag_frac[2]:.6f}\n")
def write_mag_steps(filename, history):    if not history: return    with open(filename, 'w') as f:        for step_idx, step_mag in enumerate(history):            f.write(f"Step {step_idx + 1}:\n")            for atom_idx, mag in enumerate(step_mag):                f.write(f"  Atom {atom_idx + 1:4d}: {mag[0]:8.4f} {mag[1]:8.4f} {mag[2]:8.4f}\n")            f.write("-" * 40 + "\n")
if __name__ == "__main__":    print("正在读取 POSCAR...")    cell, symbols, coords = read_poscar("POSCAR")    num_atoms = len(symbols)        print("正在处理 INCAR 初始磁性设置...")    incar_mags = read_incar_mag("INCAR", num_atoms)    if incar_mags is not None:        write_vesta_final(OUTPUT_VESTA_INCAR, cell, symbols, coords, incar_mags)        write_mcif(OUTPUT_MCIF_INCAR, cell, symbols, coords, incar_mags)        print(f"  -> 已同步生成 INCAR 文件组: {OUTPUT_VESTA_INCAR} & {OUTPUT_MCIF_INCAR}")    else:        print("  -> 未检测到 INCAR 或无 MAGMOM 设置。")
    print("正在精准提取 OUTCAR 实际收敛磁性...")    outcar_history = extract_outcar_mag("OUTCAR", num_atoms)    if outcar_history is not None:        write_mag_steps(MAG_STEPS_FILE, outcar_history)        write_vesta_final(OUTPUT_VESTA_OUTCAR, cell, symbols, coords, outcar_history[-1])        write_mcif(OUTPUT_MCIF_OUTCAR, cell, symbols, coords, outcar_history[-1])        print(f"  -> 已同步生成 OUTCAR 文件组: {OUTPUT_VESTA_OUTCAR} & {OUTPUT_MCIF_OUTCAR}")        print(f"  -> 磁矩收敛步进历史已导出至: {MAG_STEPS_FILE}")    else:        print("  -> 未检测到 OUTCAR 或 OUTCAR 中无自旋磁矩数据。")            print("\n任务圆满完成!")
相关推荐
daidaidaiyu3 小时前
ThingsBoard 集群的核心逻辑源码分析
java
To_OC4 小时前
从一头雾水到跑通全流程:我用一个周末啃透了JWT登录鉴权
前端·后端·http
我可能是个假开发4 小时前
FTP Unicode 文件名导致 `550` 的问题分析与解决方案
java·开发语言·spring
caoerzhong4 小时前
中小企业上 WMS 该先上哪几块:JeeWMS 开源 Java 仓库管理系统的分批上线清单
java·python·开源
【JAVA】玩家5 小时前
Spring核心原理全解析:从零到生产实战
java·后端·spring
excel5 小时前
研究 Vue 3 源码的收获
前端·vue.js
Wang's Blog6 小时前
Java框架 SpringCloud 快速入门: 实现 Feign 最佳实践(抽取方式)
java·spring cloud
MandalaO_O7 小时前
IDEA 开发(快捷键 + 调试 + 序列化)
java·ide·intellij-idea
可乐鸡翅yeah_7 小时前
业务中 M3U8 水印相关坑,硬水印和动态水印区别
前端·网络·数据库·ffmpeg·m3u8在线
xiaoqiMikko7 小时前
JVM 线上排查实战(七):jps 看不见它、jstack 连不上它,可它明明活得好好的
java·jvm