from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, Tuple, List
import numpy as np
from .grid import trapezoid_weights
from .hartree import v_hartree
from .operator import (
solve_bound_states_fd,
solve_bound_states_fd5,
solve_bound_states_fd5_auxlinear,
solve_bound_states_transformed,
)
from .numerov import (
numerov_eigen_log_ground,
numerov_find_k_log,
numerov_find_k_log_matching,
)
from .shooting import shooting_refine_energy
from .occupations import OrbitalSpec, default_occupations
from .utils import trapz, normalize_radial_u
from .xc.lda import vx_dirac, lda_c_pz81, ex_dirac_density
from .xc.vwn import lda_c_vwn
__all__ = [
"SCFConfig",
"SCFResult",
"run_lsda_x_only",
"run_lsda_pz81",
"run_lsda_vwn",
]
def _estimate_energy_bounds(Z: int, l: int) -> tuple[float, float]:
"""基于氢样近似估计该 (Z,l) 通道的能量搜索窗口。
- 近似基态能量:E ~ -Z^2/(2 n^2),n >= l+1;
- 留出 50% 的安全裕度,并将上界设置为浅束缚阈值(-1e-2)。
"""
n_min = max(l + 1, 1)
e_est = -(Z * Z) / (2.0 * n_min * n_min)
eps_min = 1.5 * e_est # 更深一些(更负)
eps_max = -1e-2
return eps_min, eps_max
def _solve_channel(
cfg: "SCFConfig", r: np.ndarray, l: int, v: np.ndarray, k: int
) -> tuple[np.ndarray, np.ndarray]:
"""根据 cfg.eig_solver 选择求解器,返回 (eps, U)。
- transformed:变量变换方法(需要 cfg.delta 和 cfg.Rp)
- numerov:要求 r 为对数等距网格(ln r 等差)。
- fd5:等间距线性网格五点格式(不满足时回退到 fd)。
- fd:默认二阶 FD(非均匀)。
注意:非等距网格上的二阶FD矩阵不对称,不能用eigh,会自动切换到FD5-aux。
"""
if cfg.eig_solver == "transformed":
# 变量变换方法:需要 delta 和 Rp 参数
if cfg.delta is None or cfg.Rp is None:
raise ValueError(
"eig_solver='transformed' 需要提供 cfg.delta 和 cfg.Rp 参数"
)
eps, U = solve_bound_states_transformed(
r, l=l, v_of_r=v, delta=cfg.delta, Rp=cfg.Rp, k=k, use_sparse=True
)
# 轻量打靶细化(仅细化通道内第一个束缚态,常用于 2p)
if getattr(cfg, "shooting_refine", False):
# 仅当请求的 (n,l) 在 shooting_channels 中时才细化;
# 此处仅细化最低态,对应 n = l + 1
n_quantum = l + 1
if (n_quantum, l) in getattr(cfg, "shooting_channels", []):
if len(eps) >= 1 and eps[0] < 0:
try:
e_ref, u_ref = shooting_refine_energy(
r=r,
l=l,
v_eff=v,
E_initial=float(eps[0]),
target_norm=1.0,
method="rk4",
E_tol=(cfg.shooting_E_tol if hasattr(cfg, "shooting_E_tol") and cfg.shooting_E_tol is not None else 1e-6),
max_iter=(cfg.shooting_max_iter if hasattr(cfg, "shooting_max_iter") and cfg.shooting_max_iter is not None else 50),
)
eps = np.array([e_ref] + list(eps[1:]))
U[0] = u_ref
except Exception:
# 打靶失败时保持原值不变
pass
return eps, U
if cfg.eig_solver == "numerov":
try:
# 使用匹配法/节点计数的 Numerov:对数等距网格;能量窗口基于氢样估计
eps_min, eps_max = _estimate_energy_bounds(cfg.Z, l)
# 直接尝试取所需 k 个态,失败再整体回退到 FD
from .numerov import numerov_find_k_log_by_nodes
eps, U = numerov_find_k_log_by_nodes(
r,
v,
l,
k=k,
eps_min=eps_min,
eps_max=eps_max,
samples=(cfg.numerov_samples if hasattr(cfg, "numerov_samples") and cfg.numerov_samples is not None else 120),
bisection_iter=(
cfg.numerov_bisection_iter
if hasattr(cfg, "numerov_bisection_iter") and cfg.numerov_bisection_iter is not None
else 50
),
)
wloc = trapezoid_weights(r)
for j in range(U.shape[0]):
U[j], _ = normalize_radial_u(U[j], r, wloc)
return eps, U
except Exception:
# 回退
pass
if cfg.eig_solver == "fd5":
try:
return solve_bound_states_fd5(r, l=l, v_of_r=v, k=k)
except Exception:
pass
if cfg.eig_solver == "fd5_aux":
return solve_bound_states_fd5_auxlinear(r, l=l, v_of_r=v, k=k)
# 默认 fd 求解器:检测网格是否等距
# 非等距网格的FD二阶导数矩阵不对称,不能用eigh,必须改用FD5-aux
dr = np.diff(r)
is_uniform = np.allclose(dr, dr[0], atol=1e-12, rtol=0)
if not is_uniform:
# 非等距网格:自动切换到FD5-aux稳妥路径
return solve_bound_states_fd5_auxlinear(r, l=l, v_of_r=v, k=k)
else:
# 等距网格:可以安全使用FD
eps, U = solve_bound_states_fd(r, l=l, v_of_r=v, k=k)
# 可选:在非 transformed 求解器下,同样支持打靶细化
if getattr(cfg, "shooting_refine", False):
n_quantum = l + 1
if (n_quantum, l) in getattr(cfg, "shooting_channels", []):
if len(eps) >= 1 and eps[0] < 0:
try:
e_ref, u_ref = shooting_refine_energy(
r=r,
l=l,
v_eff=v,
E_initial=float(eps[0]),
target_norm=1.0,
method="rk4",
E_tol=(cfg.shooting_E_tol if hasattr(cfg, "shooting_E_tol") and cfg.shooting_E_tol is not None else 1e-6),
max_iter=(cfg.shooting_max_iter if hasattr(cfg, "shooting_max_iter") and cfg.shooting_max_iter is not None else 50),
)
eps = np.array([e_ref] + list(eps[1:]))
U[0] = u_ref
except Exception:
pass
return eps, U
[文档]
@dataclass
class SCFConfig:
r"""SCF 配置参数。
Attributes
----------
Z : int
原子序数。
r : numpy.ndarray
径向网格 :math:`r_i`,要求严格单调递增且 :math:`r_0>0`。
w : numpy.ndarray
梯形积分权重 :math:`w_i`。
lmax : int
最大角动量量子数 :math:`\ell_{\max}`(决定求解哪些通道)。
mix_alpha : float
密度(或势)混合参数 :math:`\alpha \in (0,1]`。
maxiter : int
最大自洽迭代次数。
tol : float
收敛阈值(默认用于密度无穷范数)。
occ : list[OrbitalSpec] | None
占据方案;若为 ``None`` 则使用 :func:`default_occupations`。
eigs_per_l : int
每个 :math:`\ell` 通道求解的最低本征态数量(需覆盖所有占据的 :math:`n`)。
spin_mode : str
自旋处理模式:"LSDA"(自旋极化,默认)或 "LDA"(自旋非极化,强制 :math:`n_\uparrow = n_\downarrow`)。
"""
Z: int
r: np.ndarray
w: np.ndarray
lmax: int = 3
mix_alpha: float = 0.3
maxiter: int = 200
tol: float = 1e-7
occ: List[OrbitalSpec] | None = None
eigs_per_l: int = 3
eig_solver: str = "fd" # 可选:"fd"(默认)、"fd5"(线性等间距五点)、"transformed"(变量变换方法)
compute_all_l: bool = True
# 计算所有 l 的模式:"always"(每轮都算)、"final"(仅在收敛后补齐)、"none"(只算占据所需)
compute_all_l_mode: str = "final"
# 混合方式:"density" 或 "potential"
mix_kind: str = "density"
# 自适应混合:当残差上升时自动降低混合比,增强收敛稳健性
adapt_mixing: bool = False
mix_alpha_min: float = 0.05
xc: str = "PZ81" # 可选 "PZ81" 或 "VWN"
# 变量变换方法所需参数(仅当 eig_solver="transformed" 时需要)
delta: float | None = None # 网格参数 δ(从 radial_grid_exp_transformed 获取)
Rp: float | None = None # 网格参数 R_p(从 radial_grid_exp_transformed 获取)
# 自旋处理模式:"LSDA"(自旋极化)或 "LDA"(自旋非极化)
spin_mode: str = "LSDA"
# Numerov 相关参数(仅当 eig_solver="numerov" 时使用;None 表示采用默认值)
numerov_samples: int | None = None
numerov_bisection_iter: int | None = None
# 打靶细化(可选开关)
shooting_refine: bool = False
# 需要细化的 (n, l) 通道列表,例如 [(2,1)] 表示 2p
shooting_channels: List[Tuple[int, int]] = None # 初始化在 __post_init__
shooting_E_tol: float | None = None
shooting_max_iter: int | None = None
[文档]
def __post_init__(self):
"""参数验证。"""
if self.spin_mode not in ("LSDA", "LDA"):
raise ValueError(
f"spin_mode 必须为 'LSDA' 或 'LDA',当前值: {self.spin_mode}"
)
if self.shooting_channels is None:
self.shooting_channels = []
[文档]
@dataclass
class SCFResult:
r"""SCF 结果容器。"""
converged: bool
iterations: int
eps_by_l_sigma: Dict[Tuple[int, str], np.ndarray]
u_by_l_sigma: Dict[Tuple[int, str], np.ndarray]
n_up: np.ndarray
n_dn: np.ndarray
v_h: np.ndarray
v_x_up: np.ndarray
v_x_dn: np.ndarray
v_c_up: np.ndarray | None = None
v_c_dn: np.ndarray | None = None
energies: dict | None = None
def _build_effective_potential(
Z: int, r: np.ndarray, n_up: np.ndarray, n_dn: np.ndarray, w: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
r"""由密度构建自旋分辨的有效势 :math:`v_{\text{eff},\sigma}`(LSDA X-only)。
.. math::
v_{\text{eff},\sigma}(r) = v_{\text{ext}}(r) + v_H[n_\uparrow+n_\downarrow](r) + v_x^\sigma[n_\sigma](r).
Parameters
----------
Z : int
原子序数。
r : numpy.ndarray
径向网格。
n_up, n_dn : numpy.ndarray
自旋分辨密度。
w : numpy.ndarray
梯形权重。
Returns
-------
v_eff_up, v_eff_dn : numpy.ndarray
两个自旋通道的有效势。
"""
r_safe = np.maximum(r, 1e-12)
v_ext = -float(Z) / r_safe
n_tot = n_up + n_dn
vH = v_hartree(n_tot, r, w)
vx_up = vx_dirac(n_up)
vx_dn = vx_dirac(n_dn)
return v_ext + vH + vx_up, v_ext + vH + vx_dn
def _build_effective_potential_pz81(
Z: int, r: np.ndarray, n_up: np.ndarray, n_dn: np.ndarray, w: np.ndarray
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
r"""含 PZ81 关联的有效势及分量:返回 (v_up, v_dn, vH, vX_up, vX_dn)。"""
r_safe = np.maximum(r, 1e-12)
v_ext = -float(Z) / r_safe
n_tot = n_up + n_dn
vH = v_hartree(n_tot, r, w)
vx_up = vx_dirac(n_up)
vx_dn = vx_dirac(n_dn)
_, vc_up, vc_dn, _ = lda_c_pz81(n_up, n_dn)
return v_ext + vH + vx_up + vc_up, v_ext + vH + vx_dn + vc_dn, vH, vx_up, vx_dn
def _init_guess_density(
Z: int, r: np.ndarray, w: np.ndarray, occ: list[OrbitalSpec]
) -> tuple[np.ndarray, np.ndarray]:
r"""用外势 :math:`v_{\text{ext}}`(不含相互作用)波函数作为初猜密度。"""
# 求解在纯库仑外势下的径向态(每个 l、两自旋势相同)
v_ext = -float(Z) / np.maximum(r, 1e-12)
n_up = np.zeros_like(r)
n_dn = np.zeros_like(r)
# 按 l 分组需要的最大 n_index
need: dict[tuple[int, str], int] = {}
for spec in occ:
key = (spec.l, spec.spin)
need[key] = max(need.get(key, -1), spec.n_index)
cache_e: dict[int, np.ndarray] = {}
cache_u: dict[int, np.ndarray] = {}
for l, max_n in {
l: max(n for (l2, _), n in need.items() if l2 == l)
for l in set(l for l, _ in need.keys())
}.items():
eps, U = solve_bound_states_fd(r, l=l, v_of_r=v_ext, k=max(max_n + 1, 1))
cache_e[l] = eps
cache_u[l] = U
# 组装密度(m 平均)
r2 = np.maximum(r, 1e-12) ** 2
for spec in occ:
u = cache_u[spec.l][spec.n_index]
contrib = (2 * spec.l + 1) * spec.f_per_m * (u * u) / (4.0 * np.pi * r2)
if spec.spin == "up":
n_up += contrib
else:
n_dn += contrib
return n_up, n_dn
[文档]
def run_lsda_x_only(
cfg: SCFConfig, verbose: bool = False, progress_every: int = 10
) -> SCFResult:
r"""运行 LSDA X-only 的自洽场计算(球对称、径向)。
Parameters
----------
cfg : SCFConfig
自洽计算的配置。
Returns
-------
SCFResult
自洽结果(是否收敛、迭代数、能级/波函数、密度与势)。
Notes
-----
- 占据方案以径向通道为单位(指定 :math:`\ell` 与通道内序号 :math:`n_{\text{index}}`)。
- 对 C 原子,2p 壳层采用 m 平均且自旋极化:上自旋每个 m 的分数占据 :math:`2/3`,下自旋为 0。
- 混合策略:密度线性混合(数值稳健,易用)。
"""
r, w = cfg.r, cfg.w
if cfg.occ is None:
occ = default_occupations(cfg.Z)
else:
occ = cfg.occ
# 初猜密度
n_up, n_dn = _init_guess_density(cfg.Z, r, w, occ)
eps_by_l_sigma: Dict[Tuple[int, str], np.ndarray] = {}
u_by_l_sigma: Dict[Tuple[int, str], np.ndarray] = {}
converged = False
prev_v_up = None
prev_v_dn = None
prev_dn_inf = None
worsen_count = 0
for it in range(1, cfg.maxiter + 1):
# 构势(当前密度)
v_up, v_dn = _build_effective_potential(cfg.Z, r, n_up, n_dn, w)
if cfg.mix_kind == "potential" and prev_v_up is not None:
v_up = (1.0 - cfg.mix_alpha) * prev_v_up + cfg.mix_alpha * v_up
v_dn = (1.0 - cfg.mix_alpha) * prev_v_dn + cfg.mix_alpha * v_dn
# 求解每个需要的 l, spin 通道的本征态
# 确定每个 (l,spin) 所需的最大 n_index
need: dict[tuple[int, str], int] = {}
for spec in occ:
key = (spec.l, spec.spin)
need[key] = max(need.get(key, -1), spec.n_index)
# 确保为可视化与 LUMO 估计计算所有 l, 两自旋的前若干本征值
if cfg.compute_all_l and cfg.compute_all_l_mode == "always":
for l_all in range(0, cfg.lmax + 1):
for spin in ("up", "down"):
need.setdefault((l_all, spin), max(cfg.eigs_per_l - 1, 0))
new_eps_by_l_sigma: Dict[Tuple[int, str], np.ndarray] = {}
new_u_by_l_sigma: Dict[Tuple[int, str], np.ndarray] = {}
for (l, spin), nmax in sorted(need.items()):
k = max(nmax + 1, cfg.eigs_per_l)
v = v_up if spin == "up" else v_dn
eps, U = _solve_channel(cfg, r, l, v, k)
new_eps_by_l_sigma[(l, spin)] = eps
new_u_by_l_sigma[(l, spin)] = U
# 由占据重建新密度
r2 = np.maximum(r, 1e-12) ** 2
n_up_out = np.zeros_like(r)
n_dn_out = np.zeros_like(r)
for spec in occ:
u = new_u_by_l_sigma[(spec.l, spec.spin)][spec.n_index]
contrib = (2 * spec.l + 1) * spec.f_per_m * (u * u) / (4.0 * np.pi * r2)
if spec.spin == "up":
n_up_out += contrib
else:
n_dn_out += contrib
# 混合
if cfg.mix_kind == "density":
n_up_mixed = (1.0 - cfg.mix_alpha) * n_up + cfg.mix_alpha * n_up_out
n_dn_mixed = (1.0 - cfg.mix_alpha) * n_dn + cfg.mix_alpha * n_dn_out
else:
# 潜在的势混合已用于解本征,此处直接替换密度
n_up_mixed = n_up_out
n_dn_mixed = n_dn_out
# LDA 模式:强制自旋非极化(n_up = n_dn = n_total/2)
if cfg.spin_mode == "LDA":
n_total = n_up_mixed + n_dn_mixed
n_up_mixed = n_total / 2.0
n_dn_mixed = n_total / 2.0
# 收敛判断(密度无穷范数 + 电子数守恒)
dn_inf = max(
np.max(np.abs(n_up_mixed - n_up)), np.max(np.abs(n_dn_mixed - n_dn))
)
# 更新状态
n_up, n_dn = n_up_mixed, n_dn_mixed
eps_by_l_sigma = new_eps_by_l_sigma
u_by_l_sigma = new_u_by_l_sigma
if verbose and (it == 1 or it % progress_every == 0):
print(f"[LSDA X-only] iter={it} |dn|_inf={dn_inf:.3e}")
# 自适应混合
if cfg.adapt_mixing:
if prev_dn_inf is not None and dn_inf > prev_dn_inf * 1.05:
worsen_count += 1
else:
worsen_count = 0
if worsen_count >= 2 and cfg.mix_alpha > cfg.mix_alpha_min:
cfg.mix_alpha = max(cfg.mix_alpha * 0.7, cfg.mix_alpha_min)
worsen_count = 0
if verbose:
print(f"[LSDA X-only] adapt mix_alpha -> {cfg.mix_alpha:.3f}")
prev_dn_inf = dn_inf
if dn_inf < cfg.tol:
converged = True
break
prev_v_up, prev_v_dn = v_up, v_dn
# 电子数守恒检查(便于测试)
n_tot = n_up + n_dn
Ne = 4.0 * np.pi * trapz(n_tot * (r**2), r, w)
_ = Ne # 未直接返回,但用于测试断言
# 最终势
v_up, v_dn = _build_effective_potential(cfg.Z, r, n_up, n_dn, w)
vH = v_hartree(n_up + n_dn, r, w)
vx_up = v_up - (-float(cfg.Z) / np.maximum(r, 1e-12)) - vH
vx_dn = v_dn - (-float(cfg.Z) / np.maximum(r, 1e-12)) - vH
return SCFResult(
converged=converged,
iterations=it,
eps_by_l_sigma=eps_by_l_sigma,
u_by_l_sigma=u_by_l_sigma,
n_up=n_up,
n_dn=n_dn,
v_h=vH,
v_x_up=vx_up,
v_x_dn=vx_dn,
)
[文档]
def run_lsda_pz81(
cfg: SCFConfig, verbose: bool = False, progress_every: int = 10
) -> SCFResult:
r"""运行 LSDA(Dirac 交换 + PZ81 关联)的自洽场计算,并给出能量分解。"""
r, w = cfg.r, cfg.w
if cfg.occ is None:
occ = default_occupations(cfg.Z)
else:
occ = cfg.occ
# 初猜密度
n_up, n_dn = _init_guess_density(cfg.Z, r, w, occ)
eps_by_l_sigma: Dict[Tuple[int, str], np.ndarray] = {}
u_by_l_sigma: Dict[Tuple[int, str], np.ndarray] = {}
converged = False
prev_v_up = None
prev_v_dn = None
prev_dn_inf = None
worsen_count = 0
for it in range(1, cfg.maxiter + 1):
v_up, v_dn, _, _, _ = _build_effective_potential_pz81(cfg.Z, r, n_up, n_dn, w)
if cfg.mix_kind == "potential" and prev_v_up is not None:
v_up = (1.0 - cfg.mix_alpha) * prev_v_up + cfg.mix_alpha * v_up
v_dn = (1.0 - cfg.mix_alpha) * prev_v_dn + cfg.mix_alpha * v_dn
# 需要的通道
need: dict[tuple[int, str], int] = {}
for spec in occ:
key = (spec.l, spec.spin)
need[key] = max(need.get(key, -1), spec.n_index)
if cfg.compute_all_l and cfg.compute_all_l_mode == "always":
for l_all in range(0, cfg.lmax + 1):
for spin in ("up", "down"):
need.setdefault((l_all, spin), max(cfg.eigs_per_l - 1, 0))
new_eps_by_l_sigma: Dict[Tuple[int, str], np.ndarray] = {}
new_u_by_l_sigma: Dict[Tuple[int, str], np.ndarray] = {}
for (l, spin), nmax in sorted(need.items()):
k = max(nmax + 1, cfg.eigs_per_l)
v = v_up if spin == "up" else v_dn
eps, U = _solve_channel(cfg, r, l, v, k)
new_eps_by_l_sigma[(l, spin)] = eps
new_u_by_l_sigma[(l, spin)] = U
# 密度
r2 = np.maximum(r, 1e-12) ** 2
n_up_out = np.zeros_like(r)
n_dn_out = np.zeros_like(r)
for spec in occ:
u = new_u_by_l_sigma[(spec.l, spec.spin)][spec.n_index]
contrib = (2 * spec.l + 1) * spec.f_per_m * (u * u) / (4.0 * np.pi * r2)
if spec.spin == "up":
n_up_out += contrib
else:
n_dn_out += contrib
# 混合与收敛
if cfg.mix_kind == "density":
n_up_mixed = (1.0 - cfg.mix_alpha) * n_up + cfg.mix_alpha * n_up_out
n_dn_mixed = (1.0 - cfg.mix_alpha) * n_dn + cfg.mix_alpha * n_dn_out
else:
n_up_mixed = n_up_out
n_dn_mixed = n_dn_out
# LDA 模式:强制自旋非极化(n_up = n_dn = n_total/2)
if cfg.spin_mode == "LDA":
n_total = n_up_mixed + n_dn_mixed
n_up_mixed = n_total / 2.0
n_dn_mixed = n_total / 2.0
dn_inf = max(
np.max(np.abs(n_up_mixed - n_up)), np.max(np.abs(n_dn_mixed - n_dn))
)
n_up, n_dn = n_up_mixed, n_dn_mixed
eps_by_l_sigma = new_eps_by_l_sigma
u_by_l_sigma = new_u_by_l_sigma
if verbose and (it == 1 or it % progress_every == 0):
print(f"[LSDA PZ81] iter={it} |dn|_inf={dn_inf:.3e}")
if cfg.adapt_mixing:
if prev_dn_inf is not None and dn_inf > prev_dn_inf * 1.05:
worsen_count += 1
else:
worsen_count = 0
if worsen_count >= 2 and cfg.mix_alpha > cfg.mix_alpha_min:
cfg.mix_alpha = max(cfg.mix_alpha * 0.7, cfg.mix_alpha_min)
worsen_count = 0
if verbose:
print(f"[LSDA PZ81] adapt mix_alpha -> {cfg.mix_alpha:.3f}")
prev_dn_inf = dn_inf
if dn_inf < cfg.tol:
converged = True
break
prev_v_up, prev_v_dn = v_up, v_dn
# 势与分量
v_up, v_dn, vH, vX_up, vX_dn = _build_effective_potential_pz81(
cfg.Z, r, n_up, n_dn, w
)
v_ext = -float(cfg.Z) / np.maximum(r, 1e-12)
vc_up = v_up - (v_ext + vH + vX_up)
vc_dn = v_dn - (v_ext + vH + vX_dn)
# 能量分解
n_tot = n_up + n_dn
r2 = r * r
e_sum = 0.0
for spec in cfg.occ or default_occupations(cfg.Z):
eps_l_sigma = eps_by_l_sigma[(spec.l, spec.spin)][spec.n_index]
e_sum += (2 * spec.l + 1) * spec.f_per_m * float(eps_l_sigma)
E_H = 0.5 * 4.0 * np.pi * trapz(n_tot * vH * r2, r, w)
e_x = ex_dirac_density(n_up, n_dn)
_, _, _, e_c = lda_c_pz81(n_up, n_dn)
E_x = 4.0 * np.pi * trapz(e_x * r2, r, w)
E_c = 4.0 * np.pi * trapz(e_c * r2, r, w)
E_ext = 4.0 * np.pi * trapz(n_tot * v_ext * r2, r, w)
E_xc_dc = (
4.0
* np.pi
* trapz((vX_up * n_up + vX_dn * n_dn + vc_up * n_up + vc_dn * n_dn) * r2, r, w)
)
E_tot = e_sum - E_H - E_xc_dc + (E_x + E_c)
# 计算 Kohn–Sham 动能 T_s = sum eps_i - ∫ n (v_ext + v_H + v_x + v_c) dr
int_n_v = (
4.0
* np.pi
* trapz(
(
n_tot
* (
v_ext
+ vH
+ vX_up * (n_up / n_tot + 0.0)
+ vX_dn * (n_dn / n_tot + 0.0)
+ vc_up * (n_up / n_tot + 0.0)
+ vc_dn * (n_dn / n_tot + 0.0)
)
)
* r2,
r,
w,
)
if np.all(n_tot > 0)
else 4.0 * np.pi * trapz(n_tot * (v_ext + vH) * r2, r, w)
)
# 更稳健地拆分:∫ n v_x = ∫ (n_up v_x_up + n_dn v_x_dn),相关同理
int_n_vx = 4.0 * np.pi * trapz((n_up * vX_up + n_dn * vX_dn) * r2, r, w)
int_n_vc = 4.0 * np.pi * trapz((n_up * vc_up + n_dn * vc_dn) * r2, r, w)
int_n_v = 4.0 * np.pi * trapz(n_tot * (v_ext + vH) * r2, r, w) + int_n_vx + int_n_vc
T_s = e_sum - int_n_v
energies = dict(
E_total=E_tot,
E_H=E_H,
E_x=E_x,
E_c=E_c,
E_ext=E_ext,
E_sum=e_sum,
E_kin=T_s,
E_coul=E_H,
)
# 若仅在收敛后补齐全部通道,则此处再求一遍所有 (l,spin) 的低能态
if cfg.compute_all_l and cfg.compute_all_l_mode == "final":
all_eps: Dict[Tuple[int, str], np.ndarray] = {}
all_u: Dict[Tuple[int, str], np.ndarray] = {}
for l_all in range(0, cfg.lmax + 1):
for spin in ("up", "down"):
v_sel = v_up if spin == "up" else v_dn
# 复用 _solve_channel,尊重 eig_solver 配置,避免强制 FD
eps, U = _solve_channel(cfg, r, l_all, v_sel, k=max(cfg.eigs_per_l, 1))
all_eps[(l_all, spin)] = eps
all_u[(l_all, spin)] = U
eps_by_l_sigma = all_eps
u_by_l_sigma = all_u
return SCFResult(
converged=converged,
iterations=it,
eps_by_l_sigma=eps_by_l_sigma,
u_by_l_sigma=u_by_l_sigma,
n_up=n_up,
n_dn=n_dn,
v_h=vH,
v_x_up=vX_up,
v_x_dn=vX_dn,
v_c_up=vc_up,
v_c_dn=vc_dn,
energies=energies,
)
[文档]
def run_lsda_vwn(
cfg: SCFConfig, verbose: bool = False, progress_every: int = 10
) -> SCFResult:
r"""运行 LSDA(Dirac 交换 + VWN 关联)的自洽场计算,并给出能量分解。"""
r, w = cfg.r, cfg.w
if cfg.occ is None:
occ = default_occupations(cfg.Z)
else:
occ = cfg.occ
n_up, n_dn = _init_guess_density(cfg.Z, r, w, occ)
eps_by_l_sigma: Dict[Tuple[int, str], np.ndarray] = {}
u_by_l_sigma: Dict[Tuple[int, str], np.ndarray] = {}
converged = False
prev_v_up = None
prev_v_dn = None
prev_dn_inf = None
worsen_count = 0
for it in range(1, cfg.maxiter + 1):
v_up, v_dn, vH, vX_up, vX_dn = _build_effective_potential_vwn(
cfg.Z, r, n_up, n_dn, w
)
if cfg.mix_kind == "potential" and prev_v_up is not None:
v_up = (1.0 - cfg.mix_alpha) * prev_v_up + cfg.mix_alpha * v_up
v_dn = (1.0 - cfg.mix_alpha) * prev_v_dn + cfg.mix_alpha * v_dn
need: dict[tuple[int, str], int] = {}
for spec in occ:
key = (spec.l, spec.spin)
need[key] = max(need.get(key, -1), spec.n_index)
if cfg.compute_all_l and cfg.compute_all_l_mode == "always":
for l_all in range(0, cfg.lmax + 1):
for spin in ("up", "down"):
need.setdefault((l_all, spin), max(cfg.eigs_per_l - 1, 0))
new_eps_by_l_sigma: Dict[Tuple[int, str], np.ndarray] = {}
new_u_by_l_sigma: Dict[Tuple[int, str], np.ndarray] = {}
for (l, spin), nmax in sorted(need.items()):
k = max(nmax + 1, cfg.eigs_per_l)
v = v_up if spin == "up" else v_dn
eps, U = _solve_channel(cfg, r, l, v, k)
new_eps_by_l_sigma[(l, spin)] = eps
new_u_by_l_sigma[(l, spin)] = U
r2 = np.maximum(r, 1e-12) ** 2
n_up_out = np.zeros_like(r)
n_dn_out = np.zeros_like(r)
for spec in occ:
u = new_u_by_l_sigma[(spec.l, spec.spin)][spec.n_index]
contrib = (2 * spec.l + 1) * spec.f_per_m * (u * u) / (4.0 * np.pi * r2)
if spec.spin == "up":
n_up_out += contrib
else:
n_dn_out += contrib
if cfg.mix_kind == "density":
n_up_mixed = (1.0 - cfg.mix_alpha) * n_up + cfg.mix_alpha * n_up_out
n_dn_mixed = (1.0 - cfg.mix_alpha) * n_dn + cfg.mix_alpha * n_dn_out
else:
n_up_mixed = n_up_out
n_dn_mixed = n_dn_out
# LDA 模式:强制自旋非极化(n_up = n_dn = n_total/2)
if cfg.spin_mode == "LDA":
n_total = n_up_mixed + n_dn_mixed
n_up_mixed = n_total / 2.0
n_dn_mixed = n_total / 2.0
dn_inf = max(
np.max(np.abs(n_up_mixed - n_up)), np.max(np.abs(n_dn_mixed - n_dn))
)
n_up, n_dn = n_up_mixed, n_dn_mixed
eps_by_l_sigma = new_eps_by_l_sigma
u_by_l_sigma = new_u_by_l_sigma
if verbose and (it == 1 or it % progress_every == 0):
print(f"[LSDA VWN] iter={it} |dn|_inf={dn_inf:.3e}")
if cfg.adapt_mixing:
if prev_dn_inf is not None and dn_inf > prev_dn_inf * 1.05:
worsen_count += 1
else:
worsen_count = 0
if worsen_count >= 2 and cfg.mix_alpha > cfg.mix_alpha_min:
cfg.mix_alpha = max(cfg.mix_alpha * 0.7, cfg.mix_alpha_min)
worsen_count = 0
if verbose:
print(f"[LSDA VWN] adapt mix_alpha -> {cfg.mix_alpha:.3f}")
prev_dn_inf = dn_inf
if dn_inf < cfg.tol:
converged = True
break
prev_v_up, prev_v_dn = v_up, v_dn
v_up, v_dn, vH, vX_up, vX_dn = _build_effective_potential_vwn(
cfg.Z, r, n_up, n_dn, w
)
v_ext = -float(cfg.Z) / np.maximum(r, 1e-12)
vc_up = v_up - (v_ext + vH + vX_up)
vc_dn = v_dn - (v_ext + vH + vX_dn)
n_tot = n_up + n_dn
r2 = r * r
e_sum = 0.0
for spec in cfg.occ or default_occupations(cfg.Z):
eps_l_sigma = eps_by_l_sigma[(spec.l, spec.spin)][spec.n_index]
e_sum += (2 * spec.l + 1) * spec.f_per_m * float(eps_l_sigma)
E_H = 0.5 * 4.0 * np.pi * trapz(n_tot * vH * r2, r, w)
e_x = ex_dirac_density(n_up, n_dn)
_, _, _, e_c = lda_c_vwn(n_up, n_dn)
E_x = 4.0 * np.pi * trapz(e_x * r2, r, w)
E_c = 4.0 * np.pi * trapz(e_c * r2, r, w)
E_ext = 4.0 * np.pi * trapz(n_tot * v_ext * r2, r, w)
E_xc_dc = (
4.0
* np.pi
* trapz((vX_up * n_up + vX_dn * n_dn + vc_up * n_up + vc_dn * n_dn) * r2, r, w)
)
E_tot = e_sum - E_H - E_xc_dc + (E_x + E_c)
# 计算 Kohn–Sham 动能 T_s
int_n_vx = 4.0 * np.pi * trapz((n_up * vX_up + n_dn * vX_dn) * r2, r, w)
int_n_vc = 4.0 * np.pi * trapz((n_up * vc_up + n_dn * vc_dn) * r2, r, w)
int_n_v = 4.0 * np.pi * trapz(n_tot * (v_ext + vH) * r2, r, w) + int_n_vx + int_n_vc
T_s = e_sum - int_n_v
energies = dict(
E_total=E_tot,
E_H=E_H,
E_x=E_x,
E_c=E_c,
E_ext=E_ext,
E_sum=e_sum,
E_kin=T_s,
E_coul=E_H,
)
# 收敛后补齐所有通道
if cfg.compute_all_l and cfg.compute_all_l_mode == "final":
all_eps: Dict[Tuple[int, str], np.ndarray] = {}
all_u: Dict[Tuple[int, str], np.ndarray] = {}
for l_all in range(0, cfg.lmax + 1):
for spin in ("up", "down"):
v_sel = v_up if spin == "up" else v_dn
# 复用 _solve_channel,尊重 eig_solver 配置,避免强制 FD
eps, U = _solve_channel(cfg, r, l_all, v_sel, k=max(cfg.eigs_per_l, 1))
all_eps[(l_all, spin)] = eps
all_u[(l_all, spin)] = U
eps_by_l_sigma = all_eps
u_by_l_sigma = all_u
return SCFResult(
converged=converged,
iterations=it,
eps_by_l_sigma=eps_by_l_sigma,
u_by_l_sigma=u_by_l_sigma,
n_up=n_up,
n_dn=n_dn,
v_h=vH,
v_x_up=vX_up,
v_x_dn=vX_dn,
v_c_up=vc_up,
v_c_dn=vc_dn,
energies=energies,
)
def _build_effective_potential_vwn(
Z: int, r: np.ndarray, n_up: np.ndarray, n_dn: np.ndarray, w: np.ndarray
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
r"""含 VWN 关联的有效势及分量:返回 (v_up, v_dn, vH, vX_up, vX_dn)。"""
r_safe = np.maximum(r, 1e-12)
v_ext = -float(Z) / r_safe
n_tot = n_up + n_dn
vH = v_hartree(n_tot, r, w)
vx_up = vx_dirac(n_up)
vx_dn = vx_dirac(n_dn)
_, vc_up, vc_dn, _ = lda_c_vwn(n_up, n_dn)
return v_ext + vH + vx_up + vc_up, v_ext + vH + vx_dn + vc_dn, vH, vx_up, vx_dn