目录

MD键长键角分析

misaraty 更新 | 2025-06-26
前言
  • 如何使用VASP进行分子动力学(MD)计算?

  • 如何处理MD的输出文件生成pfiles

  • 如何从pfiles中提取键长、键角随时间变化的数据并绘图?

  • 下载示例脚本 计算样例

MD计算

  • POSCAR,

CaTiO₃ mp-5827 为例,先进行高精度 SCF 计算,得到用于 MD 的 POSCAR 输入文件。

  • INCAR,
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
PREC = Accurate
SMASS = -3   #micro canonical ensemble
LREAL = A   #real space
IBRION = 0   #molecular dynamics
NBLOCK = 1   #update XDATCAR every x steps
TEBEG = 300   #start temperature
TEEND = 300   #final temperature
ISIF = 2   #2, ions change; 3, shape and ions change
ISYM = 0   #symmetry
NSW = 1000   #max ionic steps
POTIM = 1   #step width scaling
EDIFFG = -0.01   #ionic relaxation
EDIFF = 1e-5   #electronic SC-loop
LCHARG = .F.   #not save CHGCAR CHG
LWAVE = .F.   #not save WAVECAR
ISMEAR = 0   #gaussian smearing
SIGMA = 0.05   #the width of the smearing in eV
ALGO = Normal   #electronic minimisation algorithm
IVDW = 12   #DFT-D3 method with Becke-Jonson damping, van der Waals
提示
如有必要,可先使用SMASS = -1进行预热,然后再改为SMASS = -3进行恒温采样。

pfils生成

方案一:基于OUTCAR + CONTCAR

  • 需要提供 MD 计算后的OUTCARCONTCAR文件。
注意
也可使用OUTCAR + POSCAR的组合,脚本稍作修改即可兼容。
  • 使用脚本md_outcar_pfiles_v2.2.py生成 pfiles。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import os
import re
import math
import shutil

def print_intro():
    print(
        "Convert OUTCAR from MD simulation to multiple pfiles. Supports generating 0-9999 pfiles.\n\n"
        
        "Usage:\n"
        "Place OUTCAR and CONTCAR from MD simulation in the same directory.\n"
        "The script will generate multiple pfiles in this directory, matching the number of MD steps (NSW in INCAR).\n\n"
        
        'copyright by misaraty (misaraty@163.com) last update: 2025-06-26\n'
    )

def extract_nsw(outcar_path):
    # Extract NSW value from OUTCAR
    with open(outcar_path, 'r') as f:
        for line in f:
            if '   NSW' in line:
                parts = line.split()
                return int(parts[2])
    raise ValueError("NSW keyword not found in OUTCAR.")

def get_atom_count(contcar_path):
    # Calculate total number of atoms from CONTCAR
    with open(contcar_path, 'r') as f:
        lines = f.readlines()
    atom_counts = list(map(int, lines[6].split()))
    return sum(atom_counts)

def extract_forces(outcar_path, atom_count):
    # Extract atomic positions from all MD steps in OUTCAR
    forces = []
    with open(outcar_path, 'r') as f:
        lines = f.readlines()

    for idx, line in enumerate(lines):
        if 'POSITION                                       TOTAL-FORCE' in line:
            block_start = idx + 2
            block = lines[block_start:block_start + atom_count]
            for atom_line in block:
                coords = atom_line.split()[:3]
                forces.append("  " + "  ".join(coords))
    return forces

def write_pfiles(nsw, atom_count, contcar_path, forces, output_dir="pfiles"):
    # Write pfiles based on extracted coordinates
    if os.path.exists(output_dir):
        shutil.rmtree(output_dir)
    os.makedirs(output_dir)

    # Read first 7 lines of CONTCAR (lattice & atom info)
    with open(contcar_path, 'r') as f:
        contcar_lines = f.readlines()[:7]

    for step in range(nsw):
        filename = os.path.join(output_dir, f"p{step+1:04d}")
        with open(filename, 'w') as f:
            f.writelines(contcar_lines)
            f.write("C\n")  # atom label (can be adjusted if necessary)
            start = step * atom_count
            end = start + atom_count
            f.writelines(line + '\n' for line in forces[start:end])

def main():
    # Set working directory to script location and execute conversion
    os.chdir(os.path.dirname(os.path.abspath(__file__)))
    outcar = 'OUTCAR'
    contcar = 'CONTCAR'

    print_intro()
    nsw = extract_nsw(outcar)
    atom_count = get_atom_count(contcar)
    forces = extract_forces(outcar, atom_count)
    write_pfiles(nsw, atom_count, contcar, forces)
    print(f"{nsw} pfiles have been created.\n")

