GPAW线性展开ΔSCF激发态
用CO分子演示AEOrbital与MolecularOrbital、约束电子和垂直激发能。
阅读本篇目录
用CO分子演示AEOrbital与MolecularOrbital、约束电子和垂直激发能。
1 下载、提交与运行#
下载完整操作包,保存为gpaw-linear-dscf.zip。在集群解压后,普通批作业可以执行sbatch job.slurm;下面的直接运行命令用于已经分配好的CPU资源内,不要在登录节点运行生产计算。
python3 -m zipfile -e gpaw-linear-dscf.zip .
cd gpaw-linear-dscf
bash setup.sh
bash run.sh
bash verify.sh
bash plot.sh
脚本按1个MPI进程、1个线程设置运行环境。先完成电子结构计算,再提取数据并画图;原始结果在results/,两张图片在plots/。verify.sh用于与附件的原样模型对照;改变体系后应自行检查科学收敛,不能继续沿用原模型数值作为目标。
2 这个例子算什么#
固定CO分子,四周3.5 Å真空;PBE、LCAO DZP、0.25 Å网格、8条带、0.01 eV费米展宽、自旋极化、关闭对称性。不优化几何,因此激发能是同一几何上的垂直ΔSCF能量差,不是吸收光谱峰位。
顺序为基态→AEOrbital受激态→MolecularOrbital受激态。它是线性展开ΔSCF,不是MOM。AE分支使用附件中从真实基态第5号带(从0编号)提取的CO 2π参考轨道和PAW投影;参考是随包提供的数值数据,不是每次自动从新基态任意选一个简并轨道。
dscf.dscf_calculation(ce, [[1.0, orb, 1]], atoms)表示在自旋1的目标线性组合轨道放入1个约束电子。普通KS占据留下9个电子,另1个由约束部分计入,总价电子仍为10。只把普通占据加起来会漏掉这个电子。
MolecularOrbital使用两原子局域投影权重{0:[0,0,0,1], 1:[0,0,0,-1]};数组顺序与所用PAW投影通道有关,不可照搬到任意元素。两个受激态用耦合全自旋Pulay混合;基态能量/密度/本征态收敛为1E−6/1E−5/1E−7,AE受激态收紧到1E−8/1E−7/1E−9,全部带参与本征态判据。
3 实际能量与电子数#
| 状态 | 能量/eV | 相对基态/eV | 普通KS+约束价电子 |
|---|---|---|---|
| 基态 | −12.9827194211 | 0 | 10+0 |
| AEOrbital | −7.2114289534 | 5.7712904677 | 9+1 |
| MolecularOrbital | −7.2114377347 | 5.7712816865 | 9+1 |

两受激态磁矩约−2.00008 μB;不能据此宣称已做自旋纯化或得到了纯单重态。CO的π简并空间允许不同方向的线性组合,能量接近不意味着两张密度差切片必须逐点相同。

