大模型

1. 原子大模型排名

https://matbench-discovery.materialsproject.org/

2. 使用ASE结合mattersim进行结构优化、分子动力学

1. 使用ASE进行包括晶胞的结构优化

from mattersim.applications.moldyn import MolecularDynamics
from ase.io import read, write
from mattersim.forcefield import MatterSimCalculator
from mattersim.applications.relax import Relaxer
import numpy as np
from ase.optimize import LBFGS
from ase.filters import ExpCellFilter
from ase.io import Trajectory

# calc = MatterSimCalculator(device='cuda')
# calc = MatterSimCalculator(device='cuda:0')
calc = MatterSimCalculator(load_path="MatterSim-v1.0.0-5M.pth", device='cuda:0')

#relaxer = Relaxer(
#    optimizer="BFGS", # the optimization method FIRE, BFGS
#    filter=None, # filter to apply to the cell
#    constrain_symmetry=True, # whether to constrain the symmetry
#)

atoms = read("POSCAR",format='vasp')
atoms.calc = calc

ecf = ExpCellFilter(atoms)

traj = Trajectory('optimization.traj', 'w', atoms)
#converged, relaxed_structure = relaxer.relax_structures(atoms,optimizer="BFGS", # the optimization method FIRE, BFGS
#    filter=None, 
#    constrain_symmetry=True, fmax=0.01 )
opt = LBFGS(ecf)
opt.attach(traj.write, interval=1)
opt.run(fmax=0.01) 

write("POSCAR.vasp", atoms, format="vasp")

2. from wangq

from mattersim.applications.moldyn import MolecularDynamics
from ase.io import read, write
from mattersim.forcefield import MatterSimCalculator
from mattersim.applications.relax import Relaxer
import numpy as np

# calc = MatterSimCalculator(device='cuda')
calc = MatterSimCalculator(load_path="MatterSim-v1.0.0-5M.pth", device='cuda:2')
#默认是1M参数大模型,下面的是5M的

relaxer = Relaxer(
    optimizer="BFGS", # the optimization method FIRE, BFGS
    filter=None, # filter to apply to the cell
    constrain_symmetry=True, # whether to constrain the symmetry
)

atoms = read("SPOSCAR",format='vasp')
atoms.calc = calc

converged, relaxed_structure = relaxer.relax_structures(atoms,optimizer="FIRE", # the optimization method FIRE, BFGS
    filter=None, 
    constrain_symmetry=True, fmax=0.1 )

# # 获取晶胞矩阵
# cell =   relaxed_structure.get_cell()

# # 尝试使用 Cholesky 分解将晶胞矩阵转换为上三角矩阵
# try:
#     # 确保晶胞矩阵是正定的
#     upper_triangular = np.linalg.cholesky(cell.dot(cell.T)).T
# except np.linalg.LinAlgError:
#     # 如果 Cholesky 分解失败,使用 QR 分解
#     _, upper_triangular = np.linalg.qr(cell)

# # 更新   relaxed_structure 对象的晶胞矩阵
# relaxed_structure.set_cell(upper_triangular, scale_atoms=True)

relaxed_structure.calc = calc

ensemble = "NVT_BERENDSEN"
temperature = 1000
timestep = 1.0
taut = 100
trajectory = "atoms.traj"
logfile = "atoms.log"
nvt_runner = MolecularDynamics(atoms=relaxed_structure, ensemble=ensemble, temperature=temperature, timestep=timestep, taut=taut, trajectory=trajectory, logfile = logfile)
nvt_runner.run(1000000)

3. 处理vaspkit 的MSD.dat得到电导率

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import linregress


def dealMSD(inputfile):
    data = np.loadtxt(inputfile,skiprows = 1 )
    data[:,0] = data[:,0] / 1000
    n = data.shape[0]
    start_idx = int(n*0.05)
    end_idx = int(n*0.95)
    time = data[start_idx:end_idx,0]
    MSD = data[start_idx:end_idx,4]
    slope, intercept, r_value, _, _ = linregress(time, MSD)
    MSD_fit = slope * time + intercept

    
    fig,ax1 = plt.subplots(figsize=(4,3),dpi = 600)
    ax1.plot(data[:, 0], data[:, 4], 
            label='600 K', 
            linewidth=2,          # 更粗的线条
            color='#2ecc71',      # 柔和的绿色
            alpha=0.8)  
    ax1.plot(time, MSD_fit, label=f'Fit: y = {slope:.2f}x + {intercept:.2f}', color='red')
    ax1.set_xlabel('t (ps)')
    ax1.set_ylabel(r'MSD ($\AA^2$)')
    ax1.legend()
    ax1.grid(True)
    output_filename = inputfile.replace('/','-') + '.png'
    plt.savefig(output_filename, bbox_inches='tight')
    plt.close()
#    plt.show()
    return slope


diffD = []
temp = [600, 800,1000]
for i in temp:
    inputfile = f"{i}/msd/MSD.dat"
    try:
        slope = dealMSD(inputfile)
        diffuD = slope / 60000
        diffD.append(diffuD)
    except FileNotFoundError:
        print(f"文件 {inputfile} 不存在")
        diffD.append(0)
logdiffD = np.log10(diffD)
temp_inv = np.array([1000 / i for i in temp]) 



slope_D, intercept_D, _, _, _ = linregress(temp_inv, logdiffD)

temp_inv_fit = np.linspace(1, 3.3, 100)
logDfit = slope_D * temp_inv_fit + intercept_D
#logDfit = slope_D * temp_inv + intercept_D
fig,ax1 = plt.subplots(figsize=(4,3),dpi = 600)
ax1.plot(temp_inv_fit, logDfit, label=f'Fit: y = {slope_D:.2f}x + {intercept_D:.2f}', color='red')
ax1.scatter(temp_inv, logdiffD,color = 'blue',marker = 'o')
ax1.set_xlabel('1000/T (1/K)')
ax1.set_ylabel(r'log10(D) (Å$^2$/ps)')
ax1.set_xlim(0.9, 3.5)
ax1.set_ylim(slope_D * 3.5 + intercept_D, slope_D * 0.9 + intercept_D+ 0.3)

y_min = slope_D * 3.5 + intercept_D
y_max = slope_D * 0.9 + intercept_D + 0.3
y_ticks = np.arange(np.floor(y_min / 0.5) * 0.5, np.ceil(y_max / 0.5) * 0.5, 0.5)
ax1.set_yticks(y_ticks)

ax1.legend()
ax1.grid(True)
plt.savefig('arrnius.png',bbox_inches='tight')


from pymatgen.core import Structure
structure = Structure.from_file("POSCAR")
volume = structure.volume
atomsnumber = 30
Eanumber = 0.198691478
T = 300
logD300 = slope_D * (1000 / T) + intercept_D
D = 10 ** logD300

sigma300 = 1.858 * atomsnumber / volume / 300 * (10**12) * D
Ea = -Eanumber * slope_D

print(Ea, sigma300)

4. mattersim-MD直接输出MSD

from mattersim.applications.moldyn import MolecularDynamics
from ase.io import read, write
from mattersim.forcefield import MatterSimCalculator
from mattersim.applications.relax import Relaxer
import numpy as np
import os

# calc = MatterSimCalculator(device='cuda')
# calc = MatterSimCalculator(device='cuda:0')
calc = MatterSimCalculator(load_path="MatterSim-v1.0.0-1M.pth", device='cuda:0')

