GPAW Pipek–Mezey局域轨道
从实际水分子波函数出发,对比两种权重下的占据轨道局域化。
阅读本篇目录
从实际水分子波函数出发,对比两种权重下的占据轨道局域化。
1 下载、提交与运行#
下载完整操作包,保存为gpaw-pipek-mezey.zip。在集群解压后,普通批作业可以执行sbatch job.slurm;下面的直接运行命令用于已经分配好的CPU资源内,不要在登录节点运行生产计算。
python3 -m zipfile -e gpaw-pipek-mezey.zip .
cd gpaw-pipek-mezey
bash setup.sh
bash run.sh
bash verify.sh
bash plot.sh
脚本按1个MPI进程、1个线程设置运行环境。先完成电子结构计算,再提取数据并画图;原始结果在results/,两张图片在plots/。verify.sh用于与附件的原样模型对照;改变体系后应自行检查科学收敛,不能继续沿用原模型数值作为目标。
2 从水分子的占据轨道开始#
固定水分子,PBE、LCAO DZP、0.22 Å网格,共23个基函数,8个价电子形成4个双占据轨道。附件先算一次基态并保存完整ground.gpw,随后读同一检查点,分别进行W和H两种权重的原生Pipek–Mezey局域化,不为每一种权重再做SCF。
关键调用是PipekMezey(calc=c, method=method, seed=42),然后localize(tolerance=1e-8)。W与H决定空间权重函数;本版本H使用原生高斯stockholder权重,不能当作自洽自由原子密度的Hirshfeld电荷分析。
脚本同时处理旧后端LCAO检查点恢复时的重叠矩阵和波函数网格初始化。若自己从GPW提取局域轨道,保留完整恢复步骤;只粘贴两行局域化调用可能缺少所需的网格波函数。
3 局域化是否改变了计算体系#
局域化对完整占据子空间作幺正旋转,改变轨道的表示,保留总占据投影与密度。它不是重新求一个更低总能量的电子态。
| 权重 | 目标函数初值 | 末值 | 迭代次数 |
|---|---|---|---|
| W | 2.154462513 | 2.405197562 | 32 |
| H | 2.749902758 | 2.808461325 | 55 |

同一种权重下目标函数提高可用来观察局域化过程;不同权重的目标函数数值不能直接排名优劣。原始轨道系数和原检查点保留。