if __name__ == "__main__":
    main()
  • 脚本将在当前目录下自动创建./pfiles文件夹,包含 p0001p0002…等每一步的结构文件。
注意
生成的pXXXX文件使用的是笛卡尔坐标。

方案二:基于XDATCAR

  • 需要提供MD计算后的XDATCAR文件。

  • 使用脚本md_xdatcar_pfiles_v1.1.py生成 pfiles。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import os
import re
import shutil

def print_intro():
    print(
        "Convert XDATCAR to multiple pfiles\n\n"
        
        "Usage:\n"
        "Place the XDATCAR file in the current directory.\n"
        "This script will automatically create a 'pfiles' directory and output one file per step.\n\n"
        
        'copyright by misaraty (misaraty@163.com) last update: 2025-06-26\n'
    )

def read_header_lines(xdatcar_path):
    # Read the first 7 lines from XDATCAR: system name, scaling factor, 3x3 lattice, element symbols, and atom counts
    with open(xdatcar_path, 'r') as f:
        header = [next(f) for _ in range(7)]
    return header

def count_atoms(header_lines):
    # Count the total number of atoms from the 7th line of the header
    atom_counts = list(map(int, header_lines[6].split()))
    return sum(atom_counts)

def parse_xdatcar_blocks(xdatcar_path, atom_count):
    """
    Parse XDATCAR and return a list of snapshots.
    Each snapshot is a list of atomic coordinates for one MD step.
    """
    snapshots = []
    with open(xdatcar_path, 'r') as f:
        lines = f.readlines()

    config_indices = [i for i, line in enumerate(lines) if line.startswith('Direct configuration=')]

    for idx in range(len(config_indices)):
        start = config_indices[idx] + 1
        end = start + atom_count
        coords = lines[start:end]
        coords = ['  '.join(line.strip().split()) + '\n' for line in coords]
        snapshots.append(coords)

    return snapshots

def write_pfiles(header_lines, snapshots, output_dir='pfiles'):
    # Write each snapshot to a separate file in the specified output directory
    if os.path.exists(output_dir):
        shutil.rmtree(output_dir)
    os.makedirs(output_dir)

    for i, coords in enumerate(snapshots, 1):
        fname = os.path.join(output_dir, f"p{i:04d}")
        with open(fname, 'w') as f:
            f.writelines(header_lines)
            f.write("Direct\n")
            f.writelines(coords)

def main():
    print_intro()
    xdatcar = 'XDATCAR'
    if not os.path.exists(xdatcar):
        raise FileNotFoundError("XDATCAR file not found in the current directory.")

    header = read_header_lines(xdatcar)
    atom_count = count_atoms(header)
    snapshots = parse_xdatcar_blocks(xdatcar, atom_count)
    write_pfiles(header, snapshots)
    print(f"Generated {len(snapshots)} pfiles in the './pfiles' directory.")

if __name__ == '__main__':
    main()
  • 同样会在当前目录下生成./pfiles文件夹,包含p0001p0002…等结构文件。
注意
此方法生成的pXXXX文件使用的是分数坐标。

键长分析

注意
脚本自动识别并兼容笛卡尔或分数坐标格式。
  • 使用脚本bond_length_time_v7.2.py可计算指定原子对的键长随时间的变化。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import os
import numpy as np
import warnings
warnings.filterwarnings('ignore')

# Set current working directory to the script location
os.chdir(os.path.dirname(os.path.abspath(__file__)))
print('copyright by misaraty (misaraty@163.com)\n' + 'last update: 2025-06-26\n')

# Parameters
num = 1000  # Number of files
name = [[2, 4], [2, 5]]  # Atom pairs (indices start from 1)

# Read lattice information and coordinate type from p0001
with open('./pfiles/p0001') as f:
    lines = f.readlines()

# Read 3×3 lattice matrix
lattice = np.array([
    list(map(float, lines[2].split())),
    list(map(float, lines[3].split())),
    list(map(float, lines[4].split()))
])

# Calculate inverse lattice matrix
inv_lattice = np.linalg.inv(lattice)

# Check if coordinates are in Direct mode
is_direct = lines[7].strip().startswith('Direct')

# Total number of atoms
natoms = sum(map(int, lines[6].split()))
coord_start = 8  # Starting line of coordinates