relaxer = Relaxer(
    optimizer="BFGS", # the optimization method FIRE, BFGS
    filter=None, # filter to apply to the cell
    constrain_symmetry=True, # whether to constrain the symmetry
)

atoms = read("POSCAR",format='vasp')
atoms.calc = calc

converged, relaxed_structure = relaxer.relax_structures(atoms,optimizer="BFGS", # the optimization method FIRE, BFGS
    filter=None, 
    constrain_symmetry=True, fmax=0.01 )

# # 获取晶胞矩阵
# cell =   relaxed_structure.get_cell()

# # 尝试使用 Cholesky 分解将晶胞矩阵转换为上三角矩阵
# try:
#     # 确保晶胞矩阵是正定的
#     upper_triangular = np.linalg.cholesky(cell.dot(cell.T)).T
# except np.linalg.LinAlgError:
#     # 如果 Cholesky 分解失败,使用 QR 分解
#     _, upper_triangular = np.linalg.qr(cell)

# # 更新   relaxed_structure 对象的晶胞矩阵
# relaxed_structure.set_cell(upper_triangular, scale_atoms=True)


relaxed_structure.calc = calc
# import torch
# torch.cuda.empty_cache()
 
ensemble = "NVT_BERENDSEN"
temperature = 600
timestep = 2.0
taut = 100
trajectory = "atoms.traj"
logfile = "atoms.log"  
nvt_runner = MolecularDynamics(atoms=relaxed_structure, ensemble=ensemble, temperature=temperature, timestep=timestep, taut=taut, trajectory=trajectory, loginterval=1,logfile = logfile)
nvt_runner.run(250000)



#将轨迹写入XDATCAR
atoms_xda = read('atoms.traj', index=':')
write('XDATCAR', atoms_xda)

os.remove('atoms.traj')



import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

input_file = 'atoms.log'

data = np.loadtxt(input_file,skiprows=1)

fig,(ax1,ax2) = plt.subplots(1,2,figsize=(10,6),dpi=100)

ax1.plot(data[:, 0], data[:, 1], 
         label='t-Energy', 
         linewidth=2,          # 更粗的线条
         color='#2ecc71',      # 柔和的绿色
         alpha=0.8)            # 轻微的透明度
ax1.set_xlabel('t (ps)')
ax1.set_ylabel('Energy (eV)')
ax1.set_title('t-Energy')
ax1.legend()
ax1.grid(True)

ax2.plot(data[:, 0], data[:, 4], 
         label='t-Temperature', 
         linewidth=2, 
         color='#e74c3c',      # 柔和的红色
         alpha=0.8)
ax2.set_xlabel('t (ps)')
ax2.set_ylabel('Temperature (K)')
ax2.set_title('t-Energy')
ax2.legend()
ax2.grid(True)


fig.tight_layout()

plot_file = 't-E-T.png'
fig.savefig(plot_file, dpi=300, bbox_inches='tight', facecolor='white')



#vaspkit处理
import subprocess

def run_vaspkit(task_id, selectype, element, skip_steps, frame_interval):
    # 构造VASPKIT命令
    command = f"(echo {task_id};echo {selectype};echo {element};echo {skip_steps};echo {frame_interval})| vaspkit"

    # 执行命令
    process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = process.communicate()

    # 输出结果
    if process.returncode == 0:
        print("VASPKIT执行成功!")
        print(stdout.decode())
    else:
        print("VASPKIT执行失败!")
        print(stderr.decode())

# 示例调用
task_id = 722
selectype = 1
element = "Na"
skip_steps = 5000
frame_interval = 1

run_vaspkit(task_id, selectype, element, skip_steps, frame_interval)

if os.path.exists('MSD.dat'):
    os.remove('XDATCAR')

5. mattersim使用Nose-Hoover

from ase.io import read, write
from ase.optimize import BFGS
from ase.md.nose_hoover_chain import NoseHooverChainNVT
from ase import units
import numpy as np
import os
from ase.md.velocitydistribution import ( 
    MaxwellBoltzmannDistribution,
    Stationary,
)
from mattersim.forcefield import MatterSimCalculator

calc = MatterSimCalculator(load_path="MatterSim-v1.0.0-1M.pth", device='cuda:0')


atoms = read("POSCAR",format='vasp')
atoms.calc = calc

dyn = BFGS(atoms)
dyn.run(fmax=0.01)
write("optimized_structure.vasp", atoms)
 
temperature = 600
timestep = 2.0 * units.fs
tdamp = 100
trajectory = "atoms.traj"
logfile = "atoms.log"  
loginterval = 1
append_trajectory = False


MaxwellBoltzmannDistribution(atoms,temperature_K=temperature,force_temp = True)

Stationary(atoms)

md1 = NoseHooverChainNVT(atoms=atoms,timestep=timestep,temperature_K = temperature,tdamp=tdamp,trajectory = None,logfile='equili.log',loginterval= loginterval,append_trajectory=append_trajectory)

md1.run(5000)

md2= NoseHooverChainNVT(atoms=atoms,timestep=timestep,temperature_K = temperature,tdamp=tdamp,trajectory = trajectory,logfile=logfile,loginterval= loginterval,append_trajectory=append_trajectory)

md2.run(5000)



#将轨迹写入XDATCAR
atoms_xda = read('atoms.traj', index=':')
write('XDATCAR', atoms_xda)

os.remove('atoms.traj')



import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

input_file = 'atoms.log'

data = np.loadtxt(input_file,skiprows=1)

fig,(ax1,ax2) = plt.subplots(1,2,figsize=(10,6),dpi=100)

ax1.plot(data[:, 0], data[:, 1], 
         label='t-Energy', 
         linewidth=2,          # 更粗的线条
         color='#2ecc71',      # 柔和的绿色
         alpha=0.8)            # 轻微的透明度
ax1.set_xlabel('t (ps)')
ax1.set_ylabel('Energy (eV)')
ax1.set_title('t-Energy')
ax1.legend()
ax1.grid(True)

ax2.plot(data[:, 0], data[:, 4], 
         label='t-Temperature', 
         linewidth=2, 
         color='#e74c3c',      # 柔和的红色
         alpha=0.8)
ax2.set_xlabel('t (ps)')
ax2.set_ylabel('Temperature (K)')
ax2.set_title('t-Energy')
ax2.legend()
ax2.grid(True)


fig.tight_layout()

plot_file = 't-E-T.png'
fig.savefig(plot_file, dpi=300, bbox_inches='tight', facecolor='white')



#vaspkit处理
import subprocess

def run_vaspkit(task_id, selectype, element, skip_steps, frame_interval):
    # 构造VASPKIT命令
    command = f"(echo {task_id};echo {selectype};echo {element};echo {skip_steps};echo {frame_interval})| vaspkit"

    # 执行命令
    process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = process.communicate()

    # 输出结果
    if process.returncode == 0:
        print("VASPKIT执行成功!")
        print(stdout.decode())
    else:
        print("VASPKIT执行失败!")
        print(stderr.decode())

# 示例调用
task_id = 722
selectype = 1
element = "Na"
skip_steps = 5000
frame_interval = 1

run_vaspkit(task_id, selectype, element, skip_steps, frame_interval)

if os.path.exists('MSD.dat'):
    os.remove('XDATCAR')

6. 计算MSD 处理equili.log & atoms.log

import matplotlib.pyplot as plt
import matplotlib
import numpy as np
import os 
from matplotlib import cm
from scipy.stats import linregress


