atomppgen.invert 源代码

"""
半局域势反演

从 TM 伪轨道反演半局域势 V_l(r),使用径向 Schrödinger 方程:

    V_l(r) = ε + (1/2) · u''(r)/u(r) - l(l+1)/(2r²)

技术要点:
- 内区(r ≤ rc):使用 TM 解析导数,数值稳定
- 外区(r > rc):使用样条法导数
- 节点保护:检测零点并插值或掩码

主要功能
--------
invert_semilocal_potential : 反演半局域势
InvertResult : 存储反演结果的数据类

参考文献
--------
Troullier & Martins, PRB 43, 1993 (1991)
Giannozzi, Notes on pseudopotential generation (2019)
"""

from dataclasses import dataclass
from typing import Dict, Optional
import numpy as np
from scipy.interpolate import CubicSpline

from atomppgen.tm import TMResult, eval_derivatives_at


__all__ = [
    "InvertResult",
    "invert_semilocal_potential",
]


[文档] @dataclass class InvertResult: """ 势反演结果 Attributes ---------- V_l : np.ndarray 半局域势 V_l(r)(Hartree) r : np.ndarray 径向网格(Bohr) l : int 角动量量子数 rc : float 伪化半径(Bohr) eps : float 伪化能量(Hartree) diagnostics : Dict 诊断信息: - n_nodes : 节点数 - V_max : 最大势值 - V_min : 最小势值 - V_at_rc : rc 处势值 - method_inner : 内区方法 ('analytical') - method_outer : 外区方法 ('spline') """ V_l: np.ndarray r: np.ndarray l: int rc: float eps: float diagnostics: Dict
[文档] def invert_semilocal_potential( tm_result: TMResult, r: np.ndarray, u_ps: Optional[np.ndarray] = None, node_tol: float = 1e-10, V_max_clip: float = 1000.0, smooth_rc: bool = False, smooth_width: float = 0.1, ) -> InvertResult: """ 从 TM 伪轨道反演半局域势 使用径向 Schrödinger 方程反演: V_l(r) = ε + (1/2) · u''(r)/u(r) - l(l+1)/(2r²) 内区(r ≤ rc)使用 TM 解析导数,外区使用样条法导数。 Parameters ---------- tm_result : TMResult TM 伪化结果(包含 a_coeff, rc, eps, l) r : np.ndarray 径向网格(Bohr) u_ps : np.ndarray, optional 伪轨道,若不提供则从 tm_result.u_ps 获取 node_tol : float, default=1e-10 节点检测阈值(|u| < node_tol 视为节点) V_max_clip : float, default=1000.0 势的裁剪上限(防止除零发散) smooth_rc : bool, default=False 是否在 rc 处应用平滑过渡(内外区势值混合) smooth_width : float, default=0.1 平滑区域半宽度(Bohr),仅在 smooth_rc=True 时有效 平滑范围为 [rc-smooth_width, rc+smooth_width] Returns ------- InvertResult 包含反演势和诊断信息 Raises ------ ValueError 如果网格与 tm_result 不匹配 Notes ----- 1. 内区(r ≤ rc):使用 TM 解析导数,从 `_eval_tm_at_rc()` 获取 u, u', u'' 2. 外区(r > rc):使用 `eval_derivatives_at()` 样条法 3. 节点保护:在 |u| < node_tol 附近使用线性插值填充势值 4. 离心势项 l(l+1)/(2r²) 在原点附近奇异,需特殊处理 5. **平滑过渡**(可选):若 smooth_rc=True,在 rc±smooth_width 区域使用 三次样条平滑内外区势值,适用于特殊 rc/网格组合导致跳变较大的情况。 默认关闭(测试显示大多数情况 rc 处相对跳变 < 1%)。 Examples -------- >>> from atomppgen import solve_ae_atom, tm_pseudize >>> ae = solve_ae_atom(Z=13, spin_mode='LDA', lmax=0) >>> tm = tm_pseudize(r=ae.r, w=ae.w, u_ae=ae.u_by_l[0][2], ... eps=ae.eps_by_l[0][2], l=0, rc=2.0) >>> inv = invert_semilocal_potential(tm, ae.r) >>> print(inv.diagnostics['V_at_rc']) # rc 处势值 """ # 参数提取 if u_ps is None: u_ps = tm_result.u_ps if len(r) != len(u_ps): raise ValueError(f"网格长度 {len(r)} 与伪轨道长度 {len(u_ps)} 不匹配") rc = tm_result.rc eps = tm_result.eps l = tm_result.l a_coeff = tm_result.a_coeff # 找到 rc 对应的索引 i_rc = np.searchsorted(r, rc) if i_rc >= len(r): i_rc = len(r) - 1 # 初始化势数组 V_l = np.zeros_like(r) # ========== 内区(r ≤ rc):使用 TM 解析导数 ========== V_l[:i_rc+1] = _invert_inner_analytical( r[:i_rc+1], u_ps[:i_rc+1], a_coeff, l, eps, node_tol, V_max_clip ) # ========== 外区(r > rc):使用样条法导数 ========== if i_rc + 1 < len(r): V_l[i_rc+1:] = _invert_outer_spline( r_full=r, # 传递完整网格 u_full=u_ps, # 传递完整轨道 i_start=i_rc+1, # 外区起始索引 l=l, eps=eps, node_tol=node_tol, V_max_clip=V_max_clip, ) # ========== rc 处平滑过渡(可选) ========== if smooth_rc and i_rc > 0 and i_rc < len(r) - 1: V_l = _smooth_at_rc(r, V_l, i_rc, smooth_width) # ========== 诊断信息 ========== diagnostics = { 'n_nodes': _count_nodes(u_ps, node_tol), 'V_max': float(np.max(V_l)), 'V_min': float(np.min(V_l)), 'V_at_rc': float(V_l[i_rc]), 'method_inner': 'analytical', 'method_outer': 'spline', } return InvertResult( V_l=V_l, r=r, l=l, rc=rc, eps=eps, diagnostics=diagnostics, )
[文档] def _invert_inner_analytical( r: np.ndarray, u: np.ndarray, a_coeff: np.ndarray, l: int, eps: float, node_tol: float, V_max_clip: float, ) -> np.ndarray: """ 内区使用 TM 解析导数反演势 对于 TM 形式 u = r^{l+1} exp(p(r)),可以解析计算: u' = [(l+1)/r + p'] u u'' = [l(l+1)/r² + 2(l+1)p'/r + p'² + p''] u 因此: u''/u = l(l+1)/r² + 2(l+1)p'/r + p'² + p'' 代入: V_l = ε + (1/2) u''/u - l(l+1)/(2r²) = ε + (1/2)[l(l+1)/r² + 2(l+1)p'/r + p'² + p''] - l(l+1)/(2r²) = ε + (l+1)p'/r + (1/2)(p'² + p'') Parameters ---------- r : np.ndarray 内区网格 u : np.ndarray 内区伪轨道 a_coeff : np.ndarray TM 系数 [a_0, a_2, a_4, ...] l : int 角动量 eps : float 伪化能量 node_tol : float 节点容忍度 V_max_clip : float 势裁剪上限 Returns ------- np.ndarray 内区势 V_l(r) """ # 计算 p(r) 及其导数 # p = a_0 + a_2 r² + a_4 r⁴ + ... # p' = 2 a_2 r + 4 a_4 r³ + ... # p'' = 2 a_2 + 12 a_4 r² + ... p = np.zeros_like(r) p1 = np.zeros_like(r) # p' p2 = np.zeros_like(r) # p'' for i in range(len(a_coeff)): p += a_coeff[i] * r**(2*i) if i >= 1: p1 += 2*i * a_coeff[i] * r**(2*i-1) p2 += 2*i * (2*i-1) * a_coeff[i] * r**(2*i-2) # V_l = ε + (l+1)p'/r + (1/2)(p'² + p'') # 处理 r→0 时的奇点:(l+1)p'/r # 当 r 很小时,p' ~ 2 a_2 r,所以 (l+1)p'/r ~ 2(l+1) a_2 V_l = np.zeros_like(r) # 对于 r > 0,使用完整公式 mask_nonzero = (r > node_tol) V_l[mask_nonzero] = ( eps + (l+1) * p1[mask_nonzero] / r[mask_nonzero] + 0.5 * (p1[mask_nonzero]**2 + p2[mask_nonzero]) ) # 对于 r ≈ 0,使用极限值(假设 p' ~ 2 a_2 r) if len(a_coeff) > 1 and not mask_nonzero[0]: V_l[0] = eps + 0.5 * p2[0] # 忽略 p'/r 项(趋于常数) # 裁剪极端值 V_l = np.clip(V_l, -V_max_clip, V_max_clip) return V_l
[文档] def _invert_outer_spline( r_full: np.ndarray, u_full: np.ndarray, i_start: int, l: int, eps: float, node_tol: float, V_max_clip: float, ) -> np.ndarray: """ 外区使用样条法导数反演势 V_l(r) = ε + (1/2) u''/u - l(l+1)/(2r²) Parameters ---------- r_full : np.ndarray 完整网格(包括内外区) u_full : np.ndarray 完整伪轨道(包括内外区) i_start : int 外区起始索引 l : int 角动量 eps : float 伪化能量 node_tol : float 节点容忍度 V_max_clip : float 势裁剪上限 Returns ------- np.ndarray 外区势 V_l(r) """ n_outer = len(r_full) - i_start V_l = np.zeros(n_outer) for i in range(n_outer): i_global = i_start + i r_i = r_full[i_global] u_i = u_full[i_global] # 检查节点 if abs(u_i) < node_tol: # 节点处使用插值(后续处理) V_l[i] = 0.0 # 占位 continue # 使用样条法计算二阶导数 # 传递完整数据,eval_derivatives_at 自动选择窗口 derivs = eval_derivatives_at( r=r_full, u=u_full, x0=r_i, max_order=2, window=7, ) u_val = derivs[0] u2 = derivs[2] # u'' # V_l = ε + (1/2) u''/u - l(l+1)/(2r²) V_l[i] = eps + 0.5 * u2 / u_val - l*(l+1) / (2 * r_i**2) # 节点插值 r_outer = r_full[i_start:] u_outer = u_full[i_start:] V_l = _interpolate_nodes(r_outer, u_outer, V_l, node_tol) # 裁剪 V_l = np.clip(V_l, -V_max_clip, V_max_clip) return V_l
[文档] def _interpolate_nodes( r: np.ndarray, u: np.ndarray, V_l: np.ndarray, node_tol: float, ) -> np.ndarray: """ 在节点附近插值势值 Parameters ---------- r : np.ndarray 网格 u : np.ndarray 轨道 V_l : np.ndarray 势(节点处为占位值) node_tol : float 节点容忍度 Returns ------- np.ndarray 插值后的势 """ # 找到节点 is_node = np.abs(u) < node_tol if not np.any(is_node): return V_l # 对节点区域进行线性插值 V_l_interp = V_l.copy() # 使用 scipy 的插值(跳过节点) r_valid = r[~is_node] V_valid = V_l[~is_node] if len(r_valid) > 1: from scipy.interpolate import interp1d f_interp = interp1d(r_valid, V_valid, kind='linear', fill_value='extrapolate') V_l_interp[is_node] = f_interp(r[is_node]) return V_l_interp
[文档] def _count_nodes(u: np.ndarray, node_tol: float) -> int: """ 计算轨道的节点数(符号变化次数) Parameters ---------- u : np.ndarray 轨道 node_tol : float 容忍度 Returns ------- int 节点数 """ # 符号变化检测 sign_changes = 0 u_filtered = u[np.abs(u) > node_tol] # 过滤掉接近零的点 if len(u_filtered) < 2: return 0 for i in range(1, len(u_filtered)): if u_filtered[i] * u_filtered[i-1] < 0: sign_changes += 1 return sign_changes
def _smooth_at_rc( r: np.ndarray, V_l: np.ndarray, i_rc: int, smooth_width: float, ) -> np.ndarray: """ 在 rc 处应用平滑过渡 在 [rc-smooth_width, rc+smooth_width] 区域使用三次样条平滑势值, 减少内外区计算方法差异导致的跳变。 Parameters ---------- r : np.ndarray 径向网格 V_l : np.ndarray 势(未平滑) i_rc : int rc 对应的网格索引 smooth_width : float 平滑区域半宽度(Bohr) Returns ------- np.ndarray 平滑后的势 """ rc = r[i_rc] r_min = rc - smooth_width r_max = rc + smooth_width # 找到平滑区域索引 i_min = np.searchsorted(r, r_min) i_max = np.searchsorted(r, r_max) # 确保区域有效 i_min = max(0, i_min) i_max = min(len(r), i_max) # 至少需要 4 个点才能构建三次样条 if i_max - i_min < 4: return V_l # 区域太小,跳过平滑 # 提取平滑区域 r_smooth = r[i_min:i_max] V_smooth_region = V_l[i_min:i_max] # 构建三次样条并评估 cs = CubicSpline(r_smooth, V_smooth_region, bc_type='not-a-knot') # 在同一区域重新评估(平滑) V_l_smoothed = V_l.copy() V_l_smoothed[i_min:i_max] = cs(r_smooth) return V_l_smoothed