thermoelasticsim.utils.optimizers 源代码

# 文件名: optimizers.py
# 作者: Gilbert Young
# 修改日期: 2025-03-27
# 文件描述: 提供多种优化算法用于原子结构优化

"""
分子动力学模拟优化器模块

提供多种优化算法用于原子结构优化,包括:
- GradientDescentOptimizer: 带动量项的梯度下降
- BFGSOptimizer: 基于scipy.optimize.minimize的BFGS
- LBFGSOptimizer: 改进的L-BFGS(有限内存BFGS)
"""

import logging

import numpy as np
from scipy.optimize import minimize

from thermoelasticsim.core.structure import Atom, Cell
from thermoelasticsim.potentials import Potential

logger = logging.getLogger(__name__)


[文档] class Optimizer: """优化器抽象基类,定义优化方法的接口 子类需要实现optimize()方法来执行具体的优化算法。 Methods ------- optimize(cell, potential) 执行优化算法,需子类实现 """
[文档] def optimize(self, cell: Cell, potential: Potential) -> tuple[bool, list]: """执行优化算法 Parameters ---------- cell : Cell 晶胞对象 potential : Potential 势能函数 Returns ------- tuple[bool, list] 返回(是否收敛, 轨迹数据) Notes ----- 轨迹数据包含每步的: - 原子位置 - 晶胞体积 - 晶格矢量 """ raise NotImplementedError
[文档] class GradientDescentOptimizer(Optimizer): """带动量项的梯度下降优化器 Parameters ---------- maxiter : int, optional 最大迭代步数,默认10000 tol : float, optional 力收敛阈值,默认1e-3 eV/Å step_size : float, optional 步长,默认1e-3 Å energy_tol : float, optional 能量变化阈值,默认1e-4 eV beta : float, optional 动量系数[0,1),默认0.9 Attributes ---------- converged : bool 优化是否收敛的标志 trajectory : list 记录优化轨迹的列表 """
[文档] def __init__( self, maxiter=10000, tol=1e-3, step_size=1e-3, energy_tol=1e-4, beta=0.9 ): """初始化带动量项的梯度下降优化器""" self.maxiter = maxiter self.tol = tol self.step_size = step_size self.energy_tol = energy_tol self.beta = beta # 动量项的系数 self.converged = False # 收敛标志 self.trajectory = [] # 记录轨迹数据
[文档] def optimize(self, cell: Cell, potential: Potential) -> tuple[bool, list[dict]]: """执行带动量项的梯度下降优化 Parameters ---------- cell : Cell 晶胞对象 potential : Potential 势能函数 Returns ------- tuple[bool, list[dict]] 返回(是否收敛, 轨迹数据字典列表) Notes ----- 收敛条件: - 最大原子力 < tol - 能量变化 < energy_tol """ logger = logging.getLogger(__name__) atoms = cell.atoms potential.calculate_forces(cell) previous_energy = potential.calculate_energy(cell) velocities = [np.zeros(3, dtype=np.float64) for _ in atoms] # 初始化动量 for step in range(1, self.maxiter + 1): positions = cell.get_positions() forces = cell.get_forces() volume = cell.volume lattice_vectors = cell.lattice_vectors.copy() # 记录当前状态 self.trajectory.append( { "step": step, "positions": positions.copy(), "volume": volume, "lattice_vectors": lattice_vectors.copy(), } ) logger.debug(f"Step {step} - Atom positions:\n{positions}") logger.debug(f"Step {step} - Atom forces:\n{forces}") # 计算最大力 max_force = max(np.linalg.norm(atom.force) for atom in atoms) potential_energy = potential.calculate_energy(cell) total_energy = potential_energy energy_change = abs(total_energy - previous_energy) logger.debug( f"GD with Momentum Step {step}: Max force = {max_force:.6f} eV/Å, " f"Total Energy = {total_energy:.6f} eV, Energy Change = {energy_change:.6e} eV" ) # 检查收敛条件 if max_force < self.tol and energy_change < self.energy_tol: logger.info( f"Gradient Descent with Momentum converged after {step} steps." ) self.converged = True break # 更新 previous_energy previous_energy = total_energy # 使用动量更新原子位置 for i, atom in enumerate(atoms): # 更新动量项 velocities[i] = self.beta * velocities[i] + (1 - self.beta) * atom.force displacement = self.step_size * velocities[i] atom.position += displacement # 应用周期性边界条件 atom.position = cell.apply_periodic_boundary(atom.position) logger.debug( f"Atom {atom.id} new position with momentum: {atom.position}" ) # 重新计算力 potential.calculate_forces(cell) # 检查原子间距离 min_distance = np.inf num_atoms = len(atoms) min_pair = (-1, -1) for i in range(num_atoms): for j in range(i + 1, num_atoms): rij = atoms[j].position - atoms[i].position if cell.pbc_enabled: rij = cell.minimum_image(rij) r = np.linalg.norm(rij) if r < min_distance: min_distance = r min_pair = (atoms[i].id, atoms[j].id) logger.debug( f"Step {step}: Min distance = {min_distance:.3f} Å between atoms {min_pair[0]} and {min_pair[1]}" ) if min_distance < 1.0: logger.error( f"Step {step}: Minimum inter-atomic distance {min_distance:.3f} Å is too small between atoms {min_pair[0]} and {min_pair[1]}. Terminating optimization." ) self.converged = False break else: logger.warning( "Gradient Descent with Momentum did not converge within the maximum number of steps." ) self.converged = False # 输出最终原子位置 final_positions = cell.get_positions() volume = cell.volume lattice_vectors = cell.lattice_vectors.copy() self.trajectory.append( { "step": self.maxiter + 1, "positions": final_positions.copy(), "volume": volume, "lattice_vectors": lattice_vectors.copy(), } ) logger.debug(f"Gradient Descent Optimizer final positions:\n{final_positions}") return self.converged, self.trajectory
[文档] class BFGSOptimizer(Optimizer): """BFGS准牛顿优化器 Parameters ---------- tol : float, optional 收敛阈值,默认1e-6 maxiter : int, optional 最大迭代步数,默认10000 Attributes ---------- converged : bool 优化是否收敛的标志 trajectory : list 记录优化轨迹的列表 Notes ----- 使用scipy.optimize.minimize的BFGS实现 适用于中等规模系统优化 """
[文档] def __init__(self, tol=1e-6, maxiter=10000): """初始化 BFGS 优化器""" self.tol = tol self.maxiter = maxiter self.converged = False self.trajectory = [] # 记录轨迹数据
[文档] def optimize(self, cell: Cell, potential: Potential) -> tuple[bool, list[dict]]: """执行 BFGS 优化 Parameters ---------- cell : Cell 晶胞对象 potential : Potential 势能函数 Returns ------- tuple[bool, list[dict]] 返回(是否收敛, 轨迹数据字典列表) """ logger = logging.getLogger(__name__) # 采用分数坐标优化,避免跨PBC边界的不连续导致的精度损失 lattice = cell.lattice_vectors.copy() def cartesian_from_frac(frac_coords): wrapped = frac_coords % 1.0 return wrapped @ lattice # 定义能量函数(自变量为分数坐标) def energy_fn(frac_flat): frac = frac_flat.reshape(-1, 3) positions = cartesian_from_frac(frac) for i, atom in enumerate(cell.atoms): atom.position = positions[i] energy = potential.calculate_energy(cell) return energy # 定义梯度函数:dE/d(frac) = (dE/dpos) · lattice^T = -F · lattice^T def grad_fn(frac_flat): frac = frac_flat.reshape(-1, 3) positions = cartesian_from_frac(frac) for i, atom in enumerate(cell.atoms): atom.position = positions[i] potential.calculate_forces(cell) forces = cell.get_forces() # (N,3) grad_frac = -(forces @ lattice.T) # (N,3) return grad_frac.flatten() # 初始分数坐标 positions0 = cell.get_positions() initial_frac = np.linalg.solve(lattice.T, positions0.T).T.flatten() # 定义回调函数,在每次迭代结束后记录轨迹 def callback(xk): frac = xk.reshape((-1, 3)) positions = cartesian_from_frac(frac) for i, atom in enumerate(cell.atoms): atom.position = positions[i] volume = cell.volume lattice_vectors = cell.lattice_vectors.copy() # 记录轨迹数据 self.trajectory.append( { "step": len(self.trajectory) + 1, "positions": positions.copy(), "volume": volume, "lattice_vectors": lattice_vectors.copy(), } ) logger.debug(f"Callback at iteration {len(self.trajectory)}") # 执行 BFGS 优化 result = minimize( energy_fn, initial_frac, method="BFGS", jac=grad_fn, tol=self.tol, options={"maxiter": self.maxiter, "disp": False}, callback=callback, # 设置回调函数 ) if result.success: self.converged = True # 更新原子的位置(分数坐标→笛卡尔) optimized_frac = result.x.reshape((-1, 3)) optimized_positions = cartesian_from_frac(optimized_frac) for i, atom in enumerate(cell.atoms): atom.position = optimized_positions[i] logger.info("BFGS Optimizer converged successfully.") else: self.converged = False logger.warning(f"BFGS Optimizer did not converge: {result.message}") # 在优化结束后,记录最终状态 volume = cell.volume lattice_vectors = cell.lattice_vectors.copy() self.trajectory.append( { "step": len(self.trajectory) + 1, "positions": cell.get_positions().copy(), "volume": volume, "lattice_vectors": lattice_vectors.copy(), } ) logger.debug(f"BFGS Optimizer final positions:\n{cell.get_positions()}") return self.converged, self.trajectory
[文档] class CGOptimizer(Optimizer): """共轭梯度优化器(分数坐标),对剪切后的浅谷更稳健。 Parameters ---------- tol : float, optional 梯度收敛阈值(映射到 scipy 的 gtol),默认 1e-6 maxiter : int, optional 最大迭代步数,默认 10000 """
[文档] def __init__(self, tol: float = 1e-6, maxiter: int = 10000): self.tol = tol self.maxiter = maxiter self.converged = False self.trajectory = []
[文档] def optimize(self, cell: Cell, potential: Potential) -> tuple[bool, list[dict]]: """执行 CG 优化(变量为分数坐标)。""" logger = logging.getLogger(__name__) lattice = cell.lattice_vectors.copy() def cartesian_from_frac(frac_coords): wrapped = frac_coords % 1.0 return wrapped @ lattice def energy_fn(frac_flat): frac = frac_flat.reshape(-1, 3) positions = cartesian_from_frac(frac) for i, atom in enumerate(cell.atoms): atom.position = positions[i] return potential.calculate_energy(cell) def grad_fn(frac_flat): frac = frac_flat.reshape(-1, 3) positions = cartesian_from_frac(frac) for i, atom in enumerate(cell.atoms): atom.position = positions[i] potential.calculate_forces(cell) forces = cell.get_forces() grad_frac = -(forces @ lattice.T) return grad_frac.flatten() positions0 = cell.get_positions() initial_frac = np.linalg.solve(lattice.T, positions0.T).T.flatten() def callback(xk): frac = xk.reshape((-1, 3)) positions = cartesian_from_frac(frac) for i, atom in enumerate(cell.atoms): atom.position = positions[i] self.trajectory.append( { "step": len(self.trajectory) + 1, "positions": positions.copy(), "volume": cell.volume, "lattice_vectors": cell.lattice_vectors.copy(), } ) result = minimize( energy_fn, initial_frac, method="CG", jac=grad_fn, options={"maxiter": self.maxiter, "gtol": self.tol, "disp": False}, callback=callback, ) if result.success: self.converged = True optimized_frac = result.x.reshape((-1, 3)) optimized_positions = cartesian_from_frac(optimized_frac) for i, atom in enumerate(cell.atoms): atom.position = optimized_positions[i] logger.info("CG Optimizer converged successfully.") else: self.converged = False logger.warning(f"CG Optimizer did not converge: {result.message}") return self.converged, self.trajectory
[文档] class LBFGSOptimizer(Optimizer): """L-BFGS优化器(有限内存BFGS) 该优化器能够处理两种模式: 1. 仅优化原子位置 (默认)。 2. 同时优化原子位置和晶胞形状 (完全弛豫)。 Parameters ---------- ftol : float, optional 函数收敛阈值,默认1e-8 gtol : float, optional 梯度收敛阈值,默认1e-6 maxiter : int, optional 最大迭代步数,默认1000 **kwargs : dict, optional 其他要传递给 `scipy.optimize.minimize` 的选项, 例如 `{'disp': True}` """
[文档] def __init__( self, ftol=1e-8, gtol=1e-6, maxiter=1000, supercell_dims=None, **kwargs ): self.ftol = ftol self.gtol = gtol self.maxiter = maxiter self.supercell_dims = supercell_dims self.extra_options = kwargs self.converged = False self.trajectory = []
[文档] def optimize( self, cell: Cell, potential: Potential, relax_cell: bool = False ) -> tuple[bool, list[dict]]: """执行 L-BFGS 优化。""" if relax_cell: return self._optimize_full(cell, potential) else: return self._optimize_positions_only(cell, potential)
def _optimize_positions_only( self, cell: Cell, potential: Potential ) -> tuple[bool, list[dict]]: """仅优化原子位置。""" logger.debug("L-BFGS: 开始仅优化原子位置...") def energy_fn(positions_flat): cell.set_positions(positions_flat.reshape(-1, 3)) return potential.calculate_energy(cell) def grad_fn(positions_flat): cell.set_positions(positions_flat.reshape(-1, 3)) potential.calculate_forces(cell) return -cell.get_forces().flatten() initial_positions = cell.get_positions().flatten() # 记录固定的晶格参数用于显示 fixed_lattice = cell.lattice_vectors fixed_a = np.linalg.norm(fixed_lattice[0]) fixed_volume = cell.volume # 创建迭代记录器用于内部弛豫 class PositionIterationLogger: def __init__(self, supercell_dims): self.niter = 0 self.supercell_dims = supercell_dims def __call__(self, xk): self.niter += 1 energy = energy_fn(xk) # 计算等效单胞参数用于显示 if self.supercell_dims is not None: equiv_a = fixed_a / self.supercell_dims[0] equiv_volume = fixed_volume / ( self.supercell_dims[0] * self.supercell_dims[1] * self.supercell_dims[2] ) logger.debug( f" 内部弛豫迭代 {self.niter:4d}: Energy={energy:12.6f} eV (固定等效单胞 a={equiv_a:.6f} Å, V={equiv_volume:.2f} ų)" ) else: logger.debug( f" 内部弛豫迭代 {self.niter:4d}: Energy={energy:12.6f} eV (固定 a={fixed_a:.6f} Å, V={fixed_volume:.2f} ų)" ) # 每10步或前几步显示进度 if self.niter <= 3 or self.niter % 10 == 0: logger.debug( f" 内部弛豫进度: 第{self.niter}步, 能量={energy:.6f} eV" ) position_logger = PositionIterationLogger(self.supercell_dims) options = { "ftol": self.ftol, "gtol": self.gtol, "maxiter": self.maxiter, "disp": True, } options.update(self.extra_options) # 详细记录优化参数用于诊断 logger.debug(f"L-BFGS优化参数诊断: {options}") result = minimize( energy_fn, initial_positions, method="L-BFGS-B", jac=grad_fn, options=options, callback=position_logger, ) self.converged = result.success if self.converged: cell.set_positions(result.x.reshape(-1, 3)) logger.debug("L-BFGS (仅原子) 优化收敛。") else: # 原始信息,不加工 logger.warning("L-BFGS (仅原子) 优化未收敛") logger.warning(f" 原始message: '{result.message}'") logger.warning(f" success: {result.success}") logger.warning(f" status: {result.status}") logger.warning(f" nfev: {result.nfev}") logger.warning(f" nit: {result.nit}") logger.warning(f" fun: {result.fun}") if hasattr(result, "jac"): logger.warning(f" jac norm: {np.linalg.norm(result.jac):.6e}") logger.warning(f" 完整result对象: {result}") # 记录maxls参数是否生效 - 检查实际传递的options字典 logger.debug(f" 设置的maxls: {options.get('maxls', '未设置')}") logger.debug(f" 设置的maxfun: {options.get('maxfun', '未设置')}") logger.debug(f" 完整options: {options}") return self.converged, [] def _optimize_full( self, cell: Cell, potential: Potential ) -> tuple[bool, list[dict]]: """同时优化原子位置和晶胞形状。""" logger.debug("L-BFGS: 开始完全弛豫 (原子+晶胞)...") initial_lattice = cell.lattice_vectors.copy() initial_volume = cell.volume initial_a = np.linalg.norm(initial_lattice[0]) atom_info = [(atom.id, atom.symbol, atom.mass_amu) for atom in cell.atoms] num_atoms = cell.num_atoms def pack_variables(positions, lattice): frac_coords = np.linalg.solve(lattice.T, positions.T).T return np.concatenate([frac_coords.flatten(), lattice.flatten()]) def unpack_variables(x): frac_coords = x[: 3 * num_atoms].reshape(num_atoms, 3) lattice = x[3 * num_atoms :].reshape(3, 3) positions = frac_coords @ lattice return positions, lattice, frac_coords def create_cell_from_variables(positions, lattice): atoms = [ Atom(id=info[0], symbol=info[1], mass_amu=info[2], position=pos) for info, pos in zip(atom_info, positions, strict=False) ] return Cell(lattice, atoms) def energy_fn(x): positions, lattice, _ = unpack_variables(x) temp_cell = create_cell_from_variables(positions, lattice) return potential.calculate_energy(temp_cell) x0 = pack_variables(cell.get_positions(), cell.lattice_vectors) options = {"ftol": self.ftol, "gtol": self.gtol, "maxiter": self.maxiter} options.update(self.extra_options) # 创建一个类来存储迭代信息,用于回调 class IterationLogger: def __init__(self, supercell_dims, ftol, initial_energy, initial_lattice): self.niter = 0 self.supercell_dims = supercell_dims self.prev_energy = None self.prev_a = None self.ftol = ftol self.initial_energy = initial_energy if supercell_dims is not None: self.initial_a = ( np.linalg.norm(initial_lattice[0]) / supercell_dims[0] ) else: self.initial_a = np.linalg.norm(initial_lattice[0]) logger.debug("优化迭代监控器初始化完成") def __call__(self, xk): self.niter += 1 logger.debug(f"优化器回调函数被调用,第{self.niter}次") energy = energy_fn(xk) positions, lattice, _ = unpack_variables(xk) current_a = np.linalg.norm(lattice[0]) # current_volume = np.abs(np.linalg.det(lattice)) # 体积监控,暂未使用 # 计算相对于上一步的变化量 energy_change_step = ( energy - self.prev_energy if self.prev_energy is not None else energy - self.initial_energy ) # 计算相对于初始值的总变化量 energy_change_total = energy - self.initial_energy # 计算等效单胞参数(如果是超胞的话) if self.supercell_dims is not None: equiv_a = current_a / self.supercell_dims[0] # equiv_volume = current_volume / ( # self.supercell_dims[0] # * self.supercell_dims[1] # * self.supercell_dims[2] # ) # 等效体积,暂未使用 a_change_step = ( equiv_a - self.prev_a if self.prev_a is not None else equiv_a - self.initial_a ) a_change_total = equiv_a - self.initial_a logger.debug( f" L-BFGS第{self.niter:2d}步: 能量={energy:12.6f} eV (步间Δ={energy_change_step:+.6f}, 总Δ={energy_change_total:+.6f})" ) logger.debug( f" 等效单胞 a={equiv_a:.6f} Å (步间Δ={a_change_step:+.6f}, 总Δ={a_change_total:+.6f})" ) # 估算收敛状态 if self.niter > 1: ftol_check = abs(energy_change_step) / max(abs(energy), 1e-12) logger.debug( f" 收敛检查: |步间ΔE|/|E| = {ftol_check:.2e} (目标: {self.ftol:.1e})" ) self.prev_a = equiv_a else: logger.debug( f" L-BFGS第{self.niter:2d}步: 能量={energy:12.6f} eV (Δ={energy_change_step:+.6f}), a={current_a:.6f} Å" ) # 每5步或前几步显示进度 if self.niter <= 3 or self.niter % 5 == 0: if self.supercell_dims is not None: equiv_a = current_a / self.supercell_dims[0] logger.info( f" 优化进度: 第{self.niter}步, 能量={energy:.6f} eV (总Δ={energy_change_total:+.3e}), a={equiv_a:.6f} Å" ) else: logger.info( f" 优化进度: 第{self.niter}步, 能量={energy:.6f} eV (Δ={energy_change_step:+.3e})" ) self.prev_energy = energy # 获取初始状态用于计算变化量 initial_energy = energy_fn(x0) initial_positions, initial_lattice, _ = unpack_variables(x0) iteration_logger = IterationLogger( self.supercell_dims, self.ftol, initial_energy, initial_lattice ) result = minimize( energy_fn, x0, method="L-BFGS-B", jac=None, options=options, callback=iteration_logger, ) self.converged = result.success if self.converged: final_positions, final_lattice, _ = unpack_variables(result.x) cell.set_lattice_vectors(final_lattice) cell.set_positions(final_positions) final_volume = cell.volume final_a = np.linalg.norm(final_lattice[0]) logger.info("L-BFGS (完全数值梯度) 优化收敛。") logger.info(f"晶格常数变化: {initial_a:.6f}{final_a:.6f} Å") logger.info(f"体积变化: {initial_volume:.6f}{final_volume:.6f} ų") rel_da = (final_a - initial_a) / max(abs(initial_a), 1e-30) * 100.0 rel_dv = ( (final_volume - initial_volume) / max(abs(initial_volume), 1e-30) * 100.0 ) logger.info(f"相对变化: Δa/a = {rel_da:.3e}%, ΔV/V = {rel_dv:.3e}%") else: logger.warning(f"L-BFGS (完全数值梯度) 优化未收敛: {result.message}") return self.converged, []