matplotlib.use('Agg')


def ET(input_file, ax1,ax2):
    data = np.loadtxt(input_file,skiprows=1)
    ax1.plot(data[:, 0], data[:, 1], 
            label='t-Energy', 
            linewidth=2,          # 更粗的线条
            color='#2ecc71',      # 柔和的绿色
            alpha=0.8)            # 轻微的透明度
    ax1.set_xlabel('t (ps)')
    ax1.set_ylabel('Energy (eV)')
    ax1.set_title('t-Energy')
    ax1.legend()
    ax1.grid(True)

    ax2.plot(data[:, 0], data[:, 4], 
            label='t-Temperature', 
            linewidth=2, 
            color='#e74c3c',      # 柔和的红色
            alpha=0.8)
    ax2.set_xlabel('t (ps)')
    ax2.set_ylabel('Temperature (K)')
    ax2.legend()
    ax2.grid(True)


def plottem(tem):
    data1 = np.loadtxt(os.path.join(base_folder, str(tem), 'MSD.dat'))
    data10 = data1[:, [0,4]]

    x = data10[:, 0] / 1000   # Convert time to ps and scale by 2 (example transformation)
    y_data = data10[:, 1]

    return x, y_data

def MSD_T(ax5):
    colors = cm.tab10
    # Plot data for each temperature
    for tem in tem_list:
        x, y_data = plottem(tem)

        n = x.shape[0]
        start_idx = int(n*0.05)
        end_idx = int(n*0.95)
        time = x[start_idx:end_idx]
        MSD = y_data[start_idx:end_idx]
        slope, intercept, r_value, _, _ = linregress(time, MSD)
        MSD_fit = slope * time + intercept

        color = colors.colors[int(tem /100)-4]
        ax5.plot(x, y_data,color=color, label=f'{tem} K',linewidth=2)
        ax5.plot(time, MSD_fit, color='red')
        ax5.set_xlabel('Time (ps)',fontsize=20, fontweight='bold')
        ax5.set_ylabel(r'MSD ($\mathrm{\AA}^2$)',fontsize=20,fontweight='bold')



        ax5.legend(prop={'weight':'bold','size':16})






def dealMSD(inputfile):
    data = np.loadtxt(inputfile,skiprows = 1 )
    data[:,0] = data[:,0] / 1000 
    n = data.shape[0]
    start_idx = int(n*0.05)
    end_idx = int(n*0.95)
    time = data[start_idx:end_idx,0]
    MSD = data[start_idx:end_idx,4]
    slope, intercept, r_value, _, _ = linregress(time, MSD)
    MSD_fit = slope * time + intercept

    return slope


def arrniums(ax6):
    diffD = []

    for i in tem_list:
        inputfile = os.path.join(base_folder,str(i),'MSD.dat')
        try:
            slope = dealMSD(inputfile)
            diffuD = slope / 60000
            diffD.append(diffuD)
        except FileNotFoundError:
            print(f"文件 {inputfile} 不存在")
            diffD.append(0)
    logdiffD = np.log10(diffD)
    temp_inv = np.array([1000 / i for i in tem_list]) 



    slope_D, intercept_D, _, _, _ = linregress(temp_inv, logdiffD)

    temp_inv_fit = np.linspace(0.7, 3.3, 100)
    logDfit = slope_D * temp_inv_fit + intercept_D
    #logDfit = slope_D * temp_inv + intercept_D
    ax6.plot(temp_inv_fit, logDfit, label=f'Fit: y = {slope_D:.2f}x + {intercept_D:.2f}', color='red')
    ax6.scatter(temp_inv, logdiffD,color = 'blue',marker = 'o')
    ax6.set_xlabel('1000/T (1/K)')
    ax6.set_ylabel(r'log10(D) (Å$^2$/ps)')
    ax6.set_xlim(0.6, 3.5)
    ax6.set_ylim(slope_D * 3.5 + intercept_D, slope_D * 0.9 + intercept_D+ 0.3)

    y_min = slope_D * 3.5 + intercept_D
    y_max = slope_D * 0.7 + intercept_D + 0.3
    y_ticks = np.arange(np.floor(y_min / 0.5) * 0.5, np.ceil(y_max / 0.5) * 0.5, 0.5)
    ax6.set_yticks(y_ticks)

    ax6.legend()
    ax6.grid(True)
    # plt.savefig('arrnius.png',bbox_inches='tight')



    from pymatgen.core import Structure
    structure = Structure.from_file("POSCAR")
    num_atoms = structure.num_sites
    if num_atoms == 132:
        volume = structure.volume
        atomsnumber = 30
        Eanumber = 0.198691478
        T = 300
        logD300 = slope_D * (1000 / T) + intercept_D
        D = 10 ** logD300

        sigma300 = 1.858 * atomsnumber / volume / 300 * (10**12) * D
        Ea = -Eanumber * slope_D

        print(Ea, sigma300)




        text_str = f'Ea = {Ea:.2f}  eV\nSigma300K = {sigma300:.2f} mS/cm'
        ax6.text(0.95, 0.95, text_str, transform=ax6.transAxes, 
                fontsize=10, verticalalignment='top', horizontalalignment='right',
                bbox=dict(facecolor='white', alpha=0.8, edgecolor='black'))

    else:
        print('not the 132 atoms cell')



def main():
    fig = plt.figure(figsize = (12,60), dpi = 300)
    gs = fig.add_gridspec(len(tem_list)* 2 + 1 ,2)
    ax1= fig.add_subplot(gs[0,0])
    ax2= fig.add_subplot(gs[0,1])
    MSD_T(ax1)
    arrniums(ax2)
    for idx, tem in enumerate(tem_list):
        row = idx*2 + 1 
        equili_folder = os.path.join(base_folder,str(tem),'equili.log')
        atoms_folder = os.path.join(base_folder,str(tem),'atoms.log')
        ax_equili_T = fig.add_subplot(gs[row,0])
        ax_equili_E = fig.add_subplot(gs[row,1])
        ax_atoms_T = fig.add_subplot(gs[row+1,0]) 
        ax_atoms_E = fig.add_subplot(gs[row+1,1])
        ET(equili_folder,ax_equili_T,ax_equili_E)
        ET(atoms_folder,ax_atoms_T,ax_atoms_E)
        ax_equili_T.set_title(f'{tem} K')
    plt.tight_layout()
    plt.savefig('msd-TE.png')

base_folder = './'
tem_list = [600,800,1000,1200]

if __name__ == '__main__':
    main()

7. 处理不同次的MSD,得到所有MSD

# %%
import numpy as np
import os
import matplotlib.pyplot as plt
from matplotlib import cm
import matplotlib.font_manager as fm

base_folder = './'

def plottem(tem, msd_data_list):
    data1 = np.loadtxt(os.path.join(base_folder, str(tem), str(1), 'msd.dat'))
    data10 = data1[:, 0]
    msd_data_list.append(data10)
    for i in range(1, 4, 1):
        target_path = os.path.join(base_folder, str(tem), str(i), 'msd.dat')
        # print(target_path)
        data = np.loadtxt(target_path, skiprows=1)
        data_all = data[:, 4]
        data_all = data_all *132 / 30
        msd_data_list.append(data_all)
    
    # Padding to match the maximum length of data
    max_len = max(len(arr) for arr in msd_data_list)
    padded_data = np.array([np.pad(arr, (0, max_len - len(arr)), 'constant', constant_values=np.nan) for arr in msd_data_list])
    
    msd_data = padded_data.T
    x = msd_data[:, 0] / 1000 * 2  # Convert time to ps and scale by 2 (example transformation)
    y_data = msd_data[:, 1:]

    return x, y_data



plt.rcParams['font.family'] = 'Times New Roman'
plt.rcParams['font.size'] = 16
plt.rcParams['axes.linewidth'] = 2

plt.rcParams['axes.titlesize'] = 16
plt.rcParams['axes.labelsize'] = 20
plt.rcParams['axes.labelweight'] = 'bold'

plt.rcParams['xtick.labelsize'] = 16
plt.rcParams['ytick.labelsize'] = 16

plt.rcParams['legend.fontsize'] = 16



# Create the plot
plt.figure(figsize=(8, 6),dpi=300)
colors = cm.tab10
# Plot data for each temperature
for tem in [600, 700, 800, 900, 1000]:
    msd_data_list = []
    x, y_data = plottem(tem, msd_data_list)
    y_min = np.nanmin(y_data, axis=1)
    y_max = np.nanmax(y_data, axis=1)
    color = colors.colors[int(tem /100)-5]
    # Plot the curves for the current temperature
    for i in range(y_data.shape[1]):
        plt.plot(x, y_data[:, i],color=color, label=f'{tem} K' if i==0 else "")
    
    # Fill between the min and max values
    plt.fill_between(x, y_min, y_max, color=color, alpha=0.3)

# Final plot settings
plt.xlabel('Time (ps)')
plt.ylabel(r'MSD ($\mathrm{\AA}^2$)')


plt.xticks(fontweight = 'bold')
plt.yticks(fontweight = 'bold')
plt.legend(prop = {'weight' :'bold'})


plt.tight_layout()
# plt.show()
plt.savefig('888.png',dpi=600)

8.计算不同次分子动力学平均后的电导率

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import linregress


def dealMSD(inputfile):
    data = np.loadtxt(inputfile,skiprows = 1 )
    data[:,0] = data[:,0] / 1000 * 2
    n = data.shape[0]
    start_idx = int(n*0.05)
    end_idx = int(n*0.95)
    time = data[start_idx:end_idx,0]
    MSD = data[start_idx:end_idx,4]
    slope, intercept, r_value, _, _ = linregress(time, MSD)
    MSD_fit = slope * time + intercept

    
    fig,ax1 = plt.subplots(figsize=(4,3),dpi = 600)
    ax1.plot(data[:, 0], data[:, 4], 
            label='600 K', 
            linewidth=2,          # 更粗的线条
            color='#2ecc71',      # 柔和的绿色
            alpha=0.8)  
    ax1.plot(time, MSD_fit, label=f'Fit: y = {slope:.2f}x + {intercept:.2f}', color='red')
    ax1.set_xlabel('t (ps)')
    ax1.set_ylabel(r'MSD ($\AA^2$)')
    ax1.legend()
    ax1.grid(True)
    output_filename = inputfile.replace('/','-') + '.png'
    plt.savefig(output_filename, bbox_inches='tight')
    plt.close()
#    plt.show()
    return slope


diffD = []
temp = [600, 700,800,900,1000]
for i in temp:
    diff_temp = []
    for j in [1,2,3]:
        inputfile = f"{i}/{j}/msd.dat"
        try:
            slope = dealMSD(inputfile)
            diffuD = slope / 60000
            diff_temp.append(diffuD)
            # diffD.append(diffuD)
        except FileNotFoundError:
            print(f"文件 {inputfile} 不存在")
            diff_temp.append(0)
    diff_temp = np.array(diff_temp)
    diff_temp_value = np.average(diff_temp)
    diffD.append(diff_temp_value)



logdiffD = np.log10(diffD)
temp_inv = np.array([1000 / i for i in temp]) 



slope_D, intercept_D, _, _, _ = linregress(temp_inv, logdiffD)

temp_inv_fit = np.linspace(1, 3.3, 100)
logDfit = slope_D * temp_inv_fit + intercept_D
#logDfit = slope_D * temp_inv + intercept_D
fig,ax1 = plt.subplots(figsize=(4,3),dpi = 600)
ax1.plot(temp_inv_fit, logDfit, label=f'Fit: y = {slope_D:.2f}x + {intercept_D:.2f}', color='red')
ax1.scatter(temp_inv, logdiffD,color = 'blue',marker = 'o')
ax1.set_xlabel('1000/T (1/K)')
ax1.set_ylabel(r'log10(D) (Å$^2$/ps)')
ax1.set_xlim(0.9, 3.5)
ax1.set_ylim(slope_D * 3.5 + intercept_D, slope_D * 0.9 + intercept_D+ 0.3)

y_min = slope_D * 3.5 + intercept_D
y_max = slope_D * 0.9 + intercept_D + 0.3
y_ticks = np.arange(np.floor(y_min / 0.5) * 0.5, np.ceil(y_max / 0.5) * 0.5, 0.5)
ax1.set_yticks(y_ticks)

ax1.legend()
ax1.grid(True)
plt.savefig('arrnius.png',bbox_inches='tight')


from pymatgen.core import Structure
structure = Structure.from_file("POSCAR")
volume = structure.volume
atomsnumber = 132
Eanumber = 0.198691478
T = 300
logD300 = slope_D * (1000 / T) + intercept_D
D = 10 ** logD300

sigma300 = 1.858 * atomsnumber / volume / 300 * (10**12) * D
Ea = -Eanumber * slope_D

print(Ea, sigma300)

3. 一些结果

大模型对NaInSiO的计算

image-20250121214124415

大模型能够比较好的模拟出Na的离子扩散行为

4. 一些脚本

1. 把ase轨迹转换为XDATCAR的脚本

使用ase自带的方法

ase convert --input-format traj --output-format vasp-xdatcar atoms.traj XDATCAR

python脚本

from ase.io import read, write

# 读取原始轨迹文件
atoms = read('atoms.traj', index=':')

# 将轨迹写入 XDATCAR 格式
write('XDATCAR', atoms)

2. 蒸馏大模型数据变成符合acnn标准的xsf文件的脚本

from mattersim.applications.moldyn import MolecularDynamics
from ase.io import read
from mattersim.forcefield import MatterSimCalculator
from mattersim.applications.relax import Relaxer
import numpy as np

# 使用 MatterSimCalculator 初始化计算器
calc = MatterSimCalculator(load_path="MatterSim-v1.0.0-5M.pth", device='cuda:0')

# 设置优化器
relaxer = Relaxer(
    optimizer="BFGS",  # 优化方法,使用 FIRE 或 BFGS
    filter=None,  # 是否应用过滤器
    constrain_symmetry=True,  # 是否约束对称性
)

# 读取 POSCAR 文件
atoms = read("POSCAR111.vasp", format='vasp')
atoms.calc = calc

# 进行结构弛豫
converged, relaxed_structure = relaxer.relax_structures(atoms, optimizer="FIRE", filter=None, constrain_symmetry=True, fmax=0.05)

# 设置计算器并准备进行分子动力学模拟
relaxed_structure.calc = calc

# 分子动力学参数
ensemble = "NVT_BERENDSEN"
temperature = 1000
timestep = 1.0
taut = 50
trajectory = "atoms.traj"
logfile = "atoms.log"
nvt_runner = MolecularDynamics(
    atoms=relaxed_structure,
    ensemble=ensemble,
    temperature=temperature,
    timestep=timestep,
    taut=taut,
    trajectory=trajectory,
    loginterval=1,
    logfile=logfile
)

