Files
solidstate-tools/MSD/utils/MSD.py
2025-09-23 18:32:34 +08:00

275 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import numpy as np
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import linregress
def plot_and_get_final_msd(file_path, ion_name='Ion',timestep_ps = 0.2):
"""
读取LAMMPS输出的MSD数据文件绘制MSD-时间图并返回最后一个MSD值。
参数:
file_path (str): msd_*.dat文件的路径。
ion_name (str, optional): 离子的名称,用于图表标题和标签。默认为'Ion'
返回:
float: 文件中记录的最后一个MSD值。
"""
# LAMMPS输出的MSD文件通常会跳过前几行注释使用np.loadtxt可以轻松处理
try:
# 尝试直接读取通常第一列是Timestep第四列是总MSD
# 文件格式: Timestep, Time, MSD_x, MSD_y, MSD_z, MSD_total
# 但fix ave/time的输出是: Timestep, c_msd[1], c_msd[2], c_msd[3], c_msd[4]
# c_msd[4] 就是总MSD
data = np.loadtxt(file_path, comments='#')
timesteps = data[:, 0]
msd_values = data[:, -1] # 最后一列通常是总MSD
except Exception as e:
print(f"读取文件时出错: {e}")
print("请检查文件格式是否正确。")
return None
# 从in.lmp文件我们知道时间步长是0.001 ps
time_ps = timesteps * timestep_ps
# --- 绘图 ---
plt.figure(figsize=(10, 6))
plt.plot(time_ps/1000, msd_values, label=f'{ion_name} MSD')
plt.xlabel('Time (ps)', fontsize=14)
plt.ylabel('MSD (Ų)', fontsize=14)
plt.title(f'Mean Squared Displacement (MSD) of {ion_name}', fontsize=16)
plt.grid(True)
plt.legend()
plt.show()
# --- 输出并返回最终值 ---
final_msd_value = msd_values[-1]
final_time = time_ps[-1]
print(f"文件路径: {file_path}")
print(f"总模拟时间: {final_time/1000:.2f} ps")
print(f"最终MSD值: {final_msd_value:.4f} Ų")
return final_msd_value
def calculate_conductivity_from_msd(
msd_file_path,
log_file_path,
ion_name,
charge,
num_ions,
fit_start_fraction=0.5,
fit_end_fraction=0.5
):
"""
从MSD数据计算电导率 (v2)。
自动从 log.lammps 文件提取温度和体积。
参数:
msd_file_path (str): msd_*.dat 文件的路径。
log_file_path (str): log.lammps 文件的路径。
ion_name (str): 离子名称 (例如 'Li⁺')。
charge (int): 离子的电荷数 (例如 Li⁺ 为 1)。
num_ions (int): 模拟盒子中该离子的总数量。
fit_fraction (float): 用于线性拟合的数据比例 (使用最后部分)。
返回:
dict: 包含计算结果的字典。
"""
# --- 1. 从 log 文件自动提取参数 ---
sim_params = extract_params_from_log(log_file_path)
if not sim_params or not sim_params["avg_volume"] or not sim_params["avg_temp"]:
print("无法从 log 文件获取必要参数,计算中止。")
return None
temp_K = sim_params["avg_temp"]
volume_A3 = sim_params["avg_volume"]
# --- 2. 读取和处理 MSD 数据 ---
try:
data = np.loadtxt(msd_file_path, comments='#')
timesteps = data[:, 0]
msd_values = data[:, -1] # 总MSD (Ų)
except Exception as e:
print(f"读取文件 '{msd_file_path}' 时出错: {e}")
return None
timestep_ps = 0.001 # 时间步长 (ps)
time_ps = timesteps * timestep_ps
# --- 3. 线性拟合计算扩散系数 ---
fit_start_index = int(len(time_ps) * fit_start_fraction)
fit_end_index = int(len(time_ps) * fit_end_fraction)
fit_time_ps = time_ps[fit_start_index:fit_end_index]
fit_msd_values = msd_values[fit_start_index:fit_end_index]
if len(fit_time_ps) < 2:
print("错误: 用于拟合的数据点不足 (少于2个),无法进行线性回归。")
return None
slope, intercept, r_value, _, _ = linregress(fit_time_ps, fit_msd_values)
diff_coeff_A2_ps = slope / 6
diff_coeff_cm2_s = diff_coeff_A2_ps * 1e-4
diff_coeff_m2_s = diff_coeff_A2_ps * 1e-8
# --- 4. 计算电导率 (Nernst-Einstein) ---
e_charge = 1.60217663e-19 # (C)
kB = 1.380649e-23 # (J/K)
q = charge * e_charge
n = num_ions / (volume_A3 * 1e-30) # (m⁻³)
conductivity_S_m = (n * q ** 2 * diff_coeff_m2_s) / (kB * temp_K)
# --- 5. 可视化 ---
plt.figure(figsize=(12, 7))
plt.plot(time_ps, msd_values, 'b.', markersize=4, label='MSD Data')
fit_line = slope * fit_time_ps + intercept
plt.plot(fit_time_ps, fit_line, 'r-', linewidth=3, label=f'Linear Fit (R² = {r_value ** 2:.4f})')
plt.xlabel('Time (ps)', fontsize=14)
plt.ylabel('MSD (Ų)', fontsize=14)
plt.title(f'MSD Analysis for {ion_name}', fontsize=16)
plt.legend(fontsize=12)
plt.grid(True)
info_text = (
f"Diffusion Coefficient (D): {diff_coeff_cm2_s:.2e} cm²/s\n"
f"Conductivity (σ): {conductivity_S_m:.4f} S/m\n"
f"Avg Temp: {temp_K:.2f} K\n"
f"Avg Volume: {volume_A3:.2f} ų"
)
plt.text(0.05, 0.95, info_text, transform=plt.gca().transAxes, fontsize=12,
verticalalignment='top', bbox=dict(boxstyle='round,pad=0.5', fc='wheat', alpha=0.5))
plt.show()
# --- 6. 返回结果 ---
results = {
"ion": ion_name,
"diffusion_coefficient_cm2/s": diff_coeff_cm2_s,
"conductivity_S/m": conductivity_S_m,
"fit_R2": r_value ** 2,
"avg_temp_K": temp_K,
"avg_volume_A3": volume_A3
}
print("--- 最终计算结果 ---")
for key, value in results.items():
print(f"{key}: {value}")
return results
def extract_params_from_log(log_file_path):
"""
从 log.lammps 文件中提取 NVT 生产阶段的平均参数。
参数:
log_file_path (str): log.lammps 文件的路径。
返回:
dict: 包含提取参数的字典 (avg_temp, avg_volume, total_atoms),如果失败则返回 None。
"""
try:
with open(log_file_path, 'r',encoding='utf-8',errors='ignore') as f:
lines = f.readlines()
except FileNotFoundError:
print(f"错误: log 文件 '{log_file_path}' 未找到。")
return None
# 初始化变量
params = {
"avg_temp": None,
"avg_volume": None,
"total_atoms": None
}
# --- 1. 提取总原子数 ---
# 通常在文件开头附近
for line in lines:
if "atoms" in line and "atom types" in line:
parts = line.split()
params["total_atoms"] = int(parts[0])
break # 找到后就停止
# --- 2. 定位并提取 NVT 生产阶段的热力学数据 ---
# 我们通过寻找 "run 2000000" 来定位生产阶段的开始
# 这是根据您提供的 in.lmp 文件
nvt_data_lines = []
in_nvt_section = False
for i, line in enumerate(lines):
# 寻找 NVT 生产阶段的开始标志
if "fix nvt_prod all nvt" in line:
# 找到 fix 命令后,热力学数据头通常在几行之后
# 我们从下一行的 "run" 命令开始找
if "run 2000000" in lines[i + 1]:
# 再往下找 "Step Time Temp..." 这样的表头
for j in range(i + 2, len(lines)):
if lines[j].strip().startswith("Step"):
header = lines[j].split()
in_nvt_section = True
break
if in_nvt_section:
continue # 跳转到下一个循环,开始读取数据
# 如果在 NVT 区域,并且行看起来像数据行,则读取
if in_nvt_section:
# 数据行以数字开头,并且列数与表头匹配
parts = line.strip().split()
if parts and parts[0].isdigit() and len(parts) == len(header):
nvt_data_lines.append(parts)
# "Loop time" 标志着一个 run 命令的结束
elif line.strip().startswith("Loop time"):
in_nvt_section = False
break # 生产阶段数据读取完毕
if not nvt_data_lines:
print("警告: 未能在 log 文件中找到 NVT 生产阶段的热力学数据。")
print("请检查 in.lmp 中的 run 命令是否为 'run 2000000'")
return params # 即使没找到,也可能返回原子数
# --- 3. 计算平均值 ---
# 将数据转换为 numpy 数组以便于计算
try:
nvt_data = np.array(nvt_data_lines).astype(float)
except ValueError as e:
print("错误:在将日志数据转换为数字时失败。")
print("这通常意味着数据行中混入了非数字字符。")
print(f"原始错误信息: {e}")
# 可以打印出有问题的数据行来帮助调试
# for i, row in enumerate(nvt_data_lines):
# try:
# [float(x) for x in row]
# except ValueError:
# print(f"问题行号 {i}: {row}")
# break
return params
# 根据表头找到 Temp 和 Volume 所在的列索引
try:
temp_col = header.index("Temp")
vol_col = header.index("Volume")
except ValueError as e:
print(f"错误: 在日志表头中找不到列: {e}")
return params
print(nvt_data[1:5,temp_col])
print(nvt_data[1:5,vol_col])
params["avg_temp"] = np.mean(nvt_data[1:20, temp_col])
params["avg_volume"] = np.mean(nvt_data[1:20, vol_col])
print("--- 从 log.lammps 提取的参数 ---")
print(f"总原子数: {params['total_atoms']}")
print(f"NVT阶段平均温度: {params['avg_temp']:.2f} K")
print(f"NVT阶段平均体积: {params['avg_volume']:.4f} ų")
print("------------------------------------")
return params