thermoelasticsim.elastic.deformation_method.zero_temp 源代码
#!/usr/bin/env python3
r"""
零温显式形变法弹性常数计算模块
该模块实现零温条件下通过显式形变法计算弹性常数。采用三层架构:
底层结构弛豫 → 中层工作流管理 → 高层数据求解
基本原理:
1. 制备无应力基态
2. 施加微小形变
3. 优化原子位置
4. 测量应力响应
5. 线性拟合求解弹性常数
数学基础:
胡克定律的张量形式::math:`\\sigma = C : \\varepsilon`
在Voigt记号下::math:`\\sigma = C \\cdot \\varepsilon`
其中:
- :math:`\\sigma`:应力向量 (6×1)
- :math:`C`:弹性常数矩阵 (6×6)
- :math:`\\varepsilon`:应变向量 (6×1)
.. moduleauthor:: Gilbert Young
.. created:: 2024-10-20
.. modified:: 2025-07-11
.. version:: 4.0.0
"""
import logging
from dataclasses import dataclass
from typing import Any
import numpy as np
from thermoelasticsim.core.structure import Atom, Cell
from thermoelasticsim.potentials import Potential
from thermoelasticsim.utils.optimizers import (
BFGSOptimizer,
CGOptimizer,
LBFGSOptimizer,
)
from thermoelasticsim.utils.utils import EV_TO_GPA, TensorConverter
logger = logging.getLogger(__name__)
[文档]
@dataclass
class DeformationResult:
"""
单次形变计算结果的数据容器
Attributes
----------
strain_voigt : numpy.ndarray
Voigt记号应变向量,形状 (6,)
stress_voigt : numpy.ndarray
Voigt记号应力向量,形状 (6,)
converged : bool
结构优化是否收敛
deformation_matrix : numpy.ndarray
形变矩阵F,形状 (3,3)
"""
strain_voigt: np.ndarray
stress_voigt: np.ndarray
converged: bool
deformation_matrix: np.ndarray
[文档]
class StructureRelaxer:
"""
结构弛豫计算引擎
负责执行结构优化计算,包括完全弛豫和内部弛豫两种模式。
Parameters
----------
optimizer_type : str, optional
优化器类型,默认 'L-BFGS'
optimizer_params : dict, optional
优化器参数字典
Attributes
----------
optimizer_type : str
使用的优化器类型
optimizer_params : dict
优化器配置参数
Notes
-----
两种弛豫模式的区别:
- **完全弛豫**:同时优化原子位置和晶胞参数,用于制备无应力基态
- **内部弛豫**:仅优化原子位置,保持晶胞形状固定,用于形变后的平衡
Examples
--------
>>> relaxer = StructureRelaxer()
>>> # 完全弛豫制备基态
>>> converged = relaxer.full_relax(cell, potential)
>>> # 形变后内部弛豫
>>> converged = relaxer.internal_relax(deformed_cell, potential)
"""
[文档]
def __init__(
self,
optimizer_type: str = "L-BFGS",
optimizer_params: dict[str, Any] | None = None,
supercell_dims: tuple[int, int, int] | None = None,
trajectory_recorder=None,
):
"""
初始化结构弛豫器
Parameters
----------
optimizer_type : str, optional
优化器类型,支持 'L-BFGS', 'BFGS', 'GD',默认 'L-BFGS'
optimizer_params : dict, optional
优化器参数,如果为None则使用默认参数
supercell_dims : tuple, optional
超胞维度(nx, ny, nz),用于正确显示等效单胞参数
trajectory_recorder : ElasticTrajectoryRecorder, optional
轨迹记录器,用于记录优化过程中的系统状态
Raises
------
ValueError
如果指定了不支持的优化器类型
"""
# 验证优化器类型
if optimizer_type not in ["L-BFGS", "BFGS", "GD"]:
raise ValueError(f"不支持的优化器类型: {optimizer_type}")
self.optimizer_type = optimizer_type
self.supercell_dims = supercell_dims
# 设置默认参数
default_params = {
"ftol": 1e-9, # 函数收敛阈值
"gtol": 1e-7, # 梯度收敛阈值
"maxiter": 200000, # 最大迭代次数(增加以应对剪切变形)
}
if optimizer_params is None:
self.optimizer_params = default_params
else:
# 合并用户参数和默认参数,但排除optimizer_type等不是LBFGSOptimizer参数的键
filtered_params = {
k: v
for k, v in optimizer_params.items()
if k not in ["optimizer_type", "optimizer_params"]
}
self.optimizer_params = {**default_params, **filtered_params}
logger.debug(f"初始化StructureRelaxer,优化器: {optimizer_type}")
logger.debug(f"优化器参数: {self.optimizer_params}")
# 设置轨迹记录器
self.trajectory_recorder = trajectory_recorder
[文档]
def full_relax(self, cell: Cell, potential: Potential) -> bool:
r"""
执行完全弛豫:同时优化原子位置和晶胞参数
完全弛豫用于制备无应力基态,是零温弹性常数计算的第一步。
通过同时优化原子位置和晶胞参数,消除系统内应力。
Parameters
----------
cell : Cell
待优化的晶胞对象,会被直接修改
potential : Potential
势能函数对象
Returns
-------
bool
优化是否成功收敛
Notes
-----
数学表述:
.. math::
\\min_{r,h} E(r,h) \\quad \\text{s.t.} \\quad \\sigma = 0
其中:
- :math:`r`:原子位置
- :math:`h`:晶胞参数
- :math:`E`:总能量
- :math:`\\sigma`:应力张量
Examples
--------
>>> relaxer = StructureRelaxer()
>>> converged = relaxer.full_relax(cell, potential)
>>> if converged:
... print("成功制备无应力基态")
"""
logger.info("开始完全弛豫:优化原子位置和晶胞参数")
# 记录初始能量用于监控
initial_energy = potential.calculate_energy(cell)
logger.debug(f"初始总能量: {initial_energy:.6f} eV")
# 完全弛豫需要同时优化晶胞参数,当前仅 LBFGS 支持变胞。
# 若用户传入 BFGS/GD,这里忽略并使用 LBFGS(记录信息)。
if self.optimizer_type != "L-BFGS":
logger.info(
"完全弛豫需要变胞,已自动使用 L-BFGS(忽略 optimizer_type=%s)",
self.optimizer_type,
)
optimizer = LBFGSOptimizer(
supercell_dims=self.supercell_dims, **self.optimizer_params
)
# 执行完全弛豫(relax_cell=True 表示同时优化晶胞)
converged, _ = optimizer.optimize(cell, potential, relax_cell=True)
# 失败时尝试一次更宽松/更耐心的回退(提高迭代数)
if not converged:
fallback_params = {**self.optimizer_params}
fallback_params["maxiter"] = max(
2 * self.optimizer_params.get("maxiter", 1000), 10000
)
logger.warning(
"完全弛豫未收敛,尝试回退设置: maxiter=%d", fallback_params["maxiter"]
)
optimizer = LBFGSOptimizer(
supercell_dims=self.supercell_dims, **fallback_params
)
converged, _ = optimizer.optimize(cell, potential, relax_cell=True)
# 记录优化结果
final_energy = potential.calculate_energy(cell)
energy_change = final_energy - initial_energy
logger.info(f"完全弛豫{'成功' if converged else '失败'}")
logger.debug(f"最终总能量: {final_energy:.6f} eV")
logger.debug(f"能量变化: {energy_change:.6f} eV")
# 收敛性警告
if not converged:
logger.warning("完全弛豫未收敛,可能影响后续计算精度")
return converged
[文档]
def internal_relax(self, cell: Cell, potential: Potential) -> bool:
r"""
执行内部弛豫:仅优化原子位置,保持晶胞形状固定
内部弛豫用于形变后的结构优化,在保持宏观形变的前提下,
寻找原子的最优位置配置。
Parameters
----------
cell : Cell
待优化的晶胞对象,会被直接修改
potential : Potential
势能函数对象
Returns
-------
bool
优化是否成功收敛
Notes
-----
数学表述:
.. math::
\\min_{r} E(r,h_{\\text{fixed}}) \\quad \\text{s.t.} \\quad h = \\text{const}
其中:
- :math:`r`:原子位置(优化变量)
- :math:`h_{\\text{fixed}}`:固定的晶胞参数
- :math:`E`:总能量
Examples
--------
>>> relaxer = StructureRelaxer()
>>> # 先施加形变
>>> cell.apply_deformation(deformation_matrix)
>>> # 再内部弛豫
>>> converged = relaxer.internal_relax(cell, potential)
"""
logger.debug("开始内部弛豫:仅优化原子位置")
# 记录初始能量
initial_energy = potential.calculate_energy(cell)
logger.debug(f"形变后初始能量: {initial_energy:.6f} eV")
# 记录弛豫前状态到轨迹
if self.trajectory_recorder:
initial_stress = cell.calculate_stress_tensor(potential)
self.trajectory_recorder.record_deformation_step(
cell,
getattr(self.trajectory_recorder, "current_strain", 0.0),
"before_internal_relax",
stress_tensor=initial_stress,
energy=initial_energy,
converged=False,
)
# 内部弛豫(固定晶胞)优先采用用户指定的优化器,推荐 BFGS。
def _build_optimizer(opt_type: str):
params = self.optimizer_params
if opt_type == "BFGS":
tol = params.get(
"tol",
min(params.get("ftol", 1e-9), params.get("gtol", 1e-7)),
)
maxiter = params.get("maxiter", 10000)
return BFGSOptimizer(tol=tol, maxiter=maxiter)
if opt_type == "L-BFGS":
# 传递所有参数,包括maxls和maxfun
return LBFGSOptimizer(
supercell_dims=self.supercell_dims,
**params, # 传递所有参数
)
elif opt_type == "BFGS":
return BFGSOptimizer(
tol=params.get("gtol", 1e-7),
maxiter=params.get("maxiter", 10000),
)
elif opt_type == "CG":
return CGOptimizer(
tol=params.get("gtol", 1e-7),
maxiter=params.get("maxiter", 10000),
)
raise ValueError(f"未知优化器类型: {opt_type}")
# 构建回退序列
primary = self.optimizer_type
if primary == "BFGS" or primary == "L-BFGS":
sequence = ["L-BFGS", "BFGS", "CG", "BFGS"]
else:
sequence = ["L-BFGS", "BFGS", "CG"]
converged = False
for idx, opt_type in enumerate(sequence):
optimizer = (
_build_optimizer(opt_type)
if opt_type != "CG"
else CGOptimizer(
tol=self.optimizer_params.get("gtol", 1e-6),
maxiter=self.optimizer_params.get("maxiter", 10000),
)
)
logger.debug(
"内部弛豫尝试优化器: %s (%d/%d)", opt_type, idx + 1, len(sequence)
)
if opt_type == "L-BFGS":
converged, _ = optimizer.optimize(cell, potential, relax_cell=False)
else:
converged, _ = optimizer.optimize(cell, potential)
if converged:
break
else:
logger.warning("内部弛豫使用 %s 未收敛,进入回退。", opt_type)
# 记录优化结果
final_energy = potential.calculate_energy(cell)
energy_change = final_energy - initial_energy
# 记录弛豫后状态到轨迹
if self.trajectory_recorder:
final_stress = cell.calculate_stress_tensor(potential)
self.trajectory_recorder.record_deformation_step(
cell,
getattr(self.trajectory_recorder, "current_strain", 0.0),
"after_internal_relax",
stress_tensor=final_stress,
energy=final_energy,
converged=converged,
)
logger.debug(f"内部弛豫{'成功' if converged else '失败'}")
logger.debug(f"最终能量: {final_energy:.6f} eV")
logger.debug(f"能量变化: {energy_change:.6f} eV")
# 收敛性警告
if not converged:
logger.warning("内部弛豫未收敛,可能影响应力计算精度")
return converged
[文档]
def uniform_lattice_relax(self, cell: Cell, potential: Potential) -> bool:
r"""
等比例晶格弛豫:只优化晶格常数,保持原子相对位置不变
这种弛豫方式特别适合基态制备,既避免了原子位置的数值噪声,
又保持了晶体结构的完美对称性,同时计算效率极高。
Parameters
----------
cell : Cell
待优化的晶胞对象,会被直接修改
potential : Potential
势能函数对象
Returns
-------
bool
优化是否成功收敛
Notes
-----
数学表述:
.. math::
\\min_{s} E(r_{\\text{scaled}}, s \\cdot L) \\quad \\text{其中} \\quad r_{\\text{scaled}} = s \\cdot r_{\\text{frac}} \\cdot L
其中:
- :math:`s`:等比例缩放因子(唯一优化变量)
- :math:`r_{\\text{frac}}`:固定的分数坐标
- :math:`L`:原始晶格矢量矩阵
- :math:`E`:总能量
优势:
- 自由度仅为1,计算极快
- 保持晶体结构完美对称性
- 避免原子位置数值噪声
- 适合各种晶系(不限于FCC)
Examples
--------
>>> relaxer = StructureRelaxer()
>>> converged = relaxer.uniform_lattice_relax(cell, potential)
>>> if converged:
... print("成功优化晶格常数,保持结构对称性")
"""
logger.info("开始等比例晶格弛豫:只优化晶格常数")
# 记录初始状态
initial_energy = potential.calculate_energy(cell)
original_lattice = cell.lattice_vectors.copy()
original_fractional_coords = cell.get_fractional_coordinates()
logger.debug(f"初始总能量: {initial_energy:.6f} eV")
logger.debug(f"原始晶格体积: {cell.calculate_volume():.6f} ų")
def objective_function(scale_factor):
"""目标函数:计算缩放因子对应的能量"""
try:
# 等比例缩放晶格矢量(使用setter以同步volume/lattice_inv)
scaled_lattice = original_lattice * scale_factor[0]
cell.set_lattice_vectors(scaled_lattice)
# 原子保持相同的分数坐标,笛卡尔坐标随晶格缩放
cell.set_fractional_coordinates(original_fractional_coords)
# 计算能量
energy = potential.calculate_energy(cell)
return energy
except Exception as e:
logger.warning(f"缩放因子 {scale_factor[0]:.6f} 计算失败: {e}")
return 1e10 # 返回一个很大的值表示失败
# 1D优化设置
from scipy.optimize import minimize, minimize_scalar
initial_scale = 1.0
# 使用标量优化(1D专用,更稳定)
try:
result = minimize_scalar(
lambda s: objective_function([s]),
bounds=(0.8, 1.2), # 缩放范围±20%
method="bounded",
options={
"xatol": self.optimizer_params.get("ftol", 1e-8),
"maxiter": self.optimizer_params.get("maxiter", 500),
},
)
converged = result.success
optimal_scale = result.x
except Exception as e:
logger.warning(f"标量优化失败,尝试通用优化器: {e}")
# 回退到通用优化器
try:
result = minimize(
objective_function,
[initial_scale],
method="BFGS",
options={
"ftol": self.optimizer_params.get("ftol", 1e-8),
"gtol": self.optimizer_params.get("gtol", 1e-6),
"maxiter": self.optimizer_params.get("maxiter", 500),
},
)
converged = result.success
optimal_scale = result.x[0]
except Exception as e2:
logger.error(f"所有优化方法都失败: {e2}")
# 恢复原始状态
cell.lattice_vectors = original_lattice
cell.set_fractional_coordinates(original_fractional_coords)
return False
# 设置到最优状态
if converged:
optimal_lattice = original_lattice * optimal_scale
cell.lattice_vectors = optimal_lattice
cell.set_fractional_coordinates(original_fractional_coords)
# 记录优化结果
final_energy = potential.calculate_energy(cell)
energy_change = final_energy - initial_energy
volume_change = cell.calculate_volume() / np.linalg.det(original_lattice)
logger.info("等比例晶格弛豫成功")
logger.debug(f"最优缩放因子: {optimal_scale:.6f}")
logger.debug(f"最终总能量: {final_energy:.6f} eV")
logger.debug(f"能量变化: {energy_change:.6f} eV")
logger.debug(f"体积变化: {volume_change:.6f} (比值)")
logger.debug(f"最终体积: {cell.calculate_volume():.6f} ų")
else:
logger.warning("等比例晶格弛豫未收敛,恢复原始状态")
# 恢复原始状态
cell.lattice_vectors = original_lattice
cell.set_fractional_coordinates(original_fractional_coords)
return converged
[文档]
class ZeroTempDeformationCalculator:
r"""
零温显式形变法计算器
管理从基态制备到弹性常数求解的完整计算流程。
实现标准的显式形变法:制备基态→施加形变→内部弛豫→测量应力→线性拟合。
Parameters
----------
cell : Cell
待计算的晶胞对象
potential : Potential
势能函数对象
delta : float, optional
应变幅度,默认0.005 (0.5%)
num_steps : int, optional
每个应变分量的步数,默认5(教学模式)
relaxer_params : dict, optional
结构弛豫器参数
Attributes
----------
cell : Cell
原始晶胞对象
potential : Potential
势能函数对象
delta : float
应变幅度
num_steps : int
形变步数
relaxer : StructureRelaxer
结构弛豫器实例
reference_stress : numpy.ndarray
基态参考应力张量
Notes
-----
计算流程包含5个步骤:
1. **基态制备**:完全弛豫获得无应力基态
2. **形变生成**:生成6个Voigt分量的形变矩阵序列
3. **应力计算**:对每个形变施加内部弛豫并测量应力
4. **数据收集**:收集所有应力-应变数据对
5. **线性拟合**:通过最小二乘法求解弹性常数矩阵
应变生成策略:
- 对称应变::math:`\\varepsilon_{11}, \\varepsilon_{22}, \\varepsilon_{33}`
- 剪切应变::math:`\\varepsilon_{23}, \\varepsilon_{13}, \\varepsilon_{12}`
- 应变范围::math:`[-\\delta, +\\delta]`,均匀分布
Examples
--------
>>> calculator = ZeroTempDeformationCalculator(cell, potential, delta=0.005)
>>> elastic_matrix, r2_score = calculator.calculate()
>>> print(f"弹性常数矩阵 (GPa):\\n{elastic_matrix}")
>>> print(f"拟合优度 R²: {r2_score:.6f}")
"""
[文档]
def __init__(
self,
cell: Cell,
potential: Potential,
delta: float = 0.005,
num_steps: int = 5,
relaxer_params: dict[str, Any] | None = None,
supercell_dims: tuple[int, int, int] | None = None,
base_relax_mode: str = "uniform",
):
"""
初始化零温形变计算器
Parameters
----------
cell : Cell
晶胞对象
potential : Potential
势能函数对象
delta : float, optional
应变幅度,建议范围0.001-0.01,默认0.005
num_steps : int, optional
每个应变分量的步数,1为生产模式,>1为教学模式,默认5
relaxer_params : dict, optional
传递给StructureRelaxer的参数
supercell_dims : tuple, optional
超胞维度(nx, ny, nz),用于正确显示等效单胞参数
Raises
------
ValueError
如果delta或num_steps参数不合理
"""
# 参数有效性检查
if delta <= 0 or delta > 0.1:
raise ValueError(f"应变幅度delta={delta}不合理,建议范围0.001-0.01")
if num_steps <= 0:
raise ValueError(f"步数num_steps={num_steps}必须为正整数")
# 保存核心对象
self.cell = cell
self.potential = potential
self.delta = delta
self.num_steps = num_steps
self.supercell_dims = supercell_dims
# 基态弛豫模式:"uniform"(等比例缩放)或 "full"(变胞+位置)
if base_relax_mode not in ("uniform", "full"):
raise ValueError("base_relax_mode 必须为 'uniform' 或 'full'")
self.base_relax_mode = base_relax_mode
# 创建结构弛豫器
if relaxer_params:
# 提取optimizer_type和optimizer_params
optimizer_type = relaxer_params.get("optimizer_type", "L-BFGS")
optimizer_params = relaxer_params.get("optimizer_params", None)
self.relaxer = StructureRelaxer(
optimizer_type=optimizer_type,
optimizer_params=optimizer_params,
supercell_dims=self.supercell_dims,
)
else:
self.relaxer = StructureRelaxer(supercell_dims=self.supercell_dims)
# 初始化状态变量
self.reference_stress = None # 基态参考应力
# 记录初始化信息
logger.info("初始化ZeroTempDeformationCalculator")
logger.info(f"应变幅度: {delta:.2e} ({delta * 100:.4f}%)")
logger.info(f"形变步数: {num_steps}")
# 物理合理性提醒
if delta > 0.01:
logger.warning(f"应变幅度{delta:.3f}较大,可能超出线性弹性范围")
def _get_equivalent_unit_cell_parameter(self, lattice_vectors: np.ndarray) -> float:
"""
计算等效单胞晶格参数a
对于超胞,将晶格矢量长度除以对应的超胞维度
"""
if self.supercell_dims is None:
# 如果没有提供超胞信息,直接返回第一个晶格矢量的长度
return np.linalg.norm(lattice_vectors[0])
else:
# 返回等效单胞参数
return np.linalg.norm(lattice_vectors[0]) / self.supercell_dims[0]
[文档]
def calculate(self) -> tuple[np.ndarray, float]:
"""
执行完整的零温弹性常数计算
Returns
-------
tuple[numpy.ndarray, float]
弹性常数矩阵(GPa)和拟合优度R²的元组
Notes
-----
该方法执行5步计算流程:
1. 制备无应力基态
2. 生成形变矩阵序列
3. 计算每个形变的应力响应
4. 收集应力-应变数据
5. 线性拟合求解弹性常数
拟合质量评估:
- R² > 0.999:优秀
- R² > 0.99:良好
- R² < 0.99:需改进
Examples
--------
>>> calculator = ZeroTempDeformationCalculator(cell, potential)
>>> C_matrix, r2 = calculator.calculate()
>>>
>>> # 检查拟合质量
>>> if r2 > 0.999:
... print("拟合质量优秀")
>>> else:
... print(f"拟合质量R²={r2:.6f},可能需要调整参数")
"""
logger.info("开始零温弹性常数计算")
# 步骤1:制备无应力基态
logger.info("步骤1/5:制备无应力基态")
self._prepare_reference_state()
# 步骤2:生成形变矩阵序列
logger.info("步骤2/5:生成形变矩阵")
deformation_matrices = self._generate_deformation_matrices()
total_deformations = len(deformation_matrices)
logger.info(f"生成{total_deformations}个形变矩阵")
# 步骤3:计算所有形变的应力响应
logger.info("步骤3/5:计算应力响应")
results = []
for i, F in enumerate(deformation_matrices):
logger.info(f"计算形变 {i + 1}/{total_deformations}")
result = self._compute_single_deformation(F)
results.append(result)
# 收敛性监控
if result.converged:
logger.debug(f"形变{i + 1}计算成功")
else:
logger.warning(f"形变{i + 1}优化未收敛")
# 步骤4:提取应力应变数据
logger.info("步骤4/5:收集应力应变数据")
strains = np.array([result.strain_voigt for result in results])
stresses = np.array([result.stress_voigt for result in results])
logger.info(f"收集到{len(strains)}组应力应变数据")
logger.debug(f"应变数据形状: {strains.shape}")
logger.debug(f"应力数据形状: {stresses.shape}")
# 步骤5:线性拟合求解弹性常数
logger.info("步骤5/5:拟合弹性常数")
solver = ElasticConstantsSolver()
elastic_matrix_eV, r2_score = solver.solve(strains, stresses)
# 单位转换:eV/ų → GPa
elastic_matrix_GPa = elastic_matrix_eV * EV_TO_GPA
# 结果总结
logger.info("零温弹性常数计算完成")
logger.info(f"拟合优度R²: {r2_score:.6f}")
# 拟合质量评估
if r2_score > 0.999:
logger.info("拟合质量:优秀")
elif r2_score > 0.99:
logger.info("拟合质量:良好")
else:
logger.warning("拟合质量:需要改进,建议检查参数设置")
return elastic_matrix_GPa, r2_score
def _prepare_reference_state(self) -> None:
"""
制备无应力基态
通过完全弛豫获得无应力基态,并计算基态应力张量作为后续计算的参考。
所有形变计算的应力都将相对于此基态应力。
Notes
-----
基态制备的重要性:
1. 消除初始应力:确保所有形变从相同的无应力状态开始
2. 提供参考点:基态应力作为后续相对应力计算的零点
3. 保证线性:在无应力基态附近,应力-应变关系最接近线性
"""
logger.debug("制备无应力基态")
# 记录初始状态
initial_energy = self.potential.calculate_energy(self.cell)
initial_lattice = self.cell.lattice_vectors.copy()
initial_a = self._get_equivalent_unit_cell_parameter(initial_lattice)
logger.info("弛豫前状态:")
logger.info(f" 初始总能量: {initial_energy:.8f} eV")
logger.info(f" 每原子能量: {initial_energy / self.cell.num_atoms:.8f} eV/atom")
logger.info(f" 初始等效单胞晶格常数: a = {initial_a:.6f} Å")
logger.info(f" 初始体积: {self.cell.volume:.6f} ų")
# 执行基态弛豫:默认使用等比例晶格弛豫(示例脚本策略)
if self.base_relax_mode == "uniform":
logger.info("开始等比例晶格弛豫(优先)...")
converged = self.relaxer.uniform_lattice_relax(self.cell, self.potential)
if not converged:
logger.warning("等比例弛豫未收敛,回退到完全弛豫(变胞+位置)")
converged = self.relaxer.full_relax(self.cell, self.potential)
else:
logger.info("开始完全弛豫(变胞+位置)...")
converged = self.relaxer.full_relax(self.cell, self.potential)
# 检查弛豫结果
final_energy = self.potential.calculate_energy(self.cell)
final_lattice = self.cell.lattice_vectors.copy()
final_a = self._get_equivalent_unit_cell_parameter(final_lattice)
energy_change = final_energy - initial_energy
logger.info("弛豫后状态:")
logger.info(f" 最终总能量: {final_energy:.8f} eV")
logger.info(f" 每原子能量: {final_energy / self.cell.num_atoms:.8f} eV/atom")
logger.info(f" 能量变化: {energy_change:.3e} eV")
logger.info(f" 弛豫后等效单胞晶格常数: a = {final_a:.6f} Å")
rel_da = (final_a - initial_a) / max(abs(initial_a), 1e-30) * 100.0
logger.info(f" 晶格常数变化: Δa = {final_a - initial_a:.6f} Å ({rel_da:.3e}%)")
logger.info(f" 最终体积: {self.cell.volume:.6f} ų")
# 与EAM Al1文献值比较
literature_a = 4.045
a_error = abs(final_a - literature_a) / literature_a * 100
logger.info(f" 与EAM Al1文献值(4.045 Å)比较: 误差 = {a_error:.2f}%")
if not converged:
logger.warning("基态弛豫未完全收敛,可能影响计算精度")
else:
logger.info("✓ 完全弛豫成功收敛")
# 计算并保存基态应力张量
self.reference_stress = self._calculate_stress_tensor(self.cell)
# 将应力转换为GPa单位并详细输出
stress_gpa = self.reference_stress * 160.2176 # eV/ų to GPa conversion
logger.info("基态应力张量 (GPa):")
for i in range(3):
row_str = " ".join(f"{stress_gpa[i, j]:10.6f}" for j in range(3))
logger.info(f" [{row_str}]")
stress_magnitude = np.linalg.norm(stress_gpa)
logger.info(f"基态应力大小: {stress_magnitude:.6f} GPa")
# 检查基态应力大小
max_stress = np.max(np.abs(self.reference_stress))
if max_stress > 0.01: # 0.01 eV/ų ≈ 16 GPa
logger.warning(
f"基态应力较大({max_stress:.6f} eV/ų = {max_stress * 160.2176:.2f} GPa),可能未完全弛豫"
)
else:
logger.info("✓ 基态应力较小,弛豫质量良好")
logger.info("无应力基态制备完成")
def _generate_deformation_matrices(self) -> list[np.ndarray]:
r"""
生成形变矩阵序列
为6个独立的Voigt应变分量生成形变矩阵。每个分量按指定步数
在±delta范围内均匀分布。
Returns
-------
List[numpy.ndarray]
形变矩阵列表,每个矩阵形状为(3,3)
Notes
-----
Voigt应变分量对应关系:
- ε₁ = ε₁₁:x方向正应变
- ε₂ = ε₂₂:y方向正应变
- ε₃ = ε₃₃:z方向正应变
- ε₄ = 2ε₂₃:yz平面剪切应变
- ε₅ = 2ε₁₃:xz平面剪切应变
- ε₆ = 2ε₁₂:xy平面剪切应变
形变矩阵构造:
.. math::
F = I + \\varepsilon
其中I是单位矩阵,ε是应变张量。
"""
logger.debug("生成形变矩阵序列")
deformation_matrices = []
# 确定应变幅度序列
if self.num_steps == 1:
# 生产模式:仅使用正应变
strain_amplitudes = [self.delta]
logger.debug("生产模式:使用单一正应变")
else:
# 教学模式:使用正负应变的对称分布
strain_amplitudes = np.linspace(-self.delta, self.delta, self.num_steps)
# 移除零应变点(避免无意义计算)
strain_amplitudes = strain_amplitudes[strain_amplitudes != 0]
logger.debug(f"教学模式:使用{len(strain_amplitudes)}个应变幅度")
# Voigt应变分量到张量指标的映射
voigt_to_tensor = {
0: (0, 0), # ε₁₁
1: (1, 1), # ε₂₂
2: (2, 2), # ε₃₃
3: (1, 2), # ε₂₃ (和ε₃₂)
4: (0, 2), # ε₁₃ (和ε₃₁)
5: (0, 1), # ε₁₂ (和ε₂₁)
}
# 为每个Voigt分量生成形变矩阵
for voigt_idx in range(6):
i, j = voigt_to_tensor[voigt_idx]
for amplitude in strain_amplitudes:
# 创建应变张量
strain_tensor = np.zeros((3, 3))
# 剪切应变需要保证对称性,幅度要减半以保持工程剪切应变γ = amplitude
if i != j:
# 对于剪切应变,工程剪切应变 γ = 2ε,所以 ε = γ/2 = amplitude/2
strain_tensor[i, j] = amplitude / 2
strain_tensor[j, i] = amplitude / 2
else:
# 对于正应变,直接使用amplitude
strain_tensor[i, j] = amplitude
# 构造形变矩阵:F = I + ε
F = np.eye(3) + strain_tensor
deformation_matrices.append(F)
logger.debug(f"生成{len(deformation_matrices)}个形变矩阵")
return deformation_matrices
def _compute_single_deformation(
self, deformation_matrix: np.ndarray
) -> DeformationResult:
r"""
计算单个形变的应力响应
对给定的形变矩阵,执行形变→内部弛豫→应力计算的完整流程。
Parameters
----------
deformation_matrix : numpy.ndarray
形变矩阵F,形状(3,3)
Returns
-------
DeformationResult
包含应力、应变和收敛信息的结果对象
Notes
-----
计算步骤:
1. **施加形变**:cell.apply_deformation(F)
2. **内部弛豫**:优化原子位置,保持晶胞形状
3. **计算应力**:使用virial公式计算应力张量
4. **计算应变**:从形变矩阵提取应变张量
5. **Voigt转换**:将张量转换为Voigt向量
应变张量计算:
.. math::
\\varepsilon = \\frac{1}{2}(F + F^T) - I
其中F是形变矩阵,I是单位矩阵。
"""
logger.debug("计算单个形变的应力响应")
# 复制原始晶胞(避免修改原始数据)
deformed_cell = self.cell.copy()
# 记录形变前状态
initial_energy_def = self.potential.calculate_energy(deformed_cell)
initial_lattice_def = deformed_cell.lattice_vectors.copy()
initial_a_def = self._get_equivalent_unit_cell_parameter(initial_lattice_def)
# 施加形变
deformed_cell.apply_deformation(deformation_matrix)
logger.debug("已施加形变到晶胞")
# 记录形变后状态(首次形变)
after_deform_energy = self.potential.calculate_energy(deformed_cell)
after_deform_lattice = deformed_cell.lattice_vectors.copy()
after_deform_a = self._get_equivalent_unit_cell_parameter(after_deform_lattice)
# 计算应变张量和应变幅度(首次形变)
strain_tensor = 0.5 * (deformation_matrix + deformation_matrix.T) - np.eye(3)
strain_voigt = TensorConverter.to_voigt(strain_tensor, tensor_type="strain")
strain_magnitude = np.linalg.norm(strain_voigt)
logger.info(" 形变详情:")
logger.info(
f" 应变Voigt向量: [{strain_voigt[0]:8.6f}, {strain_voigt[1]:8.6f}, {strain_voigt[2]:8.6f}, {strain_voigt[3]:8.6f}, {strain_voigt[4]:8.6f}, {strain_voigt[5]:8.6f}]"
)
logger.info(f" 应变幅度: {strain_magnitude:.6f}")
logger.info(
f" 等效单胞晶格常数变化: {initial_a_def:.6f} → {after_deform_a:.6f} Å"
)
logger.info(
f" 能量变化: {after_deform_energy - initial_energy_def:.8f} eV"
)
# 剪切检测(仅对剪切分量应用更强的收敛策略与降幅重试)
shear_pairs = [(1, 2), (0, 2), (0, 1)]
shear_pair_used = None
for ii, jj in shear_pairs:
if abs(strain_tensor[ii, jj]) > 0:
shear_pair_used = (ii, jj)
break
# 内部弛豫(仅优化原子位置)
logger.info(" 开始内部弛豫 (固定晶胞,优化原子位置)...")
# 对剪切分量,按更大的迭代数尝试一次
if shear_pair_used is not None:
logger.info(" 剪切分量检测到,使用剪切专用收敛参数 (maxiter↑)")
from typing import Any
shear_params: dict[str, Any] = (
getattr(self.relaxer, "optimizer_params", {}) or {}
)
shear_params = {**shear_params}
shear_params["maxiter"] = max(
int(shear_params.get("maxiter", 10000)), 20000
)
shear_relaxer = StructureRelaxer(
optimizer_type=getattr(self.relaxer, "optimizer_type", "L-BFGS"),
optimizer_params=shear_params,
supercell_dims=self.supercell_dims,
)
converged = shear_relaxer.internal_relax(deformed_cell, self.potential)
else:
converged = self.relaxer.internal_relax(deformed_cell, self.potential)
# 若剪切仍未收敛:将剪切应变幅度减半,重试一次
if (shear_pair_used is not None) and (not converged):
ii, jj = shear_pair_used
logger.warning(" 剪切未收敛,降幅重试:γ -> γ/2")
# 从基态重新复制、应用半幅剪切
deformed_cell_half = self.cell.copy()
# 原 off-diagonal 应变分量 ε = γ/2,在 F 中体现在 F[ii,jj] 与 F[jj,ii]
eps_old = strain_tensor[ii, jj]
eps_new = 0.5 * eps_old
F_half = np.eye(3)
F_half[ii, jj] += eps_new
F_half[jj, ii] += eps_new
deformed_cell_half.apply_deformation(F_half)
# 重新弛豫
converged_half = shear_relaxer.internal_relax(
deformed_cell_half, self.potential
)
# 若半幅成功,则用半幅的形变/应变数据替换
if converged_half:
deformed_cell = deformed_cell_half
deformation_matrix = F_half
strain_tensor = 0.5 * (
deformation_matrix + deformation_matrix.T
) - np.eye(3)
strain_voigt = TensorConverter.to_voigt(
strain_tensor, tensor_type="strain"
)
strain_magnitude = np.linalg.norm(strain_voigt)
logger.info(" 降幅重试成功,采用半幅剪切结果")
converged = True
# 记录弛豫后状态
after_relax_energy = self.potential.calculate_energy(deformed_cell)
logger.info(" 内部弛豫完成:")
logger.info(f" 收敛状态: {'✓ 成功' if converged else '✗ 未收敛'}")
logger.info(
f" 弛豫能量变化: {after_relax_energy - after_deform_energy:.8f} eV"
)
logger.info(
f" 总能量变化: {after_relax_energy - initial_energy_def:.8f} eV"
)
# 计算应力张量
stress_tensor = self._calculate_stress_tensor(deformed_cell)
# 应力相对于基态(消除基态应力的影响)
stress_tensor_relative = stress_tensor - self.reference_stress
# 转换为Voigt记号
stress_voigt = TensorConverter.to_voigt(
stress_tensor_relative, tensor_type="stress"
)
# 详细输出应力信息
stress_gpa = stress_voigt * 160.2176 # 转换为GPa
logger.info(" 应力计算结果:")
logger.info(
f" 相对应力Voigt向量 (GPa): [{stress_gpa[0]:8.3f}, {stress_gpa[1]:8.3f}, {stress_gpa[2]:8.3f}, {stress_gpa[3]:8.3f}, {stress_gpa[4]:8.3f}, {stress_gpa[5]:8.3f}]"
)
# 计算应力应变比 (粗略的弹性模量指示)
if strain_magnitude > 1e-8:
stress_strain_ratio = np.linalg.norm(stress_gpa) / strain_magnitude
logger.info(
f" 应力应变比: {stress_strain_ratio:.2f} GPa (粗略弹性模量指示)"
)
logger.debug(f"应变(Voigt): {strain_voigt}")
logger.debug(f"应力(Voigt): {stress_voigt}")
return DeformationResult(
strain_voigt=strain_voigt,
stress_voigt=stress_voigt,
converged=converged,
deformation_matrix=deformation_matrix,
)
def _calculate_stress_tensor(self, cell: Cell) -> np.ndarray:
r"""
计算应力张量
Parameters
----------
cell : Cell
晶胞对象
Returns
-------
numpy.ndarray
应力张量,形状(3,3)
Notes
-----
应力张量通过virial公式计算:
.. math::
\\sigma_{\\alpha\\beta} = \\frac{1}{V} \\sum_i r_{i\\alpha} f_{i\\beta}
其中:
- :math:`V`:晶胞体积
- :math:`r_{i\\alpha}`:第i个原子在α方向的位置
- :math:`f_{i\\beta}`:第i个原子在β方向受到的力
应力张量必须是对称的,任何非对称性都被平均处理。
"""
# 使用Cell类的应力计算方法
stress_tensor = cell.calculate_stress_tensor(self.potential)
# 确保对称性(消除数值误差导致的微小非对称性)
stress_tensor = 0.5 * (stress_tensor + stress_tensor.T)
return stress_tensor
[文档]
class ElasticConstantsSolver:
r"""
弹性常数求解器
从应力应变数据通过线性回归求解弹性常数矩阵。
支持最小二乘法和岭回归两种方法,并提供拟合质量评估。
Methods
-------
solve(strains, stresses, method='least_squares')
求解弹性常数矩阵
Notes
-----
数学原理:
根据胡克定律::math:`\\sigma = C \\cdot \\varepsilon`
其中:
- :math:`\\sigma`:应力向量 (N×6)
- :math:`C`:弹性常数矩阵 (6×6)
- :math:`\\varepsilon`:应变向量 (N×6)
最小二乘求解:
.. math::
\\min_C ||\\sigma - C \\cdot \\varepsilon||^2
解析解:
.. math::
C = (\\varepsilon^T \\varepsilon)^{-1} \\varepsilon^T \\sigma
Examples
--------
>>> solver = ElasticConstantsSolver()
>>> C_matrix, r2_score = solver.solve(strains, stresses)
>>> print(f"弹性常数矩阵:\\n{C_matrix}")
>>> print(f"拟合优度: {r2_score:.6f}")
"""
[文档]
def solve(
self,
strains: np.ndarray,
stresses: np.ndarray,
method: str = "least_squares",
alpha: float = 1e-5,
) -> tuple[np.ndarray, float]:
r"""
求解弹性常数矩阵
Parameters
----------
strains : numpy.ndarray
应变数据,形状(N, 6),N为数据点数
stresses : numpy.ndarray
应力数据,形状(N, 6)
method : str, optional
求解方法,支持'least_squares'和'ridge',默认'least_squares'
alpha : float, optional
岭回归正则化参数,仅在method='ridge'时使用,默认1e-5
Returns
-------
tuple[numpy.ndarray, float]
弹性常数矩阵(6,6)和拟合优度R²
Raises
------
ValueError
如果输入数据格式不正确或求解方法不支持
Notes
-----
支持的求解方法:
1. **最小二乘法** ('least_squares'):
标准线性回归,适用于大多数情况
2. **岭回归** ('ridge'):
加入L2正则化,适用于病态矩阵情况
拟合优度R²计算:
.. math::
R^2 = 1 - \\frac{SS_{res}}{SS_{tot}}
其中:
- :math:`SS_{res} = \\sum(y_i - \\hat{y}_i)^2`:残差平方和
- :math:`SS_{tot} = \\sum(y_i - \\bar{y})^2`:总平方和
Examples
--------
>>> solver = ElasticConstantsSolver()
>>> # 使用最小二乘法
>>> C, r2 = solver.solve(strains, stresses)
>>> # 使用岭回归(适用于病态情况)
>>> C, r2 = solver.solve(strains, stresses, method='ridge', alpha=1e-3)
"""
logger.info(f"开始求解弹性常数,方法: {method}")
# 数据有效性验证
self._validate_data(strains, stresses)
# 根据方法选择求解器
if method == "least_squares":
C, r2_score = self._least_squares_solve(strains, stresses)
elif method == "ridge":
C, r2_score = self._ridge_regression_solve(strains, stresses, alpha)
else:
raise ValueError(f"不支持的求解方法: {method}")
# 若全局拟合质量差或矩阵明显不物理,使用“按应变分量逐列稳健回归”的保险方案
needs_rescue = (
not np.allclose(C, C.T, rtol=1e-6, atol=1e-6)
or np.any(np.isnan(C))
or np.isinf(np.linalg.cond(strains))
or r2_score < 0.95
)
if needs_rescue:
C_alt, r2_alt = self._per_mode_columnwise_solve(strains, stresses)
# 对称化,提升物理一致性
C_alt = 0.5 * (C_alt + C_alt.T)
# 若保险方案更好,则采用
if r2_alt >= r2_score or r2_score < 0.9:
C, r2_score = C_alt, r2_alt
# 如仍不理想,再尝试立方晶系约束拟合(三参数:C11, C12, C44)
if r2_score < 0.95:
C_cubic, r2_cubic = self._cubic_constrained_fit(strains, stresses)
if r2_cubic >= r2_score:
C, r2_score = C_cubic, r2_cubic
logger.info(f"弹性常数求解完成,R²: {r2_score:.6f}")
# 物理合理性检查
self._validate_elastic_matrix(C)
return C, r2_score
def _per_mode_columnwise_solve(
self, strains: np.ndarray, stresses: np.ndarray
) -> tuple[np.ndarray, float]:
"""逐列稳健求解 C
- 对第 k 个 Voigt 分量,仅用该分量非零且其余分量近零的样本行,拟合 C[:, k]
- 适用于我们"单分量逐一施加形变"的数据结构,可显著降低全局拟合的病态和串扰
"""
num_modes = 6
C = np.zeros((num_modes, num_modes), dtype=np.float64)
# 判定“其余分量近零”的阈值(相对量级)
other_tol = 1e-12
for k in range(num_modes):
eps_k = strains[:, k]
others = np.delete(strains, k, axis=1)
mask = (np.abs(eps_k) > other_tol) & (
np.max(np.abs(others), axis=1) < other_tol
)
if not np.any(mask):
# 回退:如果严格筛选无样本,放宽对其它分量的阈值
mask = np.abs(eps_k) > other_tol
X = eps_k[mask][:, None] # (M,1)
Y = stresses[mask] # (M,6)
if X.shape[0] == 0:
# 无法求解该列,保持零,后续对称化会缓解
continue
# 一维最小二乘:X @ col = Y → col = (X^T X)^-1 X^T Y
denom = float(X.T @ X)
if denom < 1e-20:
continue
col_k = (X.T @ Y) / denom # 形状 (1,6)
C[:, k] = col_k.ravel()
# 拟合优度:用组装出的 C 复原应力
Y_pred = strains @ C.T
ss_res = np.sum((stresses - Y_pred) ** 2)
ss_tot = np.sum((stresses - np.mean(stresses)) ** 2)
r2 = 1.0 - ss_res / ss_tot if ss_tot != 0 else (1.0 if ss_res == 0 else 0.0)
return C, r2
def _cubic_constrained_fit(
self, strains: np.ndarray, stresses: np.ndarray
) -> tuple[np.ndarray, float]:
"""在立方晶系假设下拟合:仅 C11, C12, C44。
使用"单分量形变"的行,分别以稳健统计(中位数斜率)估计:
- C11: 使用 ε11 行的 σ11/ε11、ε22 行的 σ22/ε22、ε33 行的 σ33/ε33 的中位数
- C12: 使用 ε11 行的 σ22/ε11、σ33/ε11 等跨分量比值的中位数(再与其它轴对换求中位数)
- C44: 使用 γ23 行的 σ23/γ23、γ13 行的 σ13/γ13、γ12 行的 σ12/γ12 的中位数
"""
def median_slope(x: np.ndarray, y: np.ndarray, eps: float = 1e-15) -> float:
mask = np.abs(x) > eps
if not np.any(mask):
return 0.0
ratios = y[mask] / x[mask]
ratios = ratios[np.isfinite(ratios)]
if ratios.size == 0:
return 0.0
return float(np.median(ratios))
# 收集按分量的行索引
idx_eps = [np.where(np.abs(strains[:, k]) > 1e-12)[0] for k in range(6)]
# C11: 取各主轴的自响应斜率的中位数
c11_candidates = []
for axis in range(3): # 0→σ11/ε11, 1→σ22/ε22, 2→σ33/ε33
rows = idx_eps[axis]
if rows.size:
c11_candidates.append(
median_slope(strains[rows, axis], stresses[rows, axis])
)
C11 = float(np.median(c11_candidates)) if c11_candidates else 0.0
# C12: 取“交叉主应力/主应变”的中位数(多个轴互换后再取中位数)
c12_candidates = []
# ε11 → σ22, σ33; ε22 → σ11, σ33; ε33 → σ11, σ22
for eps_axis in range(3):
rows = idx_eps[eps_axis]
if rows.size:
for sig_axis in range(3):
if sig_axis == eps_axis:
continue
c12_candidates.append(
median_slope(strains[rows, eps_axis], stresses[rows, sig_axis])
)
C12 = float(np.median(c12_candidates)) if c12_candidates else 0.0
# C44: 取剪切自响应的中位数(γ23→σ23 等)
shear_pairs = [(3, 3), (4, 4), (5, 5)] # (εk, σk) in Voigt ordering
c44_candidates = []
for eps_k, sig_k in shear_pairs:
rows = idx_eps[eps_k]
if rows.size:
c44_candidates.append(
median_slope(strains[rows, eps_k], stresses[rows, sig_k])
)
C44 = float(np.median(c44_candidates)) if c44_candidates else 0.0
# 组装立方晶系 6×6 矩阵
C = np.zeros((6, 6), dtype=np.float64)
# 主块
C[0, 0] = C[1, 1] = C[2, 2] = C11
C[0, 1] = C[0, 2] = C[1, 0] = C[1, 2] = C[2, 0] = C[2, 1] = C12
# 剪切块
C[3, 3] = C[4, 4] = C[5, 5] = C44
# 用该 C 评估 R²
Y_pred = strains @ C.T
ss_res = np.sum((stresses - Y_pred) ** 2)
ss_tot = np.sum((stresses - np.mean(stresses)) ** 2)
r2 = 1.0 - ss_res / ss_tot if ss_tot != 0 else (1.0 if ss_res == 0 else 0.0)
return C, r2
def _validate_data(self, strains: np.ndarray, stresses: np.ndarray) -> None:
"""
验证输入数据的有效性
Parameters
----------
strains : numpy.ndarray
应变数据
stresses : numpy.ndarray
应力数据
Raises
------
ValueError
如果数据不符合要求
"""
# 形状检查
if strains.shape != stresses.shape:
raise ValueError(
f"应变和应力数据形状不匹配: {strains.shape} vs {stresses.shape}"
)
if strains.ndim != 2 or strains.shape[1] != 6:
raise ValueError(f"应变数据必须是(N,6)形状,当前: {strains.shape}")
if stresses.ndim != 2 or stresses.shape[1] != 6:
raise ValueError(f"应力数据必须是(N,6)形状,当前: {stresses.shape}")
# 数据点数量检查
if strains.shape[0] < 6:
raise ValueError(f"数据点数量{strains.shape[0]}少于未知数数量6")
# 数值有效性检查
if np.any(~np.isfinite(strains)):
raise ValueError("应变数据包含无效值(NaN或Inf)")
if np.any(~np.isfinite(stresses)):
raise ValueError("应力数据包含无效值(NaN或Inf)")
logger.debug(f"数据验证通过: {strains.shape[0]}个数据点")
def _least_squares_solve(
self, strains: np.ndarray, stresses: np.ndarray
) -> tuple[np.ndarray, float]:
r"""
最小二乘法求解弹性常数
Parameters
----------
strains : numpy.ndarray
应变数据(N, 6)
stresses : numpy.ndarray
应力数据(N, 6)
Returns
-------
tuple[numpy.ndarray, float]
弹性常数矩阵(6,6)和拟合优度R²
Notes
-----
求解线性方程组:
.. math::
\\varepsilon \\cdot C = \\sigma
使用numpy.linalg.lstsq求解,该函数使用SVD分解处理病态矩阵。
"""
logger.debug("使用最小二乘法求解")
# 求解线性方程组:strains @ C = stresses
# 注意:lstsq求解的是 X @ beta = y 形式,所以结果需要转置
C, _, rank, _ = np.linalg.lstsq(strains, stresses, rcond=None)
C = C.T # 转置得到(6,6)弹性常数矩阵
# 计算拟合优度R²
predicted_stresses = strains @ C.T # 预测应力
ss_res = np.sum((stresses - predicted_stresses) ** 2) # 残差平方和
ss_tot = np.sum((stresses - np.mean(stresses)) ** 2) # 总平方和
# 处理零方差情况(所有应力都相同)
r2_score = (1.0 if ss_res == 0 else 0.0) if ss_tot == 0 else 1 - ss_res / ss_tot
# 记录求解信息
logger.debug(f"矩阵秩: {rank}/6")
logger.debug(f"条件数: {np.linalg.cond(strains):.2e}")
# 秩亏警告
if rank < 6:
logger.warning(f"应变矩阵秩亏({rank}/6),可能导致解不唯一")
# 病态矩阵警告
if np.linalg.cond(strains) > 1e12:
logger.warning("应变矩阵病态,建议使用岭回归")
return C, r2_score
def _ridge_regression_solve(
self, strains: np.ndarray, stresses: np.ndarray, alpha: float
) -> tuple[np.ndarray, float]:
r"""
岭回归求解弹性常数
Parameters
----------
strains : numpy.ndarray
应变数据(N, 6)
stresses : numpy.ndarray
应力数据(N, 6)
alpha : float
正则化参数
Returns
-------
tuple[numpy.ndarray, float]
弹性常数矩阵(6,6)和拟合优度R²
Notes
-----
岭回归目标函数:
.. math::
\\min_C ||\\sigma - C \\cdot \\varepsilon||^2 + \\alpha ||C||^2
解析解:
.. math::
C = (\\varepsilon^T \\varepsilon + \\alpha I)^{-1} \\varepsilon^T \\sigma
正则化参数α控制平滑程度:
- α = 0:退化为最小二乘法
- α较大:更平滑但可能欠拟合
- α较小:接近最小二乘法但改善条件数
"""
logger.debug(f"使用岭回归求解,正则化参数: {alpha}")
# 岭回归求解
XTX = strains.T @ strains # ε^T ε
XTX_reg = XTX + alpha * np.eye(XTX.shape[0]) # ε^T ε + αI
XTy = strains.T @ stresses # ε^T σ
# 求解正则化方程组
C = np.linalg.solve(XTX_reg, XTy).T
# 计算拟合优度R²
predicted_stresses = strains @ C.T
ss_res = np.sum((stresses - predicted_stresses) ** 2)
ss_tot = np.sum((stresses - np.mean(stresses)) ** 2)
# 处理零方差情况(所有应力都相同)
r2_score = (1.0 if ss_res == 0 else 0.0) if ss_tot == 0 else 1 - ss_res / ss_tot
# 记录求解信息
logger.debug(f"正则化后条件数: {np.linalg.cond(XTX_reg):.2e}")
return C, r2_score
def _validate_elastic_matrix(self, C: np.ndarray) -> None:
"""
验证弹性常数矩阵的物理合理性
Parameters
----------
C : numpy.ndarray
弹性常数矩阵(6,6)
Notes
-----
物理约束检查:
1. **对角元素正定**:所有模量必须为正
2. **矩阵对称**:热力学要求Cᵢⱼ = Cⱼᵢ
3. **矩阵正定**:所有特征值必须为正
4. **数值稳定**:条件数不能过大
这些检查有助于发现计算错误或物理不合理的结果。
"""
logger.debug("验证弹性常数矩阵的物理合理性")
# 检查对角元素(各向模量必须为正)
diagonal = np.diag(C)
if np.any(diagonal <= 0):
logger.warning("弹性常数对角元素存在非正值")
logger.warning(f"对角元素: {diagonal}")
# 检查对称性(热力学互易关系)
if not np.allclose(C, C.T, rtol=1e-10):
logger.warning("弹性常数矩阵不对称")
asymmetry = np.max(np.abs(C - C.T))
logger.warning(f"最大非对称度: {asymmetry:.2e}")
# 检查正定性(稳定性要求)
eigenvalues = np.linalg.eigvals(C)
if np.any(eigenvalues <= 0):
logger.warning("弹性常数矩阵不是正定的")
logger.warning(f"特征值: {eigenvalues}")
# 检查数值稳定性
condition_number = np.linalg.cond(C)
if condition_number > 1e12:
logger.warning(f"弹性常数矩阵条件数过大: {condition_number:.2e}")
logger.debug("弹性常数矩阵验证完成")
[文档]
class ShearDeformationMethod:
r"""
LAMMPS风格剪切形变方法
实现经过验证的LAMMPS风格剪切形变算法,用于计算剪切弹性常数(C44, C55, C66)。
该方法通过晶胞剪切和原子位置调整来施加纯剪切应变,避免了传统形变矩阵方法
在剪切计算中的数值问题。
Methods
-------
apply_shear_deformation(cell, direction, strain_magnitude)
对晶胞施加LAMMPS风格剪切形变
calculate_c44_response(cell, potential, strain_amplitudes, relaxer)
计算C44剪切响应
Notes
-----
LAMMPS剪切形变的核心原理:
1. **晶胞剪切**:直接修改晶格矢量的非对角分量
2. **原子位置调整**:按照仿射变换调整所有原子位置
3. **应变-应力关系**:通过virial应力张量计算剪切应力
支持的剪切方向:
- direction=4: yz剪切 (σ23, C44)
- direction=5: xz剪切 (σ13, C55)
- direction=6: xy剪切 (σ12, C66)
剪切应变定义:
.. math::
\\gamma_{ij} = \\frac{\\partial u_i}{\\partial x_j} + \\frac{\\partial u_j}{\\partial x_i}
其中γ是工程剪切应变,与张量剪切应变的关系为 :math:`\\gamma = 2\\varepsilon`
Examples
--------
>>> shear_method = ShearDeformationMethod()
>>>
>>> # 施加xy剪切形变
>>> deformed_cell = shear_method.apply_shear_deformation(
... cell, direction=6, strain_magnitude=0.005
... )
>>>
>>> # 计算C44响应
>>> strains = np.linspace(-0.004, 0.004, 9)
>>> c44_data = shear_method.calculate_c44_response(
... cell, potential, strains, relaxer
... )
"""
[文档]
def __init__(self):
"""初始化剪切形变方法"""
self.supported_directions = {
4: "yz剪切(C44)",
5: "xz剪切(C55)",
6: "xy剪切(C66)",
}
# Voigt记号映射:direction -> (应力分量i, j)
self.direction_to_stress_indices = {
4: (1, 2), # yz剪切 -> σ23
5: (0, 2), # xz剪切 -> σ13
6: (0, 1), # xy剪切 -> σ12
}
logger.debug("初始化ShearDeformationMethod")
[文档]
def apply_shear_deformation(
self, cell: Cell, direction: int, strain_magnitude: float
) -> Cell:
r"""
对晶胞施加LAMMPS风格剪切形变
该方法实现了LAMMPS中的剪切形变算法,通过同时修改晶胞参数和原子位置
来施加纯剪切应变。这种方法保持了晶胞的周期性边界条件,并确保
剪切形变的一致性。
Parameters
----------
cell : Cell
待形变的原始晶胞
direction : int
剪切方向代码:4(yz), 5(xz), 6(xy)
strain_magnitude : float
剪切应变幅度(工程剪切应变γ)
Returns
-------
Cell
施加剪切形变后的新晶胞对象
Raises
------
ValueError
如果指定了不支持的剪切方向
Notes
-----
LAMMPS剪切形变算法:
1. **yz剪切** (direction=4):
.. math::
h_{zy} \\leftarrow h_{zy} + \\gamma \\cdot h_{zz}
y_i \\leftarrow y_i + \\gamma \\cdot z_i
2. **xz剪切** (direction=5):
.. math::
h_{zx} \\leftarrow h_{zx} + \\gamma \\cdot h_{zz}
x_i \\leftarrow x_i + \\gamma \\cdot z_i
3. **xy剪切** (direction=6):
.. math::
h_{yx} \\leftarrow h_{yx} + \\gamma \\cdot h_{yy}
x_i \\leftarrow x_i + \\gamma \\cdot y_i
其中:
- h是晶格矢量矩阵
- γ是工程剪切应变
- (x_i, y_i, z_i)是第i个原子的位置
Examples
--------
>>> method = ShearDeformationMethod()
>>>
>>> # 施加0.5%的xy剪切
>>> deformed = method.apply_shear_deformation(cell, 6, 0.005)
>>>
>>> # 验证剪切应变
>>> original_pos = cell.get_positions()
>>> deformed_pos = deformed.get_positions()
>>> shear_strain = np.mean(deformed_pos[:, 0] - original_pos[:, 0]) / np.mean(original_pos[:, 1])
>>> print(f"实际剪切应变: {shear_strain:.6f}")
"""
if direction not in self.supported_directions:
raise ValueError(
f"不支持的剪切方向: {direction}. "
f"支持的方向: {list(self.supported_directions.keys())}"
)
# 复制原始数据避免修改输入
lattice = cell.lattice_vectors.copy()
positions = cell.get_positions().copy()
logger.debug(
f"施加{self.supported_directions[direction]},应变幅度: {strain_magnitude:.6f}"
)
# 根据方向施加相应的剪切形变
if direction == 4: # yz剪切 → σ23
# 晶格剪切:h[z,y] += γ * h[z,z]
lattice[2, 1] += strain_magnitude * lattice[2, 2]
# 原子位置调整:y_i += γ * z_i
for i in range(len(positions)):
positions[i, 1] += strain_magnitude * positions[i, 2]
elif direction == 5: # xz剪切 → σ13
# 晶格剪切:h[z,x] += γ * h[z,z]
lattice[2, 0] += strain_magnitude * lattice[2, 2]
# 原子位置调整:x_i += γ * z_i
for i in range(len(positions)):
positions[i, 0] += strain_magnitude * positions[i, 2]
elif direction == 6: # xy剪切 → σ12
# 晶格剪切:h[y,x] += γ * h[y,y]
lattice[1, 0] += strain_magnitude * lattice[1, 1]
# 原子位置调整:x_i += γ * y_i
for i in range(len(positions)):
positions[i, 0] += strain_magnitude * positions[i, 1]
# 创建新的原子对象(保持原有属性)
new_atoms = []
for i, atom in enumerate(cell.atoms):
new_atom = Atom(
id=atom.id,
symbol=atom.symbol,
mass_amu=atom.mass_amu,
position=positions[i],
)
new_atoms.append(new_atom)
# 创建形变后的晶胞
deformed_cell = Cell(
lattice_vectors=lattice, atoms=new_atoms, pbc_enabled=cell.pbc_enabled
)
logger.debug(
f"剪切形变完成,晶胞体积变化: {deformed_cell.volume / cell.volume:.6f}"
)
return deformed_cell
[文档]
def calculate_c44_response(
self,
cell: Cell,
potential: Potential,
strain_amplitudes: np.ndarray,
relaxer: StructureRelaxer,
include_base_state: bool = True,
) -> dict[str, Any]:
r"""
计算C44剪切响应
使用LAMMPS风格剪切方法计算三个剪切弹性常数(C44, C55, C66)的应力响应。
该方法对每个剪切方向施加一系列应变,通过内部弛豫消除原子间力,
然后测量相应的剪切应力。
Parameters
----------
cell : Cell
基态晶胞对象
potential : Potential
势能函数
strain_amplitudes : numpy.ndarray
应变幅度数组,建议范围[-0.005, 0.005]
relaxer : StructureRelaxer
结构弛豫器实例
include_base_state : bool, optional
是否包含零应变基态点,默认True
Returns
-------
dict
包含以下键的计算结果:
- 'directions': 剪切方向信息
- 'detailed_results': 每个方向的详细数据
- 'summary': 拟合结果汇总
Notes
-----
计算流程:
1. **基态制备**:确保从无应力基态开始
2. **多方向扫描**:对三个剪切方向分别计算
3. **应变序列**:对每个方向施加应变序列
4. **内部弛豫**:固定晶胞形状,优化原子位置
5. **应力测量**:计算剪切分量应力响应
6. **线性拟合**:通过最小二乘法求解弹性常数
立方晶系的剪切弹性常数:
.. math::
C_{44} = C_{55} = C_{66} = \\frac{\\partial\\sigma_{ij}}{\\partial\\gamma_{ij}}
其中σ是剪切应力,γ是工程剪切应变。
Examples
--------
>>> shear_method = ShearDeformationMethod()
>>> strain_points = np.linspace(-0.004, 0.004, 9)
>>>
>>> results = shear_method.calculate_c44_response(
... base_cell, potential, strain_points, relaxer
... )
>>>
>>> # 查看结果
>>> print(f"C44 = {results['summary']['C44']:.2f} GPa")
>>> print(f"拟合优度 R² = {results['summary']['r2_score']:.6f}")
"""
logger.info("开始C44剪切响应计算")
# 准备基态(确保无应力)
base_cell = cell.copy()
logger.info("制备无应力基态(优先等比例晶格弛豫)...")
base_converged = relaxer.uniform_lattice_relax(base_cell, potential)
if not base_converged:
logger.warning("等比例晶格弛豫未收敛,回退到完全弛豫(变胞+位置)")
base_converged = relaxer.full_relax(base_cell, potential)
if not base_converged:
logger.warning("基态弛豫未完全收敛")
# 计算基态应力作为参考
base_stress = base_cell.calculate_stress_tensor(potential)
logger.debug(f"基态应力张量范数: {np.linalg.norm(base_stress):.6f} eV/ų")
# 准备计算数据
detailed_results = []
all_strains = []
all_stresses = []
# 对三个剪切方向分别计算
for direction in [4, 5, 6]:
direction_name = self.supported_directions[direction]
stress_i, stress_j = self.direction_to_stress_indices[direction]
logger.info(f"计算{direction_name}响应...")
strains = []
stresses = []
converged_states = []
# 构建应变序列(包含零点或不包含)
strain_sequence = strain_amplitudes.copy()
if include_base_state and 0.0 not in strain_sequence:
strain_sequence = np.append(strain_sequence, 0.0)
strain_sequence = np.sort(strain_sequence)
for strain_amp in strain_sequence:
logger.debug(f" 应变 {strain_amp:+.6f}...")
if abs(strain_amp) < 1e-12:
# 基态零应变点
stress_value = base_stress[stress_i, stress_j] * EV_TO_GPA
converged = True
logger.debug(f" 基态应力: {stress_value:.6f} GPa")
else:
# 施加剪切形变
deformed_cell = self.apply_shear_deformation(
base_cell, direction, strain_amp
)
# 内部弛豫
converged = relaxer.internal_relax(deformed_cell, potential)
# 计算应力响应
stress_tensor = deformed_cell.calculate_stress_tensor(potential)
# 与示例脚本保持一致:不做基态差分,直接使用总应力分量
stress_value = stress_tensor[stress_i, stress_j] * EV_TO_GPA
logger.debug(
f" 相对应力: {stress_value:.6f} GPa, 收敛: {converged}"
)
strains.append(strain_amp)
stresses.append(stress_value)
converged_states.append(converged)
# 线性拟合求解该方向的弹性常数
strains_array = np.array(strains)
stresses_array = np.array(stresses)
# 最小二乘拟合: σ = C * ε
if len(strains_array) > 1:
coeffs = np.polyfit(strains_array, stresses_array, 1)
elastic_constant = coeffs[0] # 斜率即为弹性常数
# 计算拟合优度
predicted = np.polyval(coeffs, strains_array)
ss_res = np.sum((stresses_array - predicted) ** 2)
ss_tot = np.sum((stresses_array - np.mean(stresses_array)) ** 2)
r2_score = 1 - ss_res / ss_tot if ss_tot > 0 else 0.0
else:
elastic_constant = 0.0
r2_score = 0.0
# 收敛率统计
converged_count = sum(converged_states)
total_count = len(converged_states)
converged_ratio = converged_count / total_count if total_count > 0 else 0.0
# 保存该方向的详细结果
direction_result = {
"direction": direction,
"name": direction_name,
"strains": strains_array,
"stresses": stresses_array,
"converged_states": converged_states,
"elastic_constant": elastic_constant,
"r2_score": r2_score,
"converged_count": converged_count,
"total_count": total_count,
"converged_ratio": converged_ratio,
}
detailed_results.append(direction_result)
# 累积到全局数据
all_strains.extend(strains)
all_stresses.extend(stresses)
logger.info(
f" {direction_name}: {elastic_constant:.2f} GPa (R²={r2_score:.6f}, 收敛率={converged_ratio:.1%})"
)
# 计算平均C44值(立方晶系中C44=C55=C66)
elastic_constants = [result["elastic_constant"] for result in detailed_results]
average_c44 = np.mean(elastic_constants)
std_c44 = np.std(elastic_constants)
# 全局收敛统计
total_converged = sum(result["converged_count"] for result in detailed_results)
total_points = sum(result["total_count"] for result in detailed_results)
overall_converged_ratio = (
total_converged / total_points if total_points > 0 else 0.0
)
# 组装结果
summary = {
"C44": average_c44,
"C55": detailed_results[1]["elastic_constant"], # xz方向
"C66": detailed_results[2]["elastic_constant"], # xy方向
"average_c44": average_c44,
"std_c44": std_c44,
"individual_r2_scores": [result["r2_score"] for result in detailed_results],
"average_r2_score": np.mean(
[result["r2_score"] for result in detailed_results]
),
"converged_ratio": overall_converged_ratio,
"total_converged": total_converged,
"total_points": total_points,
}
logger.info("C44剪切响应计算完成")
logger.info(f" 平均C44: {average_c44:.2f} ± {std_c44:.2f} GPa")
logger.info(f" 平均拟合优度: {summary['average_r2_score']:.6f}")
logger.info(
f" 总收敛率: {overall_converged_ratio:.1%} ({total_converged}/{total_points})"
)
return {
"directions": list(self.supported_directions.keys()),
"detailed_results": detailed_results,
"summary": summary,
"base_stress": base_stress,
"method": "LAMMPS_shear",
}
[文档]
def calculate_zero_temp_elastic_constants(
cell: Cell, potential: Potential, delta: float = 0.005, num_steps: int = 5, **kwargs
) -> tuple[np.ndarray, float]:
r"""
零温弹性常数计算的便捷函数
这是一个高级接口,封装了完整的零温弹性常数计算流程。
适合快速计算和脚本使用。
Parameters
----------
cell : Cell
晶胞对象
potential : Potential
势能函数对象
delta : float, optional
应变幅度,默认0.005 (0.5%)
num_steps : int, optional
每个应变分量的步数,默认5
**kwargs
其他参数传递给ZeroTempDeformationCalculator
Returns
-------
tuple[numpy.ndarray, float]
弹性常数矩阵(GPa)和拟合优度R²
Examples
--------
>>> from thermoelasticsim.elastic.deformation_method.zero_temp import calculate_zero_temp_elastic_constants
>>>
>>> # 快速计算
>>> C_matrix, r2 = calculate_zero_temp_elastic_constants(cell, potential)
>>> print(f"弹性常数矩阵(GPa):\\n{C_matrix}")
>>> print(f"拟合优度: {r2:.6f}")
>>>
>>> # 高精度计算
>>> C_matrix, r2 = calculate_zero_temp_elastic_constants(
... cell, potential, delta=0.001, num_steps=1
... )
"""
# 创建计算器实例
calculator = ZeroTempDeformationCalculator(
cell, potential, delta, num_steps, **kwargs
)
# 执行计算
return calculator.calculate()