# 保存每一帧为 .xsf 文件的函数
def write2my(file_path, ene_i, lat_i, ele_i, coo_i, foc_i):
    lat_i = lat_i.reshape(3, 3)
    coo_i = coo_i.reshape(-1, 3)
    foc_i = foc_i.reshape(-1, 3)
    with open(file_path, 'w') as file:
        file.write(f"# total energy = {ene_i} eV\n\n")
        file.write("CRYSTAL\n")
        file.write("PRIMVEC\n")
        
        for j in lat_i:
            for k in j:
                file.write(f'{k:20.8f}')
            file.write('\n')

        file.write("PRIMCOORD\n")
        file.write(f"{len(coo_i)} 1\n")

        for j in range(len(coo_i)):
            element_name = chemical_symbols[ele_i[j]]
            file.write(f'{element_name:2} ')
            # 写入坐标
            for k in coo_i[j]:
                file.write(f"{k:20.8f}")
            # 写入受力
            for k in foc_i[j]:
                file.write(f"{k:20.8f}")
            file.write("\n")

# 模拟运行并每50步保存一次 .xsf 文件
for step in range(1000000):  # 你可以根据需要设置步数
    nvt_runner.run(1)  # 运行一个步长
    atoms = nvt_runner.atoms  # 获取当前步长的原子对象
    
    # 每50步保存一次数据
    if step % 100 == 0:
        # 获取总能量
        energy = atoms.get_total_energy()
        
        # 获取晶胞矩阵
        cell = atoms.get_cell()
        
        # 获取原子元素类型
        atomic_numbers = atoms.get_atomic_numbers()
        
        # 获取原子坐标
        positions = atoms.get_positions()
        
        # 获取原子受力
        forces = atoms.get_forces()
        
        # 保存当前帧数据为 .xsf 文件
        frame_filename = f"frame_{step}.xsf"
        write2my(frame_filename, energy, cell, atomic_numbers, positions, forces)

3. 扩胞脚本

from pymatgen.io.vasp import Poscar

# 读取 POSCAR 文件
poscar = Poscar.from_file("POSCAR")

# 扩胞为 2x2x3
supercell = poscar.structure * [6, 6, 6]

# 将扩胞后的结构写入新的 POSCAR 文件
poscar = Poscar(supercell)
poscar.write_file("POSCAR666.vasp")
from ase.io import read,write
from ase.build import make_supercell
atoms = read("POSCAR")

P = [[3, 0, 0],
     [0, 3, 0],
     [0, 0, 3]]

supercell = make_supercell(atoms,P)
supercell.write("POSCAR333")

4. 计算体积脚本

from pymatgen.core import Structure

# 读取POSCAR文件
#structure = Structure.from_file("POSCAR.vasp")
structure = Structure.from_file("POSCAR.vasp")

# 计算体积
volume = structure.volume

# 输出体积
print(f"{volume} ų")

5. 元素替换脚本

import os
import shutil

# 创建文件夹
base_path = "./"  # 替换为你的根目录路径
element_symbols = ["La", "Ce","Pr","Nd","Pm","Eu","Sm","Gd","Tb","Dy","Ho","Er","Tm","Sc","Ti","V","Nb","Ta","Cr","Mo","Mn","Fe","Ru","Co","Rh","Ir","Ni","Pd","Cu","B","Al","Ga","In","N","P","Sb","Bi"]

for idx, element in enumerate(element_symbols, start=1):
    folder_name = f"{idx}-{element}"
    folder_path = os.path.join(base_path, folder_name)
    
    if not os.path.exists(folder_path):
        os.makedirs(folder_path)

# 读取原始 POSCAR 文件
input_file = "./POSCAR"  # 替换为你的 POSCAR 文件路径

with open(input_file, "r") as file:
    lines = file.readlines()

# 逐个元素进行替换并创建文件夹
for idx, element in enumerate(element_symbols, start=1):
    folder_name = f"{idx}-{element}"
    folder_path = os.path.join(base_path, folder_name)
    
    modified_lines = [line.replace("Y", element) for line in lines]
    
    # 写入修改后的内容到对应的文件夹
    output_file = os.path.join(folder_path, "POSCAR")
    with open(output_file, "w") as file:
        file.writelines(modified_lines)
    
    # 复制同名的 INCAR 文件到对应的文件夹
    incar_source = "./INCAR"  # 替换为你的 INCAR 文件路径
    incar_destination = os.path.join(folder_path, "INCAR")
    shutil.copy(incar_source, incar_destination)
    lsf_source = "./vasp.pbs"  # 替换为你的 INCAR 文件路径
    lsf_destination = os.path.join(folder_path, "opt.lsf")
    shutil.copy(lsf_source, lsf_destination)
print("Replacement and file writing complete.")

5. 使用m3gnet

# import warnings

# from m3gnet.models import Relaxer
# from pymatgen.core import Lattice, Structure
# from ase.io import read, write
# import tensorflow as tf
# from ase.optimize import BFGS

# for category in (UserWarning, DeprecationWarning):
#     warnings.filterwarnings("ignore", category=category, module="tensorflow")


# gpus = tf.config.list_physical_devices('GPU')
# if len(gpus) > 0:
#     tf.config.set_visible_devices(gpus[0], 'GPU') 

# atoms = read("POSCAR222.vasp",format='vasp')

#  # 只使用第一块 GPU

# relaxer = Relaxer(optimizer = "BFGS")  # This loads the default pre-trained model

# relax_results = relaxer.relax(atoms,verbose=True)

# final_structure = relax_results['final_structure']
# final_energy_per_atom = float(relax_results['trajectory'].energies[-1] / len(atoms))

# print(f"Relaxed lattice parameter is {final_structure.lattice.abc[0]:.3f} Å")
# print(f"Final energy is {final_energy_per_atom:.3f} eV/atom")

from ase.optimize import BFGS
from m3gnet.models import Relaxer,M3GNetCalculator
from ase.io import read, write
import tensorflow as tf

gpus = tf.config.list_physical_devices('GPU')
if len(gpus) > 0:
    tf.config.set_visible_devices(gpus[0], 'GPU') 

relaxer = Relaxer()
calculator = relaxer.calculator

atoms = read("POSCAR222.vasp",format='vasp')
# print(f"unrelaxed lattice parameter is {atoms.lattice.abc[0]:.3f} Å")
atoms.calc = calculator

optimizer = BFGS(atoms)

optimizer.run(fmax=0.01) 

# print(f"Relaxed lattice parameter is {atoms.lattice.abc[0]:.3f} Å")

6. ASE结合phonopy 使用大模型作为计算器

1.关于声子谱

有限位移法(Finite Displacement Method)是一种用于计算晶体声子谱的数值方法,其基本原理是通过对晶体结构中的原子进行微小的位移,计算相应的力,从而构建力常数矩阵,进而求解声子频率和振动模式。

具体步骤如下:

结构优化:

首先,对晶体结构进行高精度的优化,以获得平衡的原子位置和晶格常数。
在优化过程中,需要设置适当的收敛标准,如能量收敛(EDIFF)和力收敛(EDIFFG)等,以确保结构的准确性。
构建超胞:

在优化后的结构基础上,使用Phonopy等工具构建超胞。
超胞的尺寸通常是原胞的整数倍,具体尺寸取决于所需的声子计算精度和计算资源。
原子位移:

对超胞中的每个原子进行微小的位移,通常在0.01到0.03埃之间。
这些位移可以是沿着晶体轴的正负方向,或者在不同方向上进行。
力计算:

在每个位移结构上,进行单点能量计算,获取每个原子所受的力。
这些力数据用于构建力常数矩阵。
构建力常数矩阵:

利用获得的力数据,构建力常数矩阵。
该矩阵描述了原子间的相互作用,是计算声子谱的基础。
声子谱计算:

通过对力常数矩阵进行对角化,得到声子的频率和振动模式。
进一步,可以绘制声子色散曲线,分析声子的行为。
注意事项:

有限位移法的计算精度高度依赖于力的计算精度,因此在结构优化和单点计算中需要设置严格的收敛标准。
位移幅度的选择需要平衡计算精度和计算量,过大的位移可能导致力常数矩阵的非线性,过小的位移可能导致数值误差。
计算过程中,可能需要考虑晶体的对称性,以减少计算量。
通过上述步骤,有限位移法能够有效地计算晶体的声子谱,为研究材料的热力学性质、动力学稳定性等提供重要信息。

2. 绘制投影声子态密度

import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

# 读取文件
input_file1 = 'projected_dos.dat'  # 输入文件名
input_file2 = 'total_dos.dat'
output_file = 'tdosandpdos.dat'  # 输出文件名

# 读取数据
data = np.loadtxt(input_file1)
data2 = np.loadtxt(input_file2)

# 处理数据
# 第一列保持不变
col0 = data2[:,1]
col1 = data[:, 0]

# 第2-31列求和放到第二列
col2 = np.sum(data[:, 1:31], axis=1)

# 第32-37列求和放到第三列
col3 = np.sum(data[:, 31:37], axis=1)

# 第38-61列求和放到第四列
col4 = np.sum(data[:, 37:61], axis=1)

# 第62-133列求和放到第五列
col5 = np.sum(data[:, 61:133], axis=1)

# 将处理后的数据保存到新文件
processed_data = np.column_stack((col1, col2, col3, col4, col5,col0))
np.savetxt(output_file, processed_data, fmt='%.6f', delimiter='\t')


plt.figure(figsize=(10, 6), dpi=100)  # 增加 dpi 以提高分辨率

# 绘制图形,增强样式
plt.plot(processed_data[:, 0], processed_data[:, 1], 
         label='PDOS_Na', 
         linewidth=2,          # 更粗的线条
         color='#2ecc71',      # 柔和的绿色
         alpha=0.8)           # 轻微的透明度
plt.plot(processed_data[:, 0], processed_data[:, 2], 
         label='PDOS_X', 
         linewidth=2, 
         color='#e74c3c',      # 柔和的红色
         alpha=0.8)
plt.plot(processed_data[:, 0], processed_data[:, 3], 
         label='PDOS_Si', 
         linewidth=2, 
         color='#3498db',      # 柔和的蓝色
         alpha=0.8)
plt.plot(processed_data[:, 0], processed_data[:, 4], 
         label='PDOS_O', 
         linewidth=2, 
         color='#9b59b6',      # 紫色
         alpha=0.8)
plt.plot(processed_data[:, 0], processed_data[:, 5], 
         label='TDOS', 
         linewidth=2.5,        # TDOS 线条稍粗
         color='#34495e',      # 深灰色
         alpha=0.9)

# 自定义坐标轴
plt.xlabel('ω (THz)', fontsize=12, labelpad=10)  # 使用希腊字母 ω 表示频率
plt.ylabel('Number', fontsize=12, labelpad=10)
plt.title('Partial and Total Density of States', 
          fontsize=14, 
          pad=15, 
          fontweight='bold')

# 增强图例
plt.legend(
    fontsize=10,
    frameon=True,           # 添加边框
    framealpha=0.9,         # 轻微透明的边框
    edgecolor='gray',       # 边框颜色
    loc='best'              # 自动选择最佳位置
)

# 自定义网格(可选 - 浅色网格)
plt.grid(True, linestyle='--', alpha=0.3, color='gray')

# 调整边框(坐标轴边框)
ax = plt.gca()
ax.spines['top'].set_color('gray')
ax.spines['right'].set_color('gray')
ax.spines['left'].set_color('gray')
ax.spines['bottom'].set_color('gray')
ax.spines['top'].set_alpha(0.5)
ax.spines['right'].set_alpha(0.5)

# 添加次要刻度
plt.minorticks_on()

# 调整刻度参数
plt.tick_params(axis='both', which='major', labelsize=10)
plt.tick_params(axis='both', which='minor', length=2)

# 紧凑布局,防止标签被截断
plt.tight_layout()

# 保存高质量图像
plot_file = 'DOS.png'
plt.savefig(plot_file, dpi=300, bbox_inches='tight', facecolor='white')

7. ASE+grace

1. BFGS优化+NVTBerendsen分子动力学

NVTBerendsen在taut比较小时可能得不到正确的动力学结果 ,因此模拟还是要用nose Hoover

image-20250320144100597

from ase.io import read, write
from ase.optimize import BFGS
from ase.md.nvtberendsen import NVTBerendsen
from ase import units
from tensorpotential.calculator import grace_fm
import numpy as np
import os
from ase.md.velocitydistribution import ( 
    MaxwellBoltzmannDistribution,
    Stationary,
)

calc = grace_fm('GRACE-2L-OMAT')
atoms = read("POSCAR",format='vasp')
atoms.calc = calc

dyn = BFGS(atoms)
dyn.run(fmax=0.01)
 
temperature = 600
timestep = 2.0 * units.fs
taut = 100
trajectory = "atoms.traj"
logfile = "atoms.log"  
loginterval = 1
append_trajectory = False


MaxwellBoltzmannDistribution(atoms,temperature_K=temperature,force_temp = True)

Stationary(atoms)

md = NVTBerendsen(atoms=atoms,timestep=timestep,temperature_K = temperature,taut=taut,trajectory = trajectory,logfile=logfile,loginterval= loginterval,append_trajectory=append_trajectory)

md.run(250000)






#后处理

#将轨迹写入XDATCAR
atoms_xda = read('atoms.traj', index=':')
write('XDATCAR', atoms_xda)

os.remove('atoms.traj')



import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

input_file = 'atoms.log'

data = np.loadtxt(input_file,skiprows=1)

fig,(ax1,ax2) = plt.subplots(1,2,figsize=(10,6),dpi=100)

ax1.plot(data[:, 0], data[:, 1], 
         label='t-Energy', 
         linewidth=2,          # 更粗的线条
         color='#2ecc71',      # 柔和的绿色
         alpha=0.8)            # 轻微的透明度
ax1.set_xlabel('t (ps)')
ax1.set_ylabel('Energy (eV)')
ax1.set_title('t-Energy')
ax1.legend()
ax1.grid(True)

ax2.plot(data[:, 0], data[:, 4], 
         label='t-Temperature', 
         linewidth=2, 
         color='#e74c3c',      # 柔和的红色
         alpha=0.8)
ax2.set_xlabel('t (ps)')
ax2.set_ylabel('Temperature (K)')
ax2.set_title('t-Energy')
ax2.legend()
ax2.grid(True)


fig.tight_layout()