def get_cart_coords(filepath):
    # Read coordinates from pfile and convert to Cartesian if needed
    with open(filepath) as f:
        lines = f.readlines()
    coords = np.array([
        list(map(float, lines[coord_start + i].split()))
        for i in range(natoms)
    ])
    if is_direct:
        coords = coords @ lattice  # Convert from Direct to Cartesian
    return coords

def minimum_image(vec, lattice, inv_lattice):
    # Apply minimum image convention to a displacement vector
    frac = vec @ inv_lattice           # Convert to fractional
    frac -= np.round(frac)            # Apply periodic boundary condition
    return frac @ lattice             # Convert back to Cartesian

# Main loop to compute distances
list_sum = []

for pair in name:
    idx_a = pair[0] - 1
    idx_b = pair[1] - 1
    dist_list = []

    for i in range(1, num + 1):
        coords = get_cart_coords(f'./pfiles/p{i:04d}')
        delta = coords[idx_a] - coords[idx_b]
        delta = minimum_image(delta, lattice, inv_lattice)
        dist = np.linalg.norm(delta)
        dist_list.append(dist)

    list_sum.append(dist_list)

# Save results to file
np.savetxt('bond_length_time.dat', np.array(list_sum).T, header=str(name))
注意
  • num = 1000表示NSW = 1000

  • name = [[2, 4], [2, 5]]指定两个原子对,编号从1开始,可通过VESTA获取原子序号。

  • 支持任意晶格,包括非直角晶系。

  • 输出结果为bond_length_time.dat文件,可用于绘图。

  • 示例图:

键角分析

  • 使用脚本angle_time_v7.2.py计算指定三原子间的夹角随时间变化。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import os
import numpy as np
import warnings
warnings.filterwarnings('ignore')

# Set the path to the current script directory
os.chdir(os.path.dirname(os.path.abspath(__file__)))
print('copyright by misaraty (misaraty@163.com)\n' + 'last update: 2025-06-26\n')

# Parameter settings
num = 1000  # Number of frames
name = [[3, 2, 5], [4, 2, 5]]  # Angles defined by triplets A-B-C, B is the vertex

# Read lattice info and coordinate type from p0001
with open('./pfiles/p0001') as f:
    lines = f.readlines()

# Lattice matrix and its inverse
lattice = np.array([
    list(map(float, lines[2].split())),
    list(map(float, lines[3].split())),
    list(map(float, lines[4].split()))
])
inv_lattice = np.linalg.inv(lattice)

# Check if coordinates are in Direct mode
is_direct = lines[7].strip().startswith('Direct')

# Total number of atoms
natoms = sum(map(int, lines[6].split()))
coord_start = 8  # Line where atomic coordinates start

# Function to read coordinates and convert to Cartesian if needed
def get_cart_coords(filepath):
    with open(filepath) as f:
        lines = f.readlines()
    coords = np.array([
        list(map(float, lines[coord_start + i].split()))
        for i in range(natoms)
    ])
    if is_direct:
        coords = coords @ lattice
    return coords

# Apply the minimum image convention
def minimum_image(vec, lattice, inv_lattice):
    frac = vec @ inv_lattice
    frac -= np.round(frac)
    return frac @ lattice

# Main loop to compute angles over all frames
list_sum = []

for triplet in name:
    idx_a, idx_b, idx_c = [x - 1 for x in triplet]
    angle_list = []

    for i in range(1, num + 1):
        coords = get_cart_coords(f'./pfiles/p{i:04d}')
        posA = coords[idx_a]
        posB = coords[idx_b]
        posC = coords[idx_c]

        vecBA = minimum_image(posA - posB, lattice, inv_lattice)
        vecBC = minimum_image(posC - posB, lattice, inv_lattice)

        # Calculate angle in degrees
        dot_product = np.dot(vecBA, vecBC)
        norm_BA = np.linalg.norm(vecBA)
        norm_BC = np.linalg.norm(vecBC)
        cos_theta = np.clip(dot_product / (norm_BA * norm_BC), -1.0, 1.0)
        angle_deg = np.degrees(np.arccos(cos_theta))

        angle_list.append(angle_deg)

    list_sum.append(angle_list)

# Save angle vs. time data
np.savetxt('angle_time.dat', np.array(list_sum).T, header=str(name))
注意
  • num = 1000表示NSW = 1000

  • name = [[3, 2, 5], [4, 2, 5]]表示分析夹角∠A–B–C,其中B是角顶点。

  • 原子编号从1开始,可通过VESTA获取;支持多个角度同时分析。

  • 同样支持任意晶格系统。

  • 输出结果为angle_time.dat文件,可用于绘图。

  • 示例图: