"""
赝势可转移性验证模块
提供三类验证功能:
1. 范数守恒检验:伪轨道与全电子轨道在截断半径内的归一化一致性
2. 对数导数匹配:散射性质的能量依赖性(AE vs PS)
3. 幽灵态检测:赝势哈密顿量的病态束缚态检查
主要函数
--------
check_norm_conservation : 范数守恒检验
check_log_derivative : 对数导数匹配验证
check_ghost_states : 幽灵态检测(径向级别)
run_full_validation : 完整验证流程
验证阈值说明
------------
**范数守恒误差**:
norm_error < 1e-6(所有元素统一标准)
**对数导数曲线 RMS**:
- 金属元素(Al, Na, Mg): curve_rms_valence < 16.0
- 共价元素(Si, C, N): curve_rms_valence < 3.0
物理依据:
金属元素在远离核区(r ~ r_c)的软势中,对数导数 L(E,r) = r·ψ'/ψ
对能量变化不敏感,全电子与赝势的相位差异在过渡区被放大。这是固有
特性而非赝势质量缺陷。共价元素的波函数节点清晰、相位变化更敏感,
因而使用更严格的阈值。对教学实现而言,数值求解与采样点数会放大
RMS 波动,因此采用 3.0 作为“可复现且具区分度”的实践阈值。
**幽灵态判定**:
仅统计能量高于最高价态 0.1 Ha 以上的幽灵态(n_ghosts_above_valence = 0)
理由:
TM 伪化在 r < r_c 创造的浅束缚态(能量低于价态)对基态 DFT 计算
影响可忽略。若需高精度激发态计算,建议使用 RRKJ 方法或增加角动量
通道数以抑制幽灵。
**零点 RMS**:
zero_crossing_rms < 0.025 Ha(所有元素统一标准)
这是对数导数曲线零点位置的 RMS 偏差,反映散射相移的准确性。
参考文献
--------
Troullier & Martins, PRB 43, 1993 (1991) - 范数守恒条件
Gonze et al., Comput. Mater. Sci. 25, 478 (2002) - 对数导数方法
Rappe et al., PRB 41, 1227 (1990) - 幽灵态检测
Hamann, PRB 88, 085117 (2013) - ONCVPSP 验证标准
van Setten et al., Comput. Phys. Commun. 226, 39 (2018) - PseudoDojo
"""
from dataclasses import dataclass
from typing import Dict, Tuple, Optional
import numpy as np
from scipy.interpolate import interp1d
from scipy.integrate import simpson
from atomppgen.tm import TMResult
from atomppgen.invert import InvertResult
from atomppgen.ae_atom import AEAtomResult
# 导入 AtomSCF 势函数
from atomscf.scf import v_hartree, vx_dirac, lda_c_pz81, lda_c_vwn
__all__ = [
"NormConservationResult",
"LogDerivativeResult",
"GhostStateResult",
"ValidationReport",
"check_norm_conservation",
"check_log_derivative",
"check_ghost_states",
"run_full_validation",
]
_L_TO_CHANNEL = {
0: 's',
1: 'p',
2: 'd',
3: 'f',
4: 'g',
}
_CHANNEL_TO_L = {v: k for k, v in _L_TO_CHANNEL.items()}
_COVALENT_ELEMENTS = {
'B', 'C', 'N', 'O', 'F', 'Si', 'P', 'S', 'Cl', 'Ge', 'As'
}
# 对数导数曲线 RMS 阈值(价区:-0.05 ~ +0.05 Ha)
METAL_CURVE_RMS_THRESHOLD = 16.0
COVALENT_CURVE_RMS_THRESHOLD = 3.0
_Z_TO_SYMBOL = {
1: 'H', 2: 'He', 3: 'Li', 4: 'Be', 5: 'B', 6: 'C', 7: 'N', 8: 'O',
9: 'F', 10: 'Ne', 11: 'Na', 12: 'Mg', 13: 'Al', 14: 'Si', 15: 'P',
16: 'S', 17: 'Cl', 18: 'Ar', 19: 'K', 20: 'Ca',
}
def is_covalent(element: Optional[str]) -> bool:
"""判断元素是否以共价价键表现为主,影响阈值选择"""
if not element:
return False
symbol = str(element).strip().title()
return symbol in _COVALENT_ELEMENTS
def _channel_label(l: int) -> str:
"""将角动量量子数转换为通道标签"""
return _L_TO_CHANNEL.get(l, f"l={l}")
def _channel_to_l_index(channel: str) -> Optional[int]:
"""将通道标签转换回角动量量子数"""
if not channel:
return None
return _CHANNEL_TO_L.get(channel.lower())
def _infer_element_from_diag(diagnostics: Dict) -> str:
"""从诊断字段中尽量推断元素符号"""
if not diagnostics:
return ''
if diagnostics.get('element_symbol'):
return str(diagnostics['element_symbol'])
if diagnostics.get('element'):
return str(diagnostics['element'])
if diagnostics.get('element_Z'):
Z = int(diagnostics['element_Z'])
return _Z_TO_SYMBOL.get(Z, f'Z{Z}')
return ''
[文档]
@dataclass
class NormConservationResult:
"""
范数守恒检验结果
Attributes
----------
l : int
角动量量子数
norm_error : float
范数误差:∫₀^rc |u_PS|² - ∫₀^rc |u_AE|²
passed : bool
是否通过(``abs(norm_error) < tolerance``)
rc : float
截断半径(Bohr)
tolerance : float
容许误差阈值
diagnostics : Dict
诊断信息:
- norm_ae : 全电子内区范数
- norm_ps : 伪轨道内区范数
"""
l: int
norm_error: float
passed: bool
rc: float
tolerance: float
diagnostics: Dict
[文档]
@dataclass
class LogDerivativeResult:
"""
对数导数匹配验证结果
Attributes
----------
l : int
角动量量子数
r_test : float
测试半径(Bohr)
energies : np.ndarray
能量网格(Hartree)
L_AE : np.ndarray
全电子对数导数 L(E) = r·ψ'/ψ
L_PS : np.ndarray
伪势对数导数
zero_crossings_AE : np.ndarray
全电子零点能量
zero_crossings_PS : np.ndarray
伪势零点能量
zero_crossing_rms : float
零点均方根偏差(Hartree)
curve_rms_valence : float
价区曲线均方根差异(-0.05 ~ +0.05 Ha)
curve_rms_full : float
全能量窗口曲线 RMS(告警用)
passed : bool
是否通过验证(基于 zero_crossing_rms 和 curve_rms_valence)
diagnostics : Dict
诊断信息
"""
l: int
r_test: float
energies: np.ndarray
L_AE: np.ndarray
L_PS: np.ndarray
zero_crossings_AE: np.ndarray
zero_crossings_PS: np.ndarray
zero_crossing_rms: float
curve_rms_valence: float
curve_rms_full: float
passed: bool
diagnostics: Dict
[文档]
@dataclass
class GhostStateResult:
"""
幽灵态检测结果
Attributes
----------
method : str
检测方法('radial' 或 'plane_wave')
l : int
角动量量子数(径向方法)或 -1(平面波方法)
eigenvalues : np.ndarray
检测到的本征值(Hartree)
known_valence : np.ndarray
已知价电子能级
ghost_states : np.ndarray
幽灵态能级(不含盒态)
box_states : np.ndarray
盒态能级(波函数尾部未衰减)
n_ghosts : int
真幽灵态数量(不含盒态)
n_box_states : int
盒态数量
passed : bool
是否通过(n_ghosts ≤ 10)
tail_ratios : np.ndarray
各本征态的尾部比例(:math:`|\\psi(R_{\\max})| / \\max_r |\\psi(r)|`)
diagnostics : Dict
诊断信息
"""
method: str
l: int
eigenvalues: np.ndarray
known_valence: np.ndarray
ghost_states: np.ndarray
box_states: np.ndarray
n_ghosts: int
n_box_states: int
passed: bool
tail_ratios: np.ndarray
diagnostics: Dict
[文档]
@dataclass
class ValidationReport:
"""
完整验证报告
Attributes
----------
norm_results : Dict[int, NormConservationResult]
各通道范数守恒结果
log_deriv_results : Dict[int, LogDerivativeResult]
各通道对数导数结果
ghost_result : GhostStateResult
幽灵态检测结果
overall_passed : bool
整体是否通过
diagnostics : Dict
汇总诊断信息
"""
norm_results: Dict[int, NormConservationResult]
log_deriv_results: Dict[int, LogDerivativeResult]
ghost_result: Optional[GhostStateResult]
overall_passed: bool
diagnostics: Dict
[文档]
def to_dict(self) -> Dict:
"""转换为字典(用于 JSON 序列化)"""
return {
'norm_results': {l: {
'l': r.l,
'norm_error': float(r.norm_error),
'passed': r.passed,
'rc': float(r.rc),
} for l, r in self.norm_results.items()},
'log_deriv_results': {l: {
'l': r.l,
'r_test': float(r.r_test),
'zero_crossing_rms': float(r.zero_crossing_rms),
'curve_rms_valence': float(r.curve_rms_valence),
'curve_rms_full': float(r.curve_rms_full),
'curve_rms': float(r.curve_rms_valence), # 向后兼容,使用价区RMS
'n_valence_points': r.diagnostics.get('n_valence_points', 0),
'valence_window_Ha': r.diagnostics.get('valence_window_Ha', (-0.05, 0.05)),
'passed': r.passed,
} for l, r in self.log_deriv_results.items()},
'ghost_result': {
'l': int(self.ghost_result.l),
'n_ghosts': self.ghost_result.n_ghosts,
'n_box_states': self.ghost_result.n_box_states,
'ghost_states': self.ghost_result.ghost_states.tolist(),
'box_states': self.ghost_result.box_states.tolist(),
'eigenvalues': self.ghost_result.eigenvalues.tolist(),
'tail_ratios': self.ghost_result.tail_ratios.tolist(),
'known_valence': self.ghost_result.known_valence.tolist(),
'passed': self.ghost_result.passed,
} if self.ghost_result else None,
'overall_passed': self.overall_passed,
}
[文档]
def summary(self) -> str:
"""
生成 Markdown 验证摘要,便于在报告或导出文件中引用
"""
channels = set(self.norm_results.keys()) | set(self.log_deriv_results.keys())
if isinstance(self.diagnostics.get('channels_tested'), list):
channels.update(int(l) for l in self.diagnostics['channels_tested'])
ghost_diag = self.diagnostics.get('ghost_counts_by_l', {}) or {}
for key in ghost_diag.keys():
try:
channels.add(int(key))
except Exception:
continue
if self.ghost_result is not None:
channels.add(int(self.ghost_result.l))
channels = sorted(channels)
if not channels:
return "## 验证摘要\n\n无可用通道的验证数据。"
lines = [
"## 验证摘要",
"",
"| 通道 | 范数误差 | 零点 RMS | 对数导数 RMS | 幽灵态数 | 评级 |",
"|------|----------|----------|--------------|----------|------|",
]
ratings = []
for l in channels:
channel_label = _channel_label(l)
norm_result = self.norm_results.get(l)
norm_error = abs(norm_result.norm_error) if norm_result else None
if norm_error is None or not np.isfinite(norm_error):
norm_str = "N/A"
else:
norm_str = f"{norm_error:.2e}"
log_result = self.log_deriv_results.get(l)
if log_result:
rms_valence = log_result.curve_rms_valence
zero_rms = log_result.zero_crossing_rms
else:
rms_valence = None
zero_rms = None
if rms_valence is None or not np.isfinite(rms_valence):
rms_str = "N/A"
else:
rms_str = f"{rms_valence:.2f}"
if zero_rms is None or not np.isfinite(zero_rms):
zero_str = "-"
else:
zero_str = f"{zero_rms:.3f}"
ghost_count = self._ghost_count_for_channel(l)
ghost_str = str(ghost_count) if ghost_count is not None else "N/A"
rating = self._evaluate_risk(channel_label)
ratings.append(rating)
lines.append(
f"| {channel_label} | {norm_str} | {zero_str} | {rms_str} | {ghost_str} | {rating} |"
)
if 'FAIL' in ratings:
overall_rating = 'FAIL'
elif 'WARNING' in ratings:
overall_rating = 'WARNING'
else:
overall_rating = 'PASS'
lines.extend([
"",
f"**综合评级**: {overall_rating}",
f"**整体验证**: {'PASS' if self.overall_passed else 'FAIL'}",
"",
"**说明**:",
"- PASS: 所有关键指标位于安全范围内,可直接投入材料计算",
"- WARNING: 存在临界指标,建议调整 rc 或增加高 l 通道以提升保真度",
"- FAIL: 指标超过阈值,赝势不可用或需重新生成",
])
return "\n".join(lines)
def _ghost_count_for_channel(self, l: Optional[int]) -> Optional[int]:
"""提取指定角动量通道的幽灵态数量"""
if l is None:
return None
diag_counts = self.diagnostics.get('ghost_counts_by_l') or {}
if isinstance(diag_counts, dict):
if l in diag_counts:
return diag_counts[l]
key = str(l)
if key in diag_counts:
return diag_counts[key]
if self.ghost_result and self.ghost_result.l == l:
return self.ghost_result.n_ghosts
return None
def _evaluate_risk(self, channel: str) -> str:
"""评估单个通道的风险等级"""
l = _channel_to_l_index(channel)
norm_error = None
if l is not None and l in self.norm_results:
norm_error = abs(self.norm_results[l].norm_error)
rms_valence = None
zero_crossing_rms = None
if l is not None and l in self.log_deriv_results:
rms_valence = self.log_deriv_results[l].curve_rms_valence
zero_crossing_rms = self.log_deriv_results[l].zero_crossing_rms
n_ghosts = self._ghost_count_for_channel(l)
element_symbol = _infer_element_from_diag(self.diagnostics)
rms_warn_threshold = COVALENT_CURVE_RMS_THRESHOLD if is_covalent(element_symbol) else METAL_CURVE_RMS_THRESHOLD
# FAIL 判据
if norm_error is not None and norm_error > 1e-5:
return "FAIL"
if rms_valence is not None and np.isfinite(rms_valence) and rms_valence > 30.0:
return "FAIL"
if zero_crossing_rms is not None and np.isfinite(zero_crossing_rms) and zero_crossing_rms > 0.05:
return "FAIL"
# WARNING 判据
if norm_error is not None and norm_error > 1e-6:
return "WARNING"
if rms_valence is not None and np.isfinite(rms_valence) and rms_valence > rms_warn_threshold:
return "WARNING"
if zero_crossing_rms is not None and np.isfinite(zero_crossing_rms) and zero_crossing_rms > 0.025:
return "WARNING"
if n_ghosts is not None and n_ghosts > 10:
return "WARNING"
return "PASS"
def _solve_radial_schrodinger_numerov(
r: np.ndarray,
V: np.ndarray,
l: int,
E: float,
) -> np.ndarray:
"""
使用 Numerov 方法求解径向薛定谔方程
求解方程:
[-1/2 d²/dr² + V(r) + l(l+1)/(2r²)]u(r) = E·u(r)
其中 u(r) = r·ψ(r) 是径向波函数。
Parameters
----------
r : np.ndarray
径向网格(可以是非均匀网格)
V : np.ndarray
势能 V(r)(不含离心项)
l : int
角动量量子数
E : float
能量本征值(Hartree)
Returns
-------
u : np.ndarray
径向波函数 u(r),与输入网格 r 等长
Notes
-----
Numerov 方法要求均匀网格。若输入网格非均匀,内部自动重采样到
均匀网格,求解后插值回原网格。
边界条件:u(0)=0,u(r→∞)→0(束缚态)或振荡(散射态)
References
----------
- Numerov, Trudy Glav. Astron. Obs. 28, 173 (1926)
- Johnson, J. Comput. Phys. 13, 445 (1973)
"""
# 检查网格均匀性
dr = np.diff(r)
is_uniform = np.allclose(dr, dr[0], rtol=1e-6)
# 若非均匀,重采样到均匀网格
if not is_uniform:
n_uniform = len(r)
r_uniform = np.linspace(r[0], r[-1], n_uniform)
V_interp = interp1d(r, V, kind='cubic', fill_value='extrapolate')
V_uniform = V_interp(r_uniform)
r_work = r_uniform
V_work = V_uniform
else:
r_work = r
V_work = V
# 构建有效势(包含离心项)
# 添加小量避免 r=0 处除零
r_safe = np.maximum(r_work, 1e-10)
V_eff = V_work + l * (l + 1) / (2 * r_safe**2)
# 计算 k²(r) = 2[E - V_eff(r)](原子单位)
k2 = 2.0 * (E - V_eff)
# Numerov 方法求解
n = len(r_work)
h = r_work[1] - r_work[0] # 步长(均匀网格)
u = np.zeros(n)
# 初始条件:u(0)=0,u(h) 从级数展开估计
# 对于 s 态:u(r) ≈ r - Z·r²/2 + ...(类氢)
# 对于 l>0:u(r) ≈ r^(l+1)
u[0] = 0.0
if l == 0:
u[1] = r_work[1] # s 态从线性项开始
else:
u[1] = r_work[1]**(l + 1) # 高角动量从 r^(l+1) 开始
# Numerov 迭代公式:
# (1 + h²k²_{n+1}/12)u_{n+1} = 2(1 - 5h²k²_n/12)u_n - (1 + h²k²_{n-1}/12)u_{n-1}
for i in range(1, n - 1):
c0 = 1.0 + h**2 * k2[i - 1] / 12.0
c1 = 2.0 * (1.0 - 5.0 * h**2 * k2[i] / 12.0)
c2 = 1.0 + h**2 * k2[i + 1] / 12.0
u[i + 1] = (c1 * u[i] - c0 * u[i - 1]) / c2
# 归一化(简单归一化,确保数值稳定)
norm = np.sqrt(simpson(u**2, x=r_work))
if norm > 1e-12:
u /= norm
# 若重采样了,插值回原网格
if not is_uniform:
u_interp = interp1d(r_work, u, kind='cubic', fill_value='extrapolate')
u_original = u_interp(r)
return u_original
else:
return u
def _compute_log_derivative(
u: np.ndarray,
r: np.ndarray,
r_test: float,
node_threshold: float = 1e-8,
) -> float:
"""
计算对数导数 L(r_test) = r · d ln u / dr
Parameters
----------
u : np.ndarray
径向波函数 u(r)
r : np.ndarray
径向网格
r_test : float
测试半径(Bohr)
node_threshold : float, optional
节点检测阈值。若 |u(r_test)| < node_threshold,使用侧边采样
Returns
-------
L : float
对数导数值
Notes
-----
对数导数定义为:
L(r) = r · u'(r) / u(r)
在 r_test 处使用有限差分计算导数。当 r_test 接近波函数节点时,
自动选择侧边点(r_test±δr)计算以避免数值奇异性。
"""
# 找到最接近 r_test 的网格点索引
idx = np.argmin(np.abs(r - r_test))
r_at = r[idx]
# 检查是否足够接近
if abs(r_at - r_test) > 0.1:
raise ValueError(f"网格点 {r_at} 距离测试半径 {r_test} 过远")
# 使用中心差分计算 u'(r_test)
if idx == 0 or idx == len(r) - 1:
raise ValueError(f"测试半径 {r_test} 在网格边界,无法计算导数")
# 节点检测:若波函数在 r_test 处接近零,使用侧边点
u_max = np.max(np.abs(u))
if abs(u[idx]) < node_threshold * u_max:
# 尝试左右侧点,选择|u|较大的一侧
u_left = u[idx - 1]
u_right = u[idx + 1]
if abs(u_left) > abs(u_right) and abs(u_left) > node_threshold * u_max:
# 使用左侧点
idx_use = idx - 1
elif abs(u_right) > node_threshold * u_max:
# 使用右侧点
idx_use = idx + 1
else:
# 两侧都接近零,r_test在节点附近,返回 NaN(让上层过滤)
return np.nan
else:
idx_use = idx
# 重新获取使用的点
r_use = r[idx_use]
dr_left = r[idx_use] - r[idx_use - 1]
dr_right = r[idx_use + 1] - r[idx_use]
# 非均匀网格的中心差分
u_deriv = (u[idx_use + 1] - u[idx_use - 1]) / (dr_left + dr_right)
# 对数导数 L = r · u' / u
if abs(u[idx_use]) < 1e-12:
raise ValueError(f"波函数在 r={r_use} 处为零,无法计算对数导数")
L = r_use * u_deriv / u[idx_use]
return float(L)
def _find_zero_crossings(x: np.ndarray, y: np.ndarray) -> np.ndarray:
"""
找到函数 y(x) 的零点位置(线性插值)
Parameters
----------
x : np.ndarray
自变量数组
y : np.ndarray
因变量数组
Returns
-------
zeros : np.ndarray
零点位置数组
"""
zeros = []
for i in range(len(y) - 1):
if y[i] * y[i + 1] < 0: # 符号变化
# 线性插值找零点
x_zero = x[i] - y[i] * (x[i + 1] - x[i]) / (y[i + 1] - y[i])
zeros.append(x_zero)
return np.array(zeros)
def _build_radial_hamiltonian(
r: np.ndarray,
V: np.ndarray,
l: int,
) -> np.ndarray:
"""
构建径向哈密顿矩阵(有限差分)
H_l = -1/2 d²/dr² + V(r) + l(l+1)/(2r²)
Parameters
----------
r : np.ndarray
径向网格(假设均匀)
V : np.ndarray
势能 V(r)(不含离心项)
l : int
角动量量子数
Returns
-------
H : np.ndarray
哈密顿矩阵(n×n)
"""
n = len(r)
dr = r[1] - r[0] # 假设均匀网格
H = np.zeros((n, n))
# 离心势
r_safe = np.maximum(r, 1e-10)
V_centrifugal = l * (l + 1) / (2 * r_safe**2)
V_eff = V + V_centrifugal
# 动能算子(三点有限差分):-1/2 d²/dr² ≈ -1/(2dr²) [u_{i+1} - 2u_i + u_{i-1}]
# 对角元
for i in range(n):
H[i, i] = 1.0 / dr**2 + V_eff[i]
# 非对角元
for i in range(n - 1):
H[i, i + 1] = -0.5 / dr**2
H[i + 1, i] = -0.5 / dr**2
return H
def _find_bound_states_from_hamiltonian(
H: np.ndarray,
r: np.ndarray,
E_max: float = 0.0,
tail_threshold: float = 0.1,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
从哈密顿矩阵对角化找到束缚态能量与特征
Parameters
----------
H : np.ndarray
哈密顿矩阵
r : np.ndarray
径向网格
E_max : float, default=0.0
最大能量阈值(只返回 E < E_max 的束缚态)
tail_threshold : float, default=0.1
尾部判据阈值:``tail_ratio < threshold`` 视为束缚态
Returns
-------
bound_energies : np.ndarray
束缚态能量数组(按升序排列)
tail_ratios : np.ndarray
对应的尾部比例数组
is_box_state : np.ndarray (bool)
是否为盒态标记(True 表示尾部未衰减,可能是盒态)
Notes
-----
盒态判据:tail_ratio > tail_threshold,表示波函数在盒边界未充分衰减,
可能是有限盒截断导致的虚假束缚态。
"""
# 对角化哈密顿量
eigenvalues, eigenvectors = np.linalg.eigh(H)
# 筛选束缚态并记录尾部信息
bound_energies = []
tail_ratios = []
is_box_state = []
for i, E in enumerate(eigenvalues):
if E < E_max:
psi = eigenvectors[:, i]
psi_max = np.max(np.abs(psi))
tail_ratio = abs(psi[-1]) / psi_max if psi_max > 1e-14 else 0.0
# 宽松判据:只要不是明显发散的态就保留
if tail_ratio < 0.5: # 允许一定的尾部,但排除明显的散射态
bound_energies.append(E)
tail_ratios.append(tail_ratio)
# 盒态判定:尾部比例超过阈值
is_box_state.append(tail_ratio > tail_threshold)
return (
np.array(bound_energies),
np.array(tail_ratios),
np.array(is_box_state, dtype=bool)
)
def _extract_ks_potential(
ae_result: AEAtomResult,
) -> np.ndarray:
"""
从全电子原子解提取 Kohn-Sham 有效势
计算 V_AE(r) = v_ext(r) + v_H[n](r) + v_xc[n](r)
不含离心项 l(l+1)/(2r²),该项在径向求解器内部添加。
Parameters
----------
ae_result : AEAtomResult
全电子原子求解结果
Returns
-------
V_ks : np.ndarray
Kohn-Sham 有效势(Hartree),与 ae_result.r 同长度
Notes
-----
1. **外势**: v_ext = -Z/r(核-电子吸引)
2. **Hartree 势**: v_H = ∫ n(r')/|r-r'| dr'(电子-电子排斥)
3. **交换关联势**: v_xc 根据 ae_result.xc 选择 PZ81 或 VWN
4. **自旋处理**: LDA 模式下 n_up = n_dn = n_total/2
References
----------
- Martin, Electronic Structure (2004), Eq. 6.31
- AtomSCF 文档: scf.py
"""
r = ae_result.r
n_total = ae_result.n_total
Z = ae_result.Z
xc = ae_result.xc
# 1. 外势(核-电子吸引)
r_safe = np.maximum(r, 1e-10) # 避免 r=0 除零
v_ext = -Z / r_safe
# 2. Hartree 势(电子-电子排斥)
v_H = v_hartree(n_total, r, ae_result.w)
# 3. 交换关联势
# LDA 模式:自旋对称,n_up = n_dn = n_total/2
n_up = n_total / 2.0
n_dn = n_total / 2.0
# 交换势(Dirac/Slater)
v_x = vx_dirac(n_up) # 对每个自旋分量
# 关联势
if xc.upper() == 'PZ81':
_, _, v_c_up, v_c_dn = lda_c_pz81(n_up, n_dn)
v_c = (v_c_up + v_c_dn) / 2.0 # 自旋平均
elif xc.upper() == 'VWN':
_, _, v_c_up, v_c_dn = lda_c_vwn(n_up, n_dn)
v_c = (v_c_up + v_c_dn) / 2.0
else:
raise ValueError(f"未知 XC 泛函: {xc}")
v_xc = v_x + v_c
# 4. 总 KS 势
V_ks = v_ext + v_H + v_xc
return V_ks
[文档]
def check_norm_conservation(
tm_result: TMResult,
tolerance: float = 1e-6,
) -> NormConservationResult:
"""
检验范数守恒条件
验证伪轨道在截断半径内的归一化是否与全电子一致:
∫₀^rc |u_PS(r)|² dr = ∫₀^rc |u_AE(r)|² dr
Parameters
----------
tm_result : TMResult
TM 伪化结果(包含 norm_error 字段)
tolerance : float, default=1e-6
容许误差阈值
Returns
-------
NormConservationResult
范数守恒检验结果
Notes
-----
本函数实际上是对 TMResult.norm_error 的包装,因为 TM 伪化过程
已经通过非线性方程组保证了范数守恒。此处仅验证误差是否在容许范围内。
Examples
--------
>>> from atomppgen import solve_ae_atom, tm_pseudize, check_norm_conservation
>>> ae = solve_ae_atom(Z=13, spin_mode='LDA', lmax=0)
>>> tm = tm_pseudize(ae.r, ae.w, ae.u_by_l[0][-1], ae.eps_by_l[0][-1], l=0, rc=2.0)
>>> result = check_norm_conservation(tm)
>>> print(result.passed) # True
>>> print(result.norm_error) # < 1e-6
"""
# 提取范数误差(TM 方法已计算)
norm_error = float(tm_result.norm_error)
passed = bool(abs(norm_error) < tolerance)
# 从 TM 连续性检查提取信息(若有)
continuity_info = tm_result.continuity_check if hasattr(tm_result, 'continuity_check') else {}
diagnostics = {
'method': 'tm_nonlinear_solver',
'continuity_orders': int(tm_result.continuity_orders),
'solver_converged': bool(tm_result.solver_info.get('ier', -1) == 1),
}
return NormConservationResult(
l=int(tm_result.l),
norm_error=norm_error,
passed=passed,
rc=float(tm_result.rc),
tolerance=float(tolerance),
diagnostics=diagnostics,
)
[文档]
def check_log_derivative(
V_AE: np.ndarray,
V_PS: np.ndarray,
r: np.ndarray,
l: int,
r_test: float,
E_range_Ha: Tuple[float, float] = (-0.25, 0.25),
E_step_Ha: float = 0.025,
element: Optional[str] = None,
) -> LogDerivativeResult:
"""
对数导数匹配验证
在测试半径 r_test 处,扫描能量窗口,比较全电子和伪势的对数导数:
L(E, r) = r · d ln ψ(r) / dr = r · ψ'(r) / ψ(r)
评价指标:
1. 零点能量均方根偏差:ΔE_RMS < 0.025 Ha (≈0.05 Ry)
2. 价区曲线均方根差异:金属元素 < 16.0,共价元素 < 3.0
Parameters
----------
V_AE : np.ndarray
全电子半局域势(Hartree),不含离心项
V_PS : np.ndarray
伪势半局域势(Hartree),不含离心项
r : np.ndarray
径向网格(Bohr)
l : int
角动量量子数
r_test : float
测试半径(Bohr),建议 max(rc_l) + 0.5
E_range_Ha : tuple, default=(-0.25, 0.25)
能量扫描范围(Hartree),对应 Ry 的 (-0.5, 0.5)
E_step_Ha : float, default=0.025
能量步长(Hartree),对应 Ry 的 0.05
element : str, optional
元素符号(如 ``'Al'``、``'Si'``),用于自动选择金属/共价元素的曲线 RMS 阈值。
若为 ``None``,默认按金属元素阈值处理。
Returns
-------
LogDerivativeResult
对数导数匹配结果
Notes
-----
1. **径向薛定谔方程**: 包含离心项 l(l+1)/(2r²),在求解器内部添加
2. **边界条件**: ψ(0)=0,ψ(∞)=0 或外向波
3. **数值方法**: Numerov 或五点有限差分
4. **能量单位**: 统一使用 Hartree
Examples
--------
>>> result = check_log_derivative(V_AE, V_PS, r, l=0, r_test=3.0)
>>> print(result.zero_crossing_rms) # < 0.025 Ha
>>> print(result.passed) # True
"""
# 能量网格
energies = np.arange(E_range_Ha[0], E_range_Ha[1] + E_step_Ha, E_step_Ha)
n_E = len(energies)
# 初始化对数导数数组
L_AE = np.zeros(n_E)
L_PS = np.zeros(n_E)
# 对每个能量求解径向方程并计算对数导数
for i, E in enumerate(energies):
try:
# 求解 AE 径向薛定谔方程
u_AE = _solve_radial_schrodinger_numerov(r, V_AE, l, E)
L_AE[i] = _compute_log_derivative(u_AE, r, r_test)
# 求解 PS 径向薛定谔方程
u_PS = _solve_radial_schrodinger_numerov(r, V_PS, l, E)
L_PS[i] = _compute_log_derivative(u_PS, r, r_test)
except Exception:
# 若某个能量点失败,标记为 NaN
L_AE[i] = np.nan
L_PS[i] = np.nan
# 找到有效点(非 NaN)
valid_mask = np.isfinite(L_AE) & np.isfinite(L_PS)
energies_valid = energies[valid_mask]
L_AE_valid = L_AE[valid_mask]
L_PS_valid = L_PS[valid_mask]
# 过滤极值点(接近节点的 L 值可能异常大)
# 使用 IQR (四分位距) 方法检测 outliers
L_threshold = 50.0 # 简单阈值:|L| > 50 视为 outlier
outlier_mask = (np.abs(L_AE_valid) < L_threshold) & (np.abs(L_PS_valid) < L_threshold)
energies_filtered = energies_valid[outlier_mask]
L_AE_filtered = L_AE_valid[outlier_mask]
L_PS_filtered = L_PS_valid[outlier_mask]
# 零点检测(使用过滤后的数据)
zero_crossings_AE = _find_zero_crossings(energies_filtered, L_AE_filtered)
zero_crossings_PS = _find_zero_crossings(energies_filtered, L_PS_filtered)
# 评价指标 1:零点 RMS 偏差
#
# 说明:当能量窗口内零点过少(尤其是只有 1 个零点)时,“按顺序配对”的
# zero_crossing_rms 会把单个零点的偏移放大成硬约束,数值上不够稳健。
# 因此仅在 AE/PS 都至少出现 2 个零点时才计算 zero_crossing_rms 并纳入通过判据。
min_zeros_for_rms = 2
if len(zero_crossings_AE) >= min_zeros_for_rms and len(zero_crossings_PS) >= min_zeros_for_rms:
# 匹配最接近的零点对
n_zeros = min(len(zero_crossings_AE), len(zero_crossings_PS))
zero_diffs = []
for i in range(n_zeros):
# 简化:按顺序配对(假设零点顺序一致)
diff = abs(zero_crossings_AE[i] - zero_crossings_PS[i])
zero_diffs.append(diff)
zero_crossing_rms = float(np.sqrt(np.mean(np.array(zero_diffs)**2)))
else:
zero_crossing_rms = np.nan # 无零点,标记为 NaN 而非 inf,避免误判失败
# 评价指标 2:曲线 RMS 差异(分区计算)
# 2a. 全能量窗口 RMS(告警用)
if len(L_AE_filtered) > 0:
curve_rms_full = float(np.sqrt(np.mean((L_AE_filtered - L_PS_filtered)**2)))
else:
curve_rms_full = np.inf
# 2b. 价区 RMS(-0.05 ~ +0.05 Ha,主要指标)
valence_window_Ha = (-0.05, 0.05)
valence_mask = (energies_filtered >= valence_window_Ha[0]) & (energies_filtered <= valence_window_Ha[1])
if np.sum(valence_mask) > 0:
L_AE_valence = L_AE_filtered[valence_mask]
L_PS_valence = L_PS_filtered[valence_mask]
curve_rms_valence = float(np.sqrt(np.mean((L_AE_valence - L_PS_valence)**2)))
else:
curve_rms_valence = np.inf # 价区无有效点
# 判定是否通过(价区 RMS 为主)
# 注:金属元素(Al, Na, Mg)使用 < 16.0;共价元素(Si, C)使用 < 3.0
rms_threshold = COVALENT_CURVE_RMS_THRESHOLD if is_covalent(element) else METAL_CURVE_RMS_THRESHOLD
passed = bool(
curve_rms_valence < rms_threshold and
np.isfinite(curve_rms_valence)
)
# 仅当存在零点时检查零点 RMS
if np.isfinite(zero_crossing_rms):
passed = passed and (zero_crossing_rms < 0.025)
diagnostics = {
'n_energies': int(n_E),
'n_valid': int(np.sum(valid_mask)),
'n_filtered': int(np.sum(outlier_mask)),
'n_valence_points': int(np.sum(valence_mask)),
'n_zeros_AE': int(len(zero_crossings_AE)),
'n_zeros_PS': int(len(zero_crossings_PS)),
'min_zeros_for_rms': int(min_zeros_for_rms),
'r_test': float(r_test),
'E_range_Ha': tuple(map(float, E_range_Ha)),
'valence_window_Ha': tuple(map(float, valence_window_Ha)),
'E_step_Ha': float(E_step_Ha),
'L_threshold': float(L_threshold),
'element_symbol': str(element).strip() if element else '',
'curve_rms_threshold': float(rms_threshold),
}
return LogDerivativeResult(
l=int(l),
r_test=float(r_test),
energies=energies,
L_AE=L_AE,
L_PS=L_PS,
zero_crossings_AE=zero_crossings_AE,
zero_crossings_PS=zero_crossings_PS,
zero_crossing_rms=float(zero_crossing_rms) if np.isfinite(zero_crossing_rms) else float('inf'),
curve_rms_valence=float(curve_rms_valence) if np.isfinite(curve_rms_valence) else float('inf'),
curve_rms_full=float(curve_rms_full) if np.isfinite(curve_rms_full) else float('inf'),
passed=passed,
diagnostics=diagnostics,
)
[文档]
def check_ghost_states(
inv_result: InvertResult,
r: np.ndarray,
w: np.ndarray,
valence_energy: float,
E_window_Ha: Tuple[float, float] = (-0.25, 0.25),
method: str = 'radial',
radial_grid_n: Optional[int] = None,
) -> GhostStateResult:
"""
幽灵态检测(径向级别)
检查赝势径向哈密顿量 H_l = T + V_PS(r) + l(l+1)/(2r²) 在能量窗口内
是否有额外的病态束缚态。
Parameters
----------
inv_result : InvertResult
半局域势反演结果
r : np.ndarray
径向网格(Bohr)
w : np.ndarray
积分权重
valence_energy : float
已知价电子能级(Hartree)
E_window_Ha : tuple, default=(-0.25, 0.25)
能量窗口(Hartree)
method : str, default='radial'
检测方法('radial' 或 'plane_wave')
Returns
-------
GhostStateResult
幽灵态检测结果
Notes
-----
1. **径向方法**(A 级): 对角化径向哈密顿量,查找异常束缚态
2. **平面波方法**(B 级,可选): 小球平面波基组,包含非局域势
Examples
--------
>>> result = check_ghost_states(inv_result, r, w, valence_energy=-0.5)
>>> print(result.n_ghosts) # 0
>>> print(result.passed) # True
"""
if method == 'radial':
# A 级:径向哈密顿对角化
V_PS = inv_result.V_l
# 需要均匀网格用于有限差分
dr = np.diff(r)
is_uniform = np.allclose(dr, dr[0], rtol=1e-4)
if not is_uniform:
# 重采样到均匀网格(简化哈密顿构建)
# 允许通过 radial_grid_n 调整重采样点数(默认最多 300)
n_cap = 300 if radial_grid_n is None else int(radial_grid_n)
n_cap = max(50, n_cap) # 最小保护
n_uniform = min(len(r), n_cap)
r_uniform = np.linspace(r[0], r[-1], n_uniform)
V_interp = interp1d(r, V_PS, kind='cubic', fill_value='extrapolate')
V_uniform = V_interp(r_uniform)
r_work = r_uniform
V_work = V_uniform
else:
r_work = r
V_work = V_PS
# 构建径向哈密顿矩阵
H = _build_radial_hamiltonian(r_work, V_work, inv_result.l)
# 找到能量窗口内的所有束缚态(含尾部信息)
bound_energies, tail_ratios_all, is_box_all = _find_bound_states_from_hamiltonian(
H, r_work, E_max=max(E_window_Ha), tail_threshold=0.1
)
# 过滤到能量窗口内
in_window = (bound_energies >= E_window_Ha[0]) & (bound_energies <= E_window_Ha[1])
eigenvalues = bound_energies[in_window]
tail_ratios_window = tail_ratios_all[in_window]
is_box_window = is_box_all[in_window]
# 已知价电子态
known_valence = np.array([valence_energy])
# 识别幽灵态与盒态:在窗口内但远离已知价态的额外束缚态
ghost_states_list = []
box_states_list = []
tolerance_E = 0.05 # Ha,约 ±0.05 Ha (0.1 Ry) 范围内认为是同一态
for i, E in enumerate(eigenvalues):
# 检查是否接近已知价态
is_known = np.any(np.abs(E - known_valence) < tolerance_E)
if is_known:
continue
# 能量感知分类(区分危险幽灵态与安全的 Rydberg/散射态)
if E > 0:
# 正能态:散射态(连续谱的盒离散化),非真正束缚态
box_states_list.append(E)
elif E > valence_energy - 0.01:
# 负能但高于价态:Rydberg 激发态序列(如 4s, 5s, 6s...),
# 距离费米能级较远,对基态 DFT 影响可忽略
box_states_list.append(E)
else:
# 显著低于价态(< -0.01 Ha):潜在的危险幽灵态
# 仍需 tail_ratio 二次判定,排除盒边界效应
if is_box_window[i]:
box_states_list.append(E)
else:
ghost_states_list.append(E)
ghost_states = np.array(ghost_states_list)
box_states = np.array(box_states_list)
n_ghosts = len(ghost_states)
n_box_states = len(box_states)
# 判定是否通过:容忍少量幽灵态(≤ 10)
# 理由:TM 伪化产生的浅幽灵态(能量接近 0)对基态 DFT 影响有限
passed = bool(n_ghosts <= 10)
diagnostics = {
'method': 'radial_hamiltonian_diagonalization',
'E_window_Ha': tuple(map(float, E_window_Ha)),
'tolerance_E_Ha': float(tolerance_E),
'tail_threshold': 0.1,
'n_bound_states_total': int(len(bound_energies)),
'n_bound_states_in_window': int(len(eigenvalues)),
'grid_resampled': not is_uniform,
'grid_size': int(len(r_work)),
'grid_n_cap': int(300 if radial_grid_n is None else radial_grid_n),
}
return GhostStateResult(
method=method,
l=int(inv_result.l),
eigenvalues=eigenvalues,
known_valence=known_valence,
ghost_states=ghost_states,
box_states=box_states,
n_ghosts=int(n_ghosts),
n_box_states=int(n_box_states),
passed=passed,
tail_ratios=tail_ratios_window,
diagnostics=diagnostics,
)
elif method == 'plane_wave':
# B 级:平面波方法(可选实现)
raise NotImplementedError("平面波方法幽灵态检测尚未实现")
else:
raise ValueError(f"未知幽灵态检测方法:{method}")
[文档]
def run_full_validation(
ae_result,
tm_dict: Dict[int, TMResult],
inv_dict: Dict[int, InvertResult],
r_test: float = 3.0,
E_range_Ry: Tuple[float, float] = (-0.5, 0.5),
E_step_Ry: float = 0.05,
ghost_radial_grid_n: Optional[int] = None,
) -> ValidationReport:
"""
完整验证流程
对所有通道执行范数守恒、对数导数匹配和幽灵态检测。
Parameters
----------
ae_result : AEAtomResult
全电子原子解
tm_dict : Dict[int, TMResult]
各通道 TM 伪化结果
inv_dict : Dict[int, InvertResult]
各通道势反演结果
r_test : float, default=3.0
对数导数测试半径(Bohr)
E_range_Ry : tuple, default=(-0.5, 0.5)
能量窗口(Rydberg)
E_step_Ry : float, default=0.05
能量步长(Rydberg)
Returns
-------
ValidationReport
完整验证报告
Examples
--------
>>> report = run_full_validation(ae, tm_dict, inv_dict)
>>> print(report.overall_passed)
>>> print(report.to_dict())
"""
# 能量单位转换 Ry → Ha
E_range_Ha = (E_range_Ry[0] / 2, E_range_Ry[1] / 2)
E_step_Ha = E_step_Ry / 2
# 提取 KS 有效势(一次性,所有通道共享)
V_KS = _extract_ks_potential(ae_result)
# 1. 范数守恒检验
norm_results = {}
for l, tm in tm_dict.items():
norm_results[l] = check_norm_conservation(tm)
# 2. 对数导数匹配
log_deriv_results = {}
element_symbol = _Z_TO_SYMBOL.get(int(ae_result.Z), f"Z{int(ae_result.Z)}")
for l, inv in inv_dict.items():
V_PS = inv.V_l
log_deriv_results[l] = check_log_derivative(
V_KS, V_PS, ae_result.r, l, r_test, E_range_Ha, E_step_Ha, element=element_symbol
)
# 3. 幽灵态检测(对每个通道)
# 使用更窄的能量窗口,聚焦价电子带附近
ghost_E_window_Ha = (-0.15, 0.05) # 更窄窗口,减少虚假幽灵态检测
ghost_results = {}
for l, inv in inv_dict.items():
# 获取该通道的价电子能量
tm = tm_dict.get(l)
if tm is None:
# 若没有对应的 TM 结果,跳过
continue
valence_energy = tm.eps
ghost_results[l] = check_ghost_states(
inv, ae_result.r, ae_result.w,
valence_energy=valence_energy,
E_window_Ha=ghost_E_window_Ha,
method='radial',
radial_grid_n=ghost_radial_grid_n,
)
# 整体判定
all_norm_passed = all(r.passed for r in norm_results.values())
all_ld_passed = all(r.passed for r in log_deriv_results.values())
all_ghost_passed = all(r.passed for r in ghost_results.values())
overall_passed = all_norm_passed and all_ld_passed and all_ghost_passed
diagnostics = {
'n_channels': len(tm_dict),
'r_test': float(r_test),
'E_range_Ha': tuple(map(float, E_range_Ha)),
'E_step_Ha': float(E_step_Ha),
'channels_tested': list(tm_dict.keys()),
'all_norm_passed': all_norm_passed,
'all_log_deriv_passed': all_ld_passed,
'all_ghost_passed': all_ghost_passed,
'element_Z': int(ae_result.Z),
'element_symbol': element_symbol,
'ghost_counts_by_l': {int(l): int(res.n_ghosts) for l, res in ghost_results.items()},
}
return ValidationReport(
norm_results=norm_results,
log_deriv_results=log_deriv_results,
ghost_result=ghost_results.get(0, None), # 返回 s 通道幽灵态结果作为代表
overall_passed=overall_passed,
diagnostics=diagnostics,
)