plot_file = 't-E-T.png'
fig.savefig(plot_file, dpi=300, bbox_inches='tight', facecolor='white')



#vaspkit处理
import subprocess

def run_vaspkit(task_id, selectype, element, skip_steps, frame_interval):
    # 构造VASPKIT命令
    command = f"(echo {task_id};echo {selectype};echo {element};echo {skip_steps};echo {frame_interval})| vaspkit"

    # 执行命令
    process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = process.communicate()

    # 输出结果
    if process.returncode == 0:
        print("VASPKIT执行成功!")
        print(stdout.decode())
    else:
        print("VASPKIT执行失败!")
        print(stderr.decode())

# 示例调用
task_id = 722
selectype = 1
element = "Na"
skip_steps = 5000
frame_interval = 1

run_vaspkit(task_id, selectype, element, skip_steps, frame_interval)

if os.path.exists('MSD.dat'):
    os.remove('XDATCAR')



2. 使用ASE中的Nose-Hoover NVT

from ase.io import read, write
from ase.optimize import BFGS
from ase.md.nose_hoover_chain import NoseHooverChainNVT
from ase import units
from tensorpotential.calculator import grace_fm
import numpy as np
import os
from ase.md.velocitydistribution import ( 
    MaxwellBoltzmannDistribution,
    Stationary,
)

calc = grace_fm('GRACE-2L-OMAT')
atoms = read("POSCAR",format='vasp')
atoms.calc = calc

dyn = BFGS(atoms)
dyn.run(fmax=0.01)
write("optimized_structure.vasp", atoms)
 
temperature = 600
timestep = 2.0 * units.fs
tdamp = 100
trajectory = "atoms.traj"
logfile = "atoms.log"  
loginterval = 1
append_trajectory = False


MaxwellBoltzmannDistribution(atoms,temperature_K=temperature,force_temp = True)

Stationary(atoms)

md1 = NoseHooverChainNVT(atoms=atoms,timestep=timestep,temperature_K = temperature,tdamp=tdamp,trajectory = None,logfile='equili.log',loginterval= loginterval,append_trajectory=append_trajectory)

md1.run(10000)

md2= NoseHooverChainNVT(atoms=atoms,timestep=timestep,temperature_K = temperature,tdamp=tdamp,trajectory = trajectory,logfile=logfile,loginterval= loginterval,append_trajectory=append_trajectory)

md2.run(250000)

8. Nose-Hoover和Berendsen的对比

更大的晶胞也需要更长的弛豫时间

  • Nose-Hoover tdamp= 200 vs Berendsen taut=100

    Nose-Hoover和Berendsen的动力学过程不同

    Nose-Hoover

    t-E-T

Berendsen

t-E-T

  • Nose-Hoover tdamp = 40 100 200 400 对比

    40

    t-E-T

​ 100

t-E-T

200

t-E-T

400

t-E-T

from deepseek

image-20250320172836556

image-20250320172853036

image-20250320172925841

image-20250320172947930

image-20250320173043216

9. 学习mattersim的phonon的方法

1. 包装ase的结构为phonon的格式

https://github.com/microsoft/mattersim/blob/main/src/mattersim/utils/phonon_utils.py

def to_phonopy_atoms(atoms: Atoms):
    """
    Transform ASE atoms object to Phonopy object
    Args:
        atoms (Atoms): ASE atoms object to provide lattice informations.
    """
    phonopy_atoms = PhonopyAtoms(
        symbols=atoms.get_chemical_symbols(),
        cell=atoms.get_cell(),
        masses=atoms.get_masses(),
        positions=atoms.get_positions(),
    )
    return phonopy_atoms


def to_ase_atoms(phonopy_atoms):
    """
    Transform Phonopy object to ASE atoms object
    Args:
        phonopy_atoms (Phonopy): Phonopy object to provide lattice informations.
    """
    atoms = Atoms(
        symbols=phonopy_atoms.symbols,
        cell=phonopy_atoms.cell,
        masses=phonopy_atoms.masses,
        positions=phonopy_atoms.positions,
        pbc=True,
    )
    return atoms

2. 修改mattersim以输出投影声子密度,并且指定固定的路径内的q点个数来匹配自己写的绘制声子谱的脚本(vasp模拟部分)(auto_band得到的q点个数在不同路径上不是固定的)

修改/lib/python3.12/site-packages/mattersim/applications/phonon.py

def compute_phonon_spectrum_dos(
        atoms: Atoms, phonon: Phonopy, k_point_mesh: Union[int, Iterable[int]]
    ):
        """
        Calculate phonon spectrum and DOS based on force constant matrix in
        phonon object

        Args:
            atoms (Atoms): ASE atoms object to provide lattice information
            phonon (Phonopy): Phonopy object which contains force constants matrix
            k_point_mesh (Union[int, Iterable[int]]): The qpoints number in First
                Brillouin Zone in three directions for DOS calculation.
        """
        print(f"Qpoints mesh for Brillouin Zone integration : {k_point_mesh}")
        phonon.run_mesh(k_point_mesh)
        print(
            "Dispersion relations using phonopy for "
            + str(atoms.symbols)
            + " ..."
            + "\n"
        )

        # plot phonon spectrum
        phonon.auto_band_structure(plot=True, write_yaml=True, with_eigenvectors=True).savefig(
            f"{str(atoms.symbols)}_phonon_band.png", dpi=300
        )
        phonon.auto_total_dos(plot=True, write_dat=True).savefig(
            f"{str(atoms.symbols)}_phonon_dos.png", dpi=300
        )

        # Save additional files
        phonon.save(settings={"force_constants": True})

主要修改了compute_force_constants 这个函数,替换为

# -*- coding: utf-8 -*-
import datetime
import os
from typing import Iterable, Union

import numpy as np
from ase import Atoms
from phonopy import Phonopy
from tqdm import tqdm
from seekpath import get_path
from phonopy.phonon.band_structure import get_band_qpoints_and_path_connections


from mattersim.utils.phonon_utils import (
    get_primitive_cell,
    to_ase_atoms,
    to_phonopy_atoms,
)
from mattersim.utils.supercell_utils import get_supercell_parameters