红蓝分别表示相对基态密度增加与减少,坐标和色标来自实际计算。这个例子不提供受激态结构优化、原子力或振子强度。
4 换自己的体系#
先完成自己的基态,选择有物理依据的目标轨道,提取波函数和PAW投影,再构造AEOrbital。附带make_target.py可从本次results/ground.gpw提取同编号参考,输出results/my-reference-orbital.npz;此命令只提取数据,不会重跑SCF,也不会自动覆盖默认参考。换结构时参考必须匹配晶胞、原子顺序、网格和PAW设置,并同步调整run.py中的参考文件与几何检查。
参考:GPAW ΔSCF教程与原生接口。
完整计算输入#
以下是附件实际执行的run.py,含结构、计算器、提取流程与输出定义。先原样运行,再按上文说明更改模型;inputs.json同时记录示例条件与旧曲线/目标轨道信息。
展开完整Python输入
from pathlib import Path
import os, json, hashlib, numpy as np
from ase.build import molecule
from ase.units import Hartree
from gpaw import GPAW, Mixer, FermiDirac
from gpaw import dscf
R = Path(__file__).resolve().parent / 'results'
R.mkdir()
p = json.loads((R.parent / 'inputs.json').read_text())
checks = []
rows = []
def ck(n, b, **kw):
checks.append(dict(name=n, passed=bool(b), **kw))
for f, h in p['source_SHA'].items():
assert hashlib.sha256(Path(f).read_bytes()).hexdigest() == h
a = molecule('CO')
a.center(vacuum=3.5)
a.pbc = False
def calc(name):
return GPAW(legacy_gpaw=True, mode='lcao', basis='dzp', h=0.25, xc='PBE', nbands=8, spinpol=True, symmetry='off', occupations=FermiDirac(0.01), mixer={'backend': 'pulay', 'nmaxold': 6, 'beta': 0.1, 'weight': 1} if name.startswith('excited') else Mixer(0.05, 6, 50), convergence=dict(energy=1e-08, density=1e-07, eigenstates=1e-09, bands='all') if name == 'excited-AE' else dict(energy=1e-06, density=1e-05, eigenstates=1e-07, bands='all'), maxiter=400, txt=str(R / (name + '-native.txt')))
def extract(name, c, energy):
spin = np.array([c.get_all_electron_density(spin=s, gridrefinement=1) for s in [0, 1]])
total = c.get_all_electron_density(gridrefinement=1)
integral = float(total.sum() * a.get_volume() / total.size)
ks = float(sum((k.f_n.sum() for k in c.wfs.kpt_u)))
ne = []
coefs = []
grams = []
eps = []
norms = []
delta = 0.0
manual = 0.0
for k in c.wfs.kpt_u:
C = k.C_nM
S = c.wfs.S_qMM[k.q].copy()
S = np.tril(S) + np.tril(S, -1).T.conj()
gram = C.conj() @ S @ C.T
grams.append(gram)
eps.append(k.eps_n * Hartree)
ck(name + ':spin' + str(k.s) + ':PAW_metric', abs(gram - np.eye(8)).max() < 1e-06, value=float(abs(gram - np.eye(8)).max()))
if hasattr(k, 'ne_o'):
ne.append(k.ne_o.copy())
coefs.append(k.c_on.copy())
cn = np.sum(abs(k.c_on) ** 2, axis=1)
norms.append(cn)
delta += float(np.dot(k.ne_o, cn))
manual += float(sum((n * np.dot(abs(v) ** 2, k.eps_n) for n, v in zip(k.ne_o, k.c_on))))
ck(name + ':spin' + str(k.s) + ':linear_expansion_normalized', abs(cn - 1).max() < 1e-08)
ck(name + ':AE14', abs(integral - 14) < 0.03, value=integral)
ck(name + ':KS_plus_constraint_valence10', abs(ks + delta - 10) < 1e-07, value=ks + delta)
ck(name + ':two_spin_density_sum', abs(spin.sum(0) - total).max() < 1e-12)
ck(name + ':finite', np.isfinite([energy, integral, ks, delta]).all() and np.isfinite(total).all())
mag = float((spin[0] - spin[1]).sum() * a.get_volume() / total.size)
ck(name + ':density_spin_matches_reported', abs(mag - c.get_magnetic_moment()) < 0.03, value=mag)
if name.startswith('excited'):
ck('native_DSCF_band_energy_identity', abs(c.wfs.occupations.calculate_band_energy(c.wfs) - manual) < 1e-10)
ck('constrained_electron1', abs(delta - 1) < 1e-08)
ck('ordinary_KS_electrons9', abs(ks - 9) < 1e-07)
np.savez_compressed(R / (name + '-fields.npz'), AE_spin_density_e_A3=spin, AE_total_density_e_A3=total, cell_A=a.cell.array, positions_A=a.positions, eigenvalues_eV=np.array(eps), KS_weighted_occupations=np.array([k.f_n for k in c.wfs.kpt_u]), PAW_Gram=np.array(grams), constraint_electrons=np.array(ne), linear_expansion_coefficients=np.array(coefs), linear_expansion_norms=np.array(norms))
row = dict(name=name, energy_eV=energy, AE_electrons=integral, ordinary_KS_valence=ks, constrained_valence=delta, total_valence=ks + delta, magnetic_moment=mag, iterations=c.get_number_of_iterations())
rows.append(row)
(R / 'PARTIAL-RESULTS.json').write_text(json.dumps(rows, indent=2) + '\n')
print(row, flush=True)
c = calc('ground')
a.calc = c
eg = float(a.get_potential_energy())
c.write(str(R / 'ground.gpw'), mode='all')
extract('ground', c, eg)
target = np.load(R.parent / 'input-data/reference-orbital.npz')
assert np.allclose(a.positions, target['positions_A'], atol=1e-12, rtol=0) and np.allclose(a.cell.array, target['cell_A'], atol=1e-12, rtol=0)
wf = list(target['pseudo_target_by_spin'])
proj = [{atom: target[f'projector_spin{spin}_atom{atom}'].copy() for atom in range(2)} for spin in range(2)]
for name, kind in [('excited-AE', 'AE'), ('excited-Molecular', 'Molecular')]:
ce = calc(name)
ce.initialize(a)
if kind == 'AE':
orb = dscf.AEOrbital(ce, wf, proj, txt=str(R / 'expansion-overlap.log'))
else:
orb = dscf.MolecularOrbital(ce, weights={0: [0.0, 0.0, 0.0, 1.0], 1: [0.0, 0.0, 0.0, -1.0]})
dscf.dscf_calculation(ce, [[1.0, orb, 1]], a)
a.calc = ce
ee = float(a.get_potential_energy())
extract(name, ce, ee)
ck(name + ':positive_vertical_excitation', 0 < ee - eg < 20, value=ee - eg)
for f, h in p['source_SHA'].items():
ck('source_immutable:' + f, hashlib.sha256(Path(f).read_bytes()).hexdigest() == h)
ck('AE_and_Molecular_excitation_consistency', abs(rows[1]['energy_eV'] - rows[2]['energy_eV']) < 0.02)
v = dict(passed=all((x['passed'] for x in checks)), checks=len(checks), items=checks, results=rows, summary=dict(vertical_excitation_eV={r['name']: r['energy_eV'] - eg for r in rows[1:]}), whole_group_complete=False, no_pure_singlet_or_triplet_claim=True, not_MOM=True)
(R / 'VALIDATION.json').write_text(json.dumps(v, indent=2) + '\n')
print('402', v['passed'], v['checks'], v['summary'], flush=True)
assert v['passed']
终端查看结果#
计算后先查看逐项结果,再打开图。下面只读取输出,不会启动新的自洽:
python3 -c "import json; v=json.load(open('results/VALIDATION.json')); print(v.get('results')); print(v.get('summary', {}))"
source env.sh
python3 make_target.py
ls -lh results/my-reference-orbital.npz plots/*.png
环境:GPAW26.7.0、GCC13.3、OpenMPI5.0.10rc2、MKL2023.2、Libxc7.0、Python3.11.16。模块为gpaw/26.7.0-8173m;实际输入由Python/ASE组织,使用附件的env.sh加载一致环境。附件不打包PAW数据集或二进制检查点,运行时使用集群已安装数据集。