图是实际赝轨道强度沿一个方向积分的投影,并标出原子位置。色标作幂次映射帮助显示弱尾部,数值仍来自真实强度,不是全电子电荷密度。轨道符号、排列和简并子空间内方向不是唯一的,所以比较两次局域化应看完整占据子空间、目标函数和相应不变量,不要求每张轨道图逐像素相同。
4 输出和后续使用#
results/W/fields.npz与results/H/fields.npz包含旋转矩阵、局域赝轨道、原始占据轨道、分区权重、原生电荷矩阵和目标函数历史。需要周期材料的Wannier模型时还要单独选择能带子空间、k网格和目标模型,不能把这个Γ点水分子例子直接当成完整周期输运工作流。
完整计算输入#
以下是附件实际执行的run.py,含结构、计算器、提取流程与输出定义。先原样运行,再按上文说明更改模型;inputs.json同时记录示例条件与旧曲线/目标轨道信息。
展开完整Python输入
from pathlib import Path
import os, json, hashlib, signal, numpy as np
from gpaw import GPAW
from gpaw.utilities.partition import AtomPartition
from gpaw.wannier.pipekmezey.pipek_mezey_wannier import PipekMezey
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))
def timeout_handler(*_):
raise TimeoutError('Finite native Pipek-Mezey postprocessing time budget reached')
signal.signal(signal.SIGALRM, timeout_handler)
from ase import Atoms
from gpaw import Mixer
geom = np.load(R.parent / 'input-data/structure.npz')
atoms = Atoms('OH2', positions=geom['positions_A'], cell=geom['cell_A'], pbc=False)
gs = GPAW(legacy_gpaw=True, mode='lcao', basis='dzp', xc='PBE', h=0.22, nbands='nao', spinpol=False, occupations={'name': 'fixed-uniform'}, mixer=Mixer(0.2, 5, 50), convergence=dict(energy=1e-06, density=1e-05, eigenstates=1e-07), maxiter=200, txt=str(R / 'ground-native.txt'))
atoms.calc = gs
atoms.get_potential_energy()
gs.write(str(R / 'ground.gpw'), mode='all')
p['seed'] = str(R / 'ground.gpw')
p['seed_SHA'] = hashlib.sha256((R / 'ground.gpw').read_bytes()).hexdigest()
seed = Path(p['seed'])
assert hashlib.sha256(seed.read_bytes()).hexdigest() == p['seed_SHA']
for method in ['W', 'H']:
D = R / method
D.mkdir()
c = GPAW(str(seed), legacy_gpaw=True, txt=str(D / 'read-only-native.txt'))
c.calculate = lambda *a, **kw: (_ for _ in ()).throw(RuntimeError('No SCF repeat in Pipek-Mezey postprocessing'))
C = c.wfs.kpt_u[0].C_nM.copy()
c.wfs.set_positions(c.spos_ac, AtomPartition(c.wfs.gd.comm, c.wfs.gd.get_ranks_from_positions(c.spos_ac), name='gd'))
c.wfs.initialize_wave_functions_from_lcao()
psi = c.wfs.kpt_u[0].psit_nG[:4].copy()
S = c.wfs.S_qMM[0].copy()
S = np.tril(S) + np.tril(S, -1).T.conj()
signal.alarm(240)
pm = PipekMezey(calc=c, method=method, seed=42)
before = float(pm.get_function_value())
pm.localize(tolerance=1e-08)
signal.alarm(0)
history = np.array(pm.P_n)
after = float(pm.get_function_value())
W = pm.W_k[0].copy()
localized = np.einsum('rn,rxyz->nxyz', W, psi)
gram = (W.T @ C[:4]).conj() @ S @ (W.T @ C[:4]).T
weights = np.array([pm.get_weight_function_atom(i) for i in range(3)])
projector = (abs(psi) ** 2).sum(0)
local_projector = (abs(localized) ** 2).sum(0)
norm_defect = float((abs(1 - weights.sum(0)) * projector).sum() * c.wfs.gd.dv)
Q = np.sum(abs(pm.Qadk_nn), axis=2) / pm.Nk
independent = float(sum(((np.diagonal(Q[:, d], axis1=1, axis2=2) ** 2).sum() * wd for d, wd in enumerate(pm.wd))) / sum(pm.wd))
relative = float(abs(history[-1] - history[-2]) / max(abs(history[-1]), 1e-15))
ck(method + ':four_occupied_orbitals', pm.nocc == 4 and pm.Nk == 1)
ck(method + ':localization_increases_objective', after >= before - 1e-08, value=after - before)
ck(method + ':optimizer_original_relative_tolerance', relative <= 1e-08, value=relative)
ck(method + ':independent_native_objective', abs(independent - after) < 1e-10)
ck(method + ':unitary_rotation', abs(W.T.conj() @ W - np.eye(4)).max() < 1e-10)
ck(method + ':PAW_metric_preserved', abs(gram - np.eye(4)).max() < 1e-08)
ck(method + ':occupied_projector_fullgrid_preserved', abs(local_projector - projector).max() < 1e-10)
ck(method + ':density_weighted_partition_defect', norm_defect < 0.001, value=norm_defect)
ck(method + ':weights_bounds', weights.min() > -1e-12 and weights.max() < 1 + 1e-12)
ck(method + ':finite_actual_fields', np.isfinite(localized).all() and np.isfinite(pm.Qadk_nn).all())
ck(method + ':original_coefficients_unchanged', np.array_equal(C, c.wfs.kpt_u[0].C_nM))
np.savez_compressed(D / 'fields.npz', rotation=W, canonical_occupied_pseudo_orbitals=psi, localized_pseudo_orbitals=localized, occupied_projector=projector, localized_projector=local_projector, PAW_Gram=gram, partition_weights=weights, native_charge_matrices=pm.Qadk_nn, charge_matrix_before_rotation=pm.Qadk_nm, direction_weights=pm.wd, objective_history=history, cell_A=c.atoms.cell.array, positions_A=c.atoms.positions, grid_spacing_Bohr=c.wfs.gd.h_cv)
rows.append(dict(method=method, before=before, after=after, relative_last_step=relative, iterations=pm.niter, occupied_orbitals=pm.nocc, partition_defect=norm_defect))
(R / 'PARTIAL-RESULTS.json').write_text(json.dumps(rows, indent=2) + '\n')
print(method, before, after, pm.niter, flush=True)
ck('immutable_original_checkpoint', hashlib.sha256(seed.read_bytes()).hexdigest() == p['seed_SHA'])
for f, h in p['source_SHA'].items():
ck('source_immutable:' + f, hashlib.sha256(Path(f).read_bytes()).hexdigest() == h)
v = dict(passed=all((x['passed'] for x in checks)), checks=len(checks), items=checks, results=rows, whole_group_complete=False, SCF_repeats=0, H_uses_native_gaussian_stockholder_not_selfconsistent_free_atom_Hirshfeld=True, no_unique_orbital_phase_or_permutation_claim=True)
(R / 'VALIDATION.json').write_text(json.dumps(v, indent=2) + '\n')
print('414', v['passed'], v['checks'], 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 -c "import numpy as np; d=np.load('results/H/fields.npz'); print([(k, d[k].shape) for k in d.files])"
ls -lh 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数据集或二进制检查点,运行时使用集群已安装数据集。