class PhononWorkflow(object):
    """
    This class is used to calculate the phonon dispersion relationship of
    material using phonopy
    """

    def __init__(
        self,
        atoms: Atoms,
        find_prim: bool = False,
        work_dir: str = None,
        amplitude: float = 0.01,
        supercell_matrix: np.ndarray = None,
        qpoints_mesh: np.ndarray = None,
        max_atoms: int = None,
        calc_spec: bool = True,
    ):
        """_summary

        Args:
            atoms (Atoms): ASE atoms object contains structure information and
                calculator.
            find_prim (bool, optional): If find the primitive cell and use it
                to calculate phonon. Default to False.
            work_dir (str, optional): workplace path to contain phonon result.
                Defaults to data + chemical_symbols + 'phonon'
            amplitude (float, optional): Magnitude of the finite difference to
                displace in force constant calculation, in Angstrom. Defaults
                to 0.01 Angstrom.
            supercell_matrix (nd.array, optional): Supercell matrix for constr
                -uct supercell, priority over than max_atoms. Defaults to None.
            qpoints_mesh (nd.array, optional): Qpoint mesh for IBZ integral,
                priority over than max_atoms. Defaults to None.
            max_atoms (int, optional): Maximum atoms number limitation for the
                supercell generation. If not set, will automatic generate super
                -cell based on symmetry. Defaults to None.
            calc_spec (bool, optional): If calculate the spectrum and check 
                imaginary frequencies. Default to True.
        """
        assert (
            atoms.calc is not None
        ), "PhononWorkflow only accepts ase atoms with an attached calculator"
        if find_prim:
            self.atoms = get_primitive_cell(atoms)
            self.atoms.calc = atoms.calc
        else:
            self.atoms = atoms
        if work_dir is not None:
            self.work_dir = work_dir
        else:
            current_datetime = datetime.datetime.now()
            formatted_datetime = current_datetime.strftime("%Y-%m-%d-%H-%M")
            self.work_dir = (
                f"{formatted_datetime}-{atoms.get_chemical_formula()}-phonon"
            )
        self.amplitude = amplitude
        if supercell_matrix is not None:
            if supercell_matrix.shape == (3, 3):
                self.supercell_matrix = supercell_matrix
            elif supercell_matrix.shape == (3,):
                self.supercell_matrix = np.diag(supercell_matrix)
            else:
                assert (
                    False
                ), "Supercell matrix must be an array (3,1) or a matrix (3,3)."
        else:
            self.supercell_matrix = supercell_matrix

        if qpoints_mesh is not None:
            assert qpoints_mesh.shape == (3,), "Qpoints mesh must be an array (3,1)."
            self.qpoints_mesh = qpoints_mesh
        else:
            self.qpoints_mesh = qpoints_mesh

        self.max_atoms = max_atoms
        self.calc_spec = calc_spec

    def compute_force_constants(self, atoms: Atoms, nrep_second: np.ndarray):
        """
        Calculate force constants

        Args:
            atoms (Atoms): ASE atoms object to provide lattice informations.
            nrep_second (np.ndarray): Supercell size used for 2nd force
                constant calculations.
        """
        print(f"Supercell matrix for 2nd force constants : \n{nrep_second}")
        # Generate phonopy object
        phonon = Phonopy(
            to_phonopy_atoms(atoms),
            supercell_matrix=nrep_second,
            primitive_matrix="auto",
            log_level=2,
        )

        # Generate displacements
        phonon.generate_displacements(distance=self.amplitude)

        # Compute force constants
        second_scs = phonon.supercells_with_displacements
        second_force_sets = []
        print("\n")
        print("Inferring forces for displaced atoms and computing fcs ...")
        for disp_second in tqdm(second_scs):
            pa_second = to_ase_atoms(disp_second)
            pa_second.calc = self.atoms.calc
            second_force_sets.append(pa_second.get_forces())

        phonon.forces = np.array(second_force_sets)
        phonon.produce_force_constants()
        phonon.symmetrize_force_constants()

        return phonon

    @staticmethod
    def compute_phonon_spectrum_dos(
        atoms: Atoms, phonon: Phonopy, k_point_mesh: Union[int, Iterable[int]]
    ):
        """
        Calculate phonon spectrum and DOS based on force constant matrix in
        phonon object

        Args:
            atoms (Atoms): ASE atoms object to provide lattice information
            phonon (Phonopy): Phonopy object which contains force constants matrix
            k_point_mesh (Union[int, Iterable[int]]): The qpoints number in First
                Brillouin Zone in three directions for DOS calculation.
        """
        print(f"Qpoints mesh for Brillouin Zone integration : {k_point_mesh}")
        phonon.run_mesh(k_point_mesh)
        print(
            "Dispersion relations using phonopy for "
            + str(atoms.symbols)
            + " ..."
            + "\n"
        )
        #phonon.auto_band_structure(plot=True, write_yaml=True, with_eigenvectors=False).savefig(
           # f"{str(atoms.symbols)}_auto_band.png", dpi=300) 
        
        lattice = atoms.cell
        scaled_positions = atoms.get_scaled_positions()
        atomic_numbers = atoms.get_atomic_numbers()
        path_data = get_path((lattice, scaled_positions, atomic_numbers))
        point_coords = path_data["point_coords"]
        band_segments = path_data["path"]

        from phonopy.phonon.band_structure import get_band_qpoints_and_path_connections
        raw_labels = []
        path = []
        connectionsall = []
        for start_label, end_label in band_segments:
            q_start = point_coords[start_label]
            q_end = point_coords[end_label]
            path.append([q_start,q_end])
            raw_labels.append(start_label)
            raw_labels.append(end_label)
            connectionsall.append('False')
        qpoints, connections = get_band_qpoints_and_path_connections(path, npoints=51)

        def convert_labels(raw_labels):
            latex_labels = []
            for label in raw_labels:
                if label.upper() == "GAMMA":
                    latex_labels.append("$\\Gamma$")
                else:
                    latex_labels.append(label)
            return latex_labels
        labels = convert_labels(raw_labels)
        phonon.run_band_structure(qpoints, path_connections=connections, labels=labels)
        bs = phonon.band_structure
        bs.write_yaml()
        plt = phonon.plot_band_structure()
        plt.savefig(f"{str(atoms.symbols)}_manual_band.png",dpi=300)


        phonon.auto_total_dos(plot=True, write_dat=True).savefig(
            f"{str(atoms.symbols)}_phonon_dos.png", dpi=300
        )
        phonon.auto_projected_dos(plot=True,write_dat=True).savefig(
            f"{str(atoms.symbols)}_phonon_projected_dos.png", dpi=300
        )
        phonon.save(settings={"force_constants": True})

    @staticmethod
    def check_imaginary_freq(phonon: Phonopy):
        """
        Check whether phonon has imaginary frequency.

        Args:
            phonon (Phonopy): Phonopy object which contains phonon spectrum frequency.
        """
        band_dict = phonon.get_band_structure_dict()
        frequencies = np.concatenate(
            [np.array(freq).flatten() for freq in band_dict["frequencies"]], axis=None
        )
        has_imaginary = False
        if np.all(np.array(frequencies) >= -0.299):
            pass
        else:
            print("Warning! Imaginary frequencies found!")
            has_imaginary = True

        return has_imaginary

    def run(self):
        """
        The entrypoint to start the workflow.
        """
        current_path = os.path.abspath(".")
        try:
            # check folder exists
            if not os.path.exists(self.work_dir):
                os.makedirs(self.work_dir)

            os.chdir(self.work_dir)

            try:
                # Generate supercell parameters based on optimized structures
                nrep_second, k_point_mesh = get_supercell_parameters(
                    self.atoms, self.supercell_matrix, self.qpoints_mesh, self.max_atoms
                )
            except Exception as e:
                print("Error whille generating supercell parameters:", e)
                raise

            try:
                # Calculate 2nd force constants
                phonon = self.compute_force_constants(self.atoms, nrep_second)
            except Exception as e:
                print("Error while computing force constants:", e)
                raise

            if self.calc_spec:
                try:
                    # Calculate phonon spectrum
                    self.compute_phonon_spectrum_dos(self.atoms, phonon, k_point_mesh)
                    # check whether has imaginary frequency
                    has_imaginary = self.check_imaginary_freq(phonon)
                except Exception as e:
                    print("Error while computing phonon spectrum and dos:", e)
                    raise
            else:
                has_imaginary = 'Not calculated, set calc_spec True'
                phonon.save(settings={"force_constants": True})

        except Exception as e:
            print("An error occurred during the Phonon workflow:", e)
            raise

        finally:
            os.chdir(current_path)

        return has_imaginary, phonon

转载请注明来源 有问题可通过github提交issue