thermoelasticsim.elastic.mechanics 源代码

# 文件名: mechanics.py
# 作者: Gilbert Young
# 修改日期: 2025-08-12
# 文件描述: 实现应力和应变计算器,包括基于 Lennard-Jones 势和EAM势的应力计算器。

import logging

import matplotlib as mpl
import numpy as np

from thermoelasticsim.interfaces.cpp_interface import CppInterface
from thermoelasticsim.utils.utils import TensorConverter

# 设置matplotlib的日志级别为WARNING,屏蔽字体调试信息
mpl.set_loglevel("WARNING")

# 配置我们自己的日志
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)


[文档] class StressCalculator: """应力张量计算器 处理三个张量贡献: 1. 动能张量: p_i⊗p_i/m_i 2. 维里张量: r_i⊗f_i 3. 晶格应变张量: ∂U/∂h·h^T """
[文档] def __init__(self): self.cpp_interface = CppInterface("stress_calculator") logger.debug("StressCalculator initialized")
[文档] def calculate_kinetic_stress(self, cell) -> np.ndarray: """ 计算纯动能应力张量 使用公式:σ_αβ^kinetic = -1/V * Σᵢ m_i v_iα v_iβ 在零温下此项为零 Parameters ---------- cell : Cell 包含原子的晶胞对象 Returns ------- np.ndarray 动能应力张量 (3x3) """ try: num_atoms = len(cell.atoms) velocities = cell.get_velocities() # shape: (num_atoms, 3) masses = np.array([atom.mass for atom in cell.atoms], dtype=np.float64) volume = cell.volume # 初始化动能应力张量 (3,3) kinetic_stress = np.zeros((3, 3), dtype=np.float64) # 动能应力项:-1/V * Σᵢ m_i v_iα v_iβ for i in range(num_atoms): for α in range(3): for β in range(3): kinetic_stress[α, β] -= ( masses[i] * velocities[i, α] * velocities[i, β] ) kinetic_stress /= volume return kinetic_stress except Exception as e: logger.error(f"Error in kinetic stress calculation: {e}") raise
[文档] def calculate_virial_stress(self, cell, potential) -> np.ndarray: """ 计算纯维里应力张量(原子间相互作用力项) 使用公式:σ_αβ^virial = -1/V * Σ(i<j) r_ij^α * F_ij^β 其中: - r_ij = r_i - r_j (从原子j指向原子i的向量) - F_ij是原子j对原子i的力 Parameters ---------- cell : Cell 包含原子的晶胞对象 potential : Potential 势能对象 Returns ------- np.ndarray 维里应力张量 (3x3) """ try: volume = cell.volume # 优先使用C++的EAM维里实现(若可用) virial_tensor = None try: if hasattr(potential, "cpp_interface"): libname = getattr(potential.cpp_interface, "_lib_name", None) num_atoms = len(cell.atoms) positions = np.ascontiguousarray( cell.get_positions(), dtype=np.float64 ) lattice = np.ascontiguousarray( cell.lattice_vectors, dtype=np.float64 ) if libname == "eam_al1" and hasattr( potential.cpp_interface, "calculate_eam_al1_virial" ): virial_tensor = ( potential.cpp_interface.calculate_eam_al1_virial( num_atoms, positions, lattice.flatten() ) ) elif libname == "eam_cu1" and hasattr( potential.cpp_interface, "calculate_eam_cu1_virial" ): virial_tensor = ( potential.cpp_interface.calculate_eam_cu1_virial( num_atoms, positions, lattice.flatten() ) ) except Exception as e_cpp: logger.debug( f"C++ EAM virial not available, fallback to Python: {e_cpp}" ) if virial_tensor is None: # 回退到Python实现(较慢)。注意:当前Python实现只实现了Al1参数。 # 对非Al1势(如Cu1)应优先启用C++后端;否则结果将不可靠。 virial_tensor = self._calculate_eam_virial_contribution(cell, potential) virial_stress = virial_tensor / volume return virial_stress except Exception as e: logger.error(f"Error in virial stress calculation: {e}") raise
[文档] def calculate_total_stress(self, cell, potential) -> np.ndarray: """ 计算总应力张量(动能+维里项) 使用公式:σ_αβ = -1/V * (Σᵢ m_i v_iα v_iβ + Σ(i<j) r_ij^α * F_ij^β) Parameters ---------- cell : Cell 包含原子的晶胞对象 potential : Potential 势能对象 Returns ------- np.ndarray 总应力张量 (3x3) """ try: kinetic_stress = self.calculate_kinetic_stress(cell) virial_stress = self.calculate_virial_stress(cell, potential) total_stress = kinetic_stress + virial_stress logger.debug(f"Total stress magnitude: {np.linalg.norm(total_stress):.2e}") return total_stress except Exception as e: logger.error(f"Error in total stress calculation: {e}") raise
def _calculate_eam_virial_contribution(self, cell, potential) -> np.ndarray: """ 计算EAM势的维里贡献 基于公式: -1/V * Σ(i<j) r_ij^α * F_ij^β 其中: - r_ij = r_i - r_j (原子i指向原子j的向量) - F_ij是原子j对原子i的力 Parameters ---------- cell : Cell 包含原子的晶胞对象 potential : Potential EAM势能对象 Returns ------- np.ndarray 维里贡献张量 (3x3) """ try: num_atoms = len(cell.atoms) positions = cell.get_positions() lattice = cell.lattice_vectors LT = lattice.T invLT = np.linalg.inv(LT) # 初始化维里张量 virial_tensor = np.zeros((3, 3), dtype=np.float64) # 首先计算电子密度 electron_density = np.zeros(num_atoms, dtype=np.float64) # 计算每个原子的电子密度 for i in range(num_atoms): for j in range(num_atoms): if i == j: continue # 计算原子间最小镜像距离向量(通用于三斜晶胞) rij_cart = positions[i] - positions[j] s = invLT @ rij_cart s -= np.round(s) rij = LT @ s r = np.linalg.norm(rij) if r <= 6.5: # EAM Al1的截断半径 electron_density[i] += self._psi(r) # 计算原子间相互作用力和维里贡献 for i in range(num_atoms): for j in range(i + 1, num_atoms): # j > i 避免重复计算 # 计算原子间最小镜像距离向量(通用于三斜晶胞) rij_cart = positions[i] - positions[j] s = invLT @ rij_cart s -= np.round(s) rij = LT @ s r = np.linalg.norm(rij) if r > 1e-6 and r <= 6.5: # 在截断半径内 # 计算EAM力的各个分量 d_phi = self._phi_grad(r) d_psi = self._psi_grad(r) d_F_i = self._F_grad(electron_density[i]) d_F_j = self._F_grad(electron_density[j]) # 计算力的大小 force_magnitude = -(d_phi + (d_F_i + d_F_j) * d_psi) # 计算力向量 F_ij(原子j对原子i的力) # 由于r_ij = r_i - r_j,力的方向是沿着-∇_i方向 # F_ij = force_magnitude * r_ij/|r_ij| force_ij = force_magnitude * (rij / r) # 计算维里贡献: -1/V * Σ(i<j) r_ij^α * F_ij^β # 由于只计算i<j,已经包含了求和的1/2因子 for α in range(3): for β in range(3): virial_tensor[α, β] -= rij[α] * force_ij[β] return virial_tensor except Exception as e: logger.error(f"Error in EAM virial contribution calculation: {e}") raise def _phi(self, r): """EAM Al1对势函数""" phi_val = 0.0 # 定义常数 a0 = 0.65196946237834 a1 = 7.6046051582736 a2 = -5.8187505542843 a3 = 1.0326940511805 b = [ 13.695567100510, -44.514029786506, 95.853674731436, -83.744769235189, 29.906639687889, ] c = [ -2.3612121457801, 2.5279092055084, -3.3656803584012, 0.94831589893263, -0.20965407907747, ] d = [ 0.24809459274509, -0.54072248340384, 0.46579408228733, -0.18481649031556, 0.028257788274378, ] # 区域1: [1.5, 2.3] if 1.5 <= r <= 2.3: phi_val += np.exp(a0 + a1 * r + a2 * r * r + a3 * r * r * r) # 区域2: (2.3, 3.2] if 2.3 < r <= 3.2: dr = 3.2 - r for n in range(5): phi_val += b[n] * (dr ** (n + 4)) # 区域3: (2.3, 4.8] if 2.3 < r <= 4.8: dr = 4.8 - r for n in range(5): phi_val += c[n] * (dr ** (n + 4)) # 区域4: (2.3, 6.5] if 2.3 < r <= 6.5: dr = 6.5 - r for n in range(5): phi_val += d[n] * (dr ** (n + 4)) return phi_val def _phi_grad(self, r): """EAM Al1对势函数的导数""" dphi = 0.0 # 定义常数 a0 = 0.65196946237834 a1 = 7.6046051582736 a2 = -5.8187505542843 a3 = 1.0326940511805 b = [ 13.695567100510, -44.514029786506, 95.853674731436, -83.744769235189, 29.906639687889, ] c = [ -2.3612121457801, 2.5279092055084, -3.3656803584012, 0.94831589893263, -0.20965407907747, ] d = [ 0.24809459274509, -0.54072248340384, 0.46579408228733, -0.18481649031556, 0.028257788274378, ] if r < 1.5: return -1e10 if 1.5 <= r <= 2.3: exp_term = np.exp(a0 + a1 * r + a2 * r * r + a3 * r * r * r) dphi += (a1 + 2.0 * a2 * r + 3.0 * a3 * r * r) * exp_term if 2.3 < r <= 3.2: dr = 3.2 - r for n in range(5): dphi += -(n + 4) * b[n] * (dr ** (n + 3)) if 2.3 < r <= 4.8: dr = 4.8 - r for n in range(5): dphi += -(n + 4) * c[n] * (dr ** (n + 3)) if 2.3 < r <= 6.5: dr = 6.5 - r for n in range(5): dphi += -(n + 4) * d[n] * (dr ** (n + 3)) return dphi def _psi(self, r): """EAM Al1电子密度贡献函数""" psi_val = 0.0 c_k = [ 0.00019850823042883, 0.10046665347629, 0.10054338881951, 0.099104582963213, 0.090086286376778, 0.0073022698419468, 0.014583614223199, -0.0010327381407070, 0.0073219994475288, 0.0095726042919017, ] r_k = [2.5, 2.6, 2.7, 2.8, 3.0, 3.4, 4.2, 4.8, 5.6, 6.5] for i in range(10): if r <= r_k[i]: psi_val += c_k[i] * ((r_k[i] - r) ** 4) return psi_val def _psi_grad(self, r): """EAM Al1电子密度贡献函数的导数""" dpsi = 0.0 c_k = [ 0.00019850823042883, 0.10046665347629, 0.10054338881951, 0.099104582963213, 0.090086286376778, 0.0073022698419468, 0.014583614223199, -0.0010327381407070, 0.0073219994475288, 0.0095726042919017, ] r_k = [2.5, 2.6, 2.7, 2.8, 3.0, 3.4, 4.2, 4.8, 5.6, 6.5] for i in range(10): if r <= r_k[i]: dpsi += -4.0 * c_k[i] * ((r_k[i] - r) ** 3) return dpsi def _F(self, rho): """EAM Al1嵌入能函数""" F_val = -np.sqrt(rho) # 对所有ρ的基本项 if rho >= 16.0: dr = rho - 16.0 D_n = [ -6.1596236428225e-5, 1.4856817073764e-5, -1.4585661621587e-6, 7.2242013524147e-8, -1.7925388537626e-9, 1.7720686711226e-11, ] for n in range(6): F_val += D_n[n] * (dr ** (n + 4)) return F_val def _F_grad(self, rho): """EAM Al1嵌入能函数的导数""" dF = -0.5 / np.sqrt(rho) # 对所有ρ的基本项 if rho >= 16.0: dr = rho - 16.0 D_n = [ -6.1596236428225e-5, 1.4856817073764e-5, -1.4585661621587e-6, 7.2242013524147e-8, -1.7925388537626e-9, 1.7720686711226e-11, ] for n in range(6): dF += (n + 4) * D_n[n] * (dr ** (n + 3)) return dF
[文档] def calculate_finite_difference_stress( self, cell, potential, dr=1e-6 ) -> np.ndarray: """ 计算有限差分应力张量(基于能量导数) Parameters ---------- cell : Cell 包含原子的晶胞对象 potential : Potential 势能对象,用于计算能量 dr : float, optional 形变量的步长, 默认值为1e-6 Returns ------- np.ndarray 有限差分应力张量 (3x3) """ try: # 初始化能量导数矩阵 dUdh = np.zeros((3, 3), dtype=np.float64) # 保存原始状态的深拷贝 original_cell = cell.copy() for i in range(3): for j in range(3): # 正向形变矩阵 deformation = np.eye(3) deformation[i, j] += dr # 负向形变矩阵 deformation_negative = np.eye(3) deformation_negative[i, j] -= dr # 应用正向形变到原始_cell的副本 deformed_cell_plus = original_cell.copy() deformed_cell_plus.apply_deformation(deformation) energy_plus = potential.calculate_energy(deformed_cell_plus) # 应用负向形变到原始_cell的副本 deformed_cell_minus = original_cell.copy() deformed_cell_minus.apply_deformation(deformation_negative) energy_minus = potential.calculate_energy(deformed_cell_minus) # 计算能量导数 dUdh[i, j] = (energy_plus - energy_minus) / (2 * dr) # 转换为应力张量 finite_diff_stress = dUdh / original_cell.volume return finite_diff_stress except Exception as e: logger.error(f"Error in finite difference stress calculation: {e}") raise
[文档] def calculate_lattice_stress(self, cell, potential, dr=1e-6) -> np.ndarray: """为保持向后兼容性的别名方法 - 指向有限差分应力方法""" return self.calculate_finite_difference_stress(cell, potential, dr)
[文档] def get_all_stress_components(self, cell, potential) -> dict[str, np.ndarray]: """ 计算应力张量的所有分量 Parameters ---------- cell : Cell 包含原子的晶胞对象 potential : Potential 势能对象 Returns ------- Dict[str, np.ndarray] 包含应力张量分量的字典,键包括: - "kinetic": 动能应力张量 - "virial": 维里应力张量 - "total": 总应力张量(kinetic + virial) - "finite_diff": 有限差分应力张量(用于验证) """ try: components = {} # 计算各个分量 kinetic_stress = self.calculate_kinetic_stress(cell) virial_stress = self.calculate_virial_stress(cell, potential) total_stress = self.calculate_total_stress(cell, potential) finite_diff_stress = self.calculate_finite_difference_stress( cell, potential ) # 标准键名 components["kinetic"] = kinetic_stress components["virial"] = virial_stress components["total"] = total_stress components["finite_diff"] = finite_diff_stress return components except Exception as e: logger.error(f"Error calculating stress tensors: {e}") raise
[文档] def compute_stress(self, cell, potential): """ 计算应力张量(使用总应力作为主要方法) Parameters ---------- cell : Cell 包含原子的晶胞对象 potential : Potential 势能对象 Returns ------- np.ndarray 3x3 应力张量矩阵(总应力 = 动能 + 维里) """ # 使用总应力作为主要计算方法 return self.calculate_total_stress(cell, potential)
# 向后兼容的别名方法
[文档] def calculate_virial_kinetic_stress(self, cell, potential) -> np.ndarray: """向后兼容别名 - 指向总应力方法""" return self.calculate_total_stress(cell, potential)
[文档] def calculate_stress_basic(self, cell, potential) -> np.ndarray: """向后兼容别名 - 指向总应力方法""" return self.calculate_total_stress(cell, potential)
[文档] def validate_tensor_symmetry( self, tensor: np.ndarray, tolerance: float = 1e-10 ) -> bool: """ 验证应力张量是否对称 Parameters ---------- tensor : np.ndarray 应力张量 (3x3) tolerance : float, optional 对称性的容差, 默认值为1e-10 Returns ------- bool 如果对称则为True, 否则为False """ if tensor.shape != (3, 3): logger.error(f"Tensor shape is {tensor.shape}, expected (3, 3).") return False is_symmetric = np.allclose(tensor, tensor.T, atol=tolerance) if not is_symmetric: logger.warning("Stress tensor is not symmetric.") return is_symmetric
# 废弃的子类 # class StressCalculatorLJ(StressCalculator): # """ # 基于 Lennard-Jones 势的应力计算器 # Parameters # ---------- # cell : Cell # 包含原子的晶胞对象 # potential : Potential # Lennard-Jones 势能对象 # """ # def __init__(self): # self.cpp_interface = CppInterface("stress_calculator") # def compute_stress(self, cell, potential): # """ # 计算 Lennard-Jones 势的应力张量 # Parameters # ---------- # cell : Cell # 包含原子的晶胞对象 # potential : Potential # Lennard-Jones 势能对象 # Returns # ------- # numpy.ndarray # 3x3 应力张量矩阵 # """ # # 计算并更新原子力 # potential.calculate_forces(cell) # # 获取相关物理量 # volume = cell.calculate_volume() # atoms = cell.atoms # num_atoms = len(atoms) # positions = np.array( # [atom.position for atom in atoms], dtype=np.float64 # ) # (num_atoms, 3) # velocities = np.array( # [atom.velocity for atom in atoms], dtype=np.float64 # ) # (num_atoms, 3) # forces = cell.get_forces() # cell.get_forces() 返回 (num_atoms, 3) # masses = np.array([atom.mass for atom in atoms], dtype=np.float64) # box_lengths = cell.get_box_lengths() # (3,) # # 初始化应力张量数组 # stress_tensor = np.zeros((3, 3), dtype=np.float64) # # 调用 C++ 接口计算应力张量 # self.cpp_interface.compute_stress( # num_atoms, # positions, # velocities, # forces, # masses, # volume, # box_lengths, # stress_tensor, # ) # # 这里因为stress_tensor已经是(3,3),无需再次reshape # return stress_tensor # class StressCalculatorEAM(StressCalculator): # """ # 基于 EAM 势的应力计算器 # 计算EAM势下的应力张量,包括: # 1. 对势项的贡献 # 2. 电子密度的贡献 # 3. 嵌入能的贡献 # Parameters # ---------- # None # """ # def __init__(self): # """初始化EAM应力计算器""" # self.cpp_interface = CppInterface("stress_calculator") # def compute_stress(self, cell, potential): # """ # 计算 EAM 势的应力张量 # Parameters # ---------- # cell : Cell # 包含原子的晶胞对象 # potential : EAMAl1Potential # EAM 势能对象 # Returns # ------- # numpy.ndarray # 3x3 应力张量矩阵,单位为 eV/ų # Notes # ----- # EAM势的应力张量计算包括: # 1. 对势部分的应力贡献 # 2. 由电子密度梯度产生的应力贡献 # 3. 嵌入能导致的应力贡献 # """ # # 计算并更新原子力 # potential.calculate_forces(cell) # # 获取相关物理量 # volume = cell.calculate_volume() # atoms = cell.atoms # num_atoms = len(atoms) # positions = np.array([atom.position for atom in atoms], dtype=np.float64) # velocities = np.array([atom.velocity for atom in atoms], dtype=np.float64) # forces = cell.get_forces() # 从cell获取更新后的力 # masses = np.array([atom.mass for atom in atoms], dtype=np.float64) # box_lengths = cell.get_box_lengths() # # 初始化应力张量数组 # stress_tensor = np.zeros((3, 3), dtype=np.float64) # # 调用C++接口计算EAM应力张量 # self.cpp_interface.compute_stress( # num_atoms, # positions, # velocities, # forces, # masses, # volume, # box_lengths, # stress_tensor, # ) # return stress_tensor
[文档] class StrainCalculator: """ 应变计算器类 Parameters ---------- F : numpy.ndarray 3x3 变形矩阵 """
[文档] def compute_strain(self, F): """ 计算应变张量并返回 Voigt 表示法 Parameters ---------- F : numpy.ndarray 3x3 变形矩阵 Returns ------- numpy.ndarray 应变向量,形状为 (6,) """ strain_tensor = 0.5 * (F + F.T) - np.identity(3) # 线性应变张量 # 转换为 Voigt 表示法 strain_voigt = TensorConverter.to_voigt(strain_tensor) # 对剪切分量乘以 2 strain_voigt[3:] *= 2 return strain_voigt