GDF periodic-SCF handover — compcell + AFT state (2026-05-20)¶
⚠️ TARGET MOVED — v0.9.0 → v0.10.0 (Mike, 2026-05-20). This file keeps its
_v0_9_filename for link stability, but the GDF chain’s release target is now v0.10.0, at the earliest. Rationale: GDF still does not reach PySCF parity on real systems (LiH primitive FCC +22 Ha, MgO 8-atom conv cell ~+1500 Ha — see TL;DR below). It has now been deferred twice (v0.8.0 → v0.9.0 → v0.10.0). There is no schedule pressure on this track — the next GDF chat should treat correctness as the only gate and take the time it needs. Do not let a future release deadline force a half-validated GDF into a tag; the v0.8.0 prep-doc reconciliation (overstated GDF maturity) is the cautionary precedent. The Ewald-3D periodic stack remains vibe-qc’s shipping periodic surface in the meantime; the BIPOLE route is the nearer-term periodic-correlated path. When GDF does reach parity, the release chat picks it up for whatever minor is current then.
This handover documents the state of the GDF route of vibe-qc’s
periodic SCF re-implementation (the parallel route to BIPOLE; see
docs/handover_bipole_v0_8_2026_05_18.md for that one). It is
written so another chat can pick the work up cold.
GDF is the Gaussian-density-fitting periodic SCF stack whose goal is
to reproduce PySCF’s KRHF + .density_fit() energies to µHa on
real systems. Release target: v0.10.0 at the earliest (deferred
from v0.9.0 on 2026-05-20; originally slated for v0.8.0).
TL;DR status¶
H2/STO-3G (Γ-only and multi-k): sub-mHa parity vs PySCF. Done.
LiH primitive FCC (2-atom, multi-k kmesh=2,2,2, STO-3G): SCF converges, lands +22 Ha above PySCF at full numerical convergence. Bug localized (see below) — NOT yet at parity.
MgO 8-atom conv cell (Γ-only): ~+1500 Ha off PySCF. Separate, larger-signature bug. Not yet diagnosed.
The compcell construction, exxdiv=’ewald’ Madelung shift, multi-k
Bloch assembly, AFT correction (now at any q), and pair-FT are all
implemented and on origin/main. The remaining gap is a convention
bug in the bare 3c lattice sum or chg single-AO FT for L≥1 shells.
What’s complete on origin/main¶
Drivers¶
vibeqc.run_pbc_gdf_rhf(python/vibeqc/pbc_gdf.py) — Γ-only RHF compcell driver.PBCMethod/PBCExxDivenums,PBCGDFResultdataclass. Hardening: rejects dim≠3, kmesh≠(1,1,1), charged cells; energy-sanity post-condition.vibeqc.run_krhf_periodic_gdf(python/vibeqc/periodic_k_gdf.py) — multi-k GDF driver. Kwargs:use_compcell,compcell_eta,apply_aft_correction,aft_ft_convention,aft_precision,rcut_strategy,rcut_precision,exxdiv.
Compcell GDF pipeline (python/vibeqc/aux_basis.py)¶
Sun 2017 compensated-charge construction — fixes the bare-aux divergence on diffuse JKfit auxiliaries:
make_modrho_aux_basis— modrho-renormalised aux.make_compensating_basis— chg compensating Gaussians (η knob).make_fused_basis+fuse_transform_matrix— fused basis + A.build_lpq_compcell— Γ-only compcell Lpq.build_lpq_bloch_compcell— multi-k compcell Lpq(k) via cell-resolved blocks + Bloch phases.
AFT long-range correction (PySCF _CCGDFBuilder)¶
_compcell_aft_correction— 2c j2c_p subtraction._compcell_aft_correction_3c— 3c add_ft_j3c subtraction.Both now accept an optional
qparameter (commit landed 2026-05-20). For q=0 they keep the historical real-valued Γ-only path bit-exactly; for q≠0 they use the q-shifted kernel4π/|G+q|² · F̂(G+q)and return complex. This lifted the Γ-only restriction on AFT for multi-k.
exxdiv=’ewald’ (python/vibeqc/madelung.py)¶
madelung_constant_for_cell,apply_exxdiv_ewald_to_K,exxdiv_ewald_energy_shift(closed-shell-¼·ξ·tr(D·S·D·S)).Multi-k K-shift:
K(k) += ξ·S(k)·D(k)·S(k)per k.
rcut auto-tuning (python/vibeqc/lattice_screening.py)¶
RcutStrategyenum (FLAT,PYSCF_AUTO),estimate_rcut_pyscf_per_shell,make_lattice_opts.PySCF-style per-shell
rcut = √(max(log_prec/(-α), 0.1)).
Tests¶
tests/test_pbc_gdf_compcell.py — 25 tests, all green. Covers
compcell metric plateau, AFT shape/symmetry, q=None≡q=0 parity,
q≠0 complex + Hermitian-under-q→-q, H2 sub-mHa (Γ + multi-k),
rcut strategies, driver hardening.
The three convention bugs found + fixed this session (2026-05-20)¶
1. AFT only worked at q=0 → now works at any q¶
build_lpq_bloch_compcell used to raise NotImplementedError for
apply_aft_correction=True at k≠0. Implemented the q-shifted FT
kernel. Result on LiH primitive multi-k: bare compcell diverged at
+10,582 Ha (not converged); AFT-q converges in 9 iter.
Commit: aux_basis: AFT compcell correction at q != 0.
2. Pair-FT (+iG)^t sign bug → fixed to (-iG)^t¶
_aopair_ft.cartesian_gaussian_product_ft used (+iG_x)^t in the
McMurchie-Davidson Hermite-chain FT. The standard identity is
F[H_t(r-P;γ)](G) = (-iG_x)^t · … because
(∂/∂P_x)^t exp(-iG·P) = (-iG_x)^t exp(-iG·P).
For same-center pairs only t=L_total survives; the odd-L
sign flip was being patched by a (-1)^L per-AO factor in
_per_ao_pair_libint_to_libcint_scale. For off-center pairs
both even-t and odd-t E-coefficient terms contribute and the
blanket per-AO sign over-corrected the even-t parts.
Two-part fix: (-iG)^t in cartesian_gaussian_product_ft +
dropped the (-1)^L sign in _per_ao_pair_libint_to_libcint_scale
(kept only the 1/√(4π/(2L+1)) magnitude — Y_lm normalization).
Verification: examples/debug/gdf_pair_ft_pyscf_diff.py —
all 16 LiH STO-3G shell-pair pair-FT blocks now match PySCF’s
ft_ao.ft_aopair to 1e-15 (was 1.58 rel-err on the off-center
Li 2p × H 1s block).
Commit: aopair_ft: fix (+iG)^t → (-iG)^t in pair-FT.
3. LiH residue localized to bare 3c / chg single-AO FT¶
After fixes 1+2, LiH primitive multi-k still had a residue. A joint precision sweep (3c-rcut × AFT-G-mesh) revealed it is non-monotonic — tightening convergence makes it WORSE:
mode |
Δ vs PySCF |
iters |
|---|---|---|
bare (no AFT) |
+10,582 Ha |
50 (✗) |
AFT / flat 30-bohr cutoff |
+109 Ha |
9 |
AFT / auto rc=1e-8 ε=1e-10 |
+3.6 Ha |
21 |
AFT / auto rc=1e-10 ε=1e-12 |
+16.4 Ha |
8 |
AFT / auto rc=1e-12 ε=1e-14 |
+22.4 Ha |
8 |
The rc=1e-8 “good” number was a coincidental cancellation from
under-converged image sums (note the 21-iter struggle). At full
convergence the true residue is +22 Ha. Per CLAUDE.md §7 this
is a bug, not a convergence-aid problem.
H2 doesn’t exercise this (L=0/L=0 only), which is why H2 sub-mHa parity is preserved.
4. (2026-05-20 cont.) Element-wise AFT diff — FT convention pinned¶
examples/debug/gdf_aft_pyscf_diff_lih.py (new) compares the
compcell-AFT intermediates against a PySCF _CCGDFBuilder
subprocess element-wise. Two distinct bugs isolated:
Bug 4a — _aft_fourier_transform_libcint_convention mis-scaled.
Probing F̂_fused at G = b0, b1, b0+b1 vs PySCF’s ft_ao:
rsgdf_aux_fourier_transform== PySCFft_aobit-exactly for all L (ratio +1.00000 at L=0,1,2,3), once the L=1 (py,pz,px) → libcint (px,py,pz) m-permutation is applied. This is the TRUE PySCF / libcint FT convention._aft_fourier_transform_libcint_conventionis mis-named: it does NOT match PySCF’sft_ao. It is off by exactly√((2L+1)/(4π))per shell — L=0: 0.28209; L=1: 0.48860; L=2: 0.63078; L=3: 0.74635. It equalsrsgdf / √(4π/(2L+1)).Consequence:
_compcell_aft_correctionwith the defaultft_convention="libcint"builds j2c_p from the mis-scaled FT → j2c_p comes out ~10× too small (block-norm ratio ≈ 0.10 vs PySCF).
Bug 4b — bare M_fused integral-output normalization mismatch.
The bare 2c metric compute_2c_eri_lattice on the fused
(modrho_aux ∪ chg) basis does NOT match PySCF’s int2c2e on the
same basis (ratio: LiH L=0 diag 0.748, L=1 diag 1.0; system-
dependent overall).
IMPORTANT — the fused BASIS is correct. The shell-by-shell
parity check (gdf_aft_pyscf_diff_lih.py) confirms: all exponents
match PySCF bit-for-bit (0 mismatches / 30 shells), and — decisive
— rsgdf_aux_fourier_transform == PySCF ft_ao bit-exactly for all
L proves the basis FUNCTIONS χ(r) are identical (the FT uniquely
determines the function). The per-shell coefficient numbers
differ, but that is only the libint-vs-libcint internal contraction-
normalization representation — two encodings of the same χ(r).
So Bug 4b is NOT a basis-construction bug.
Bug 4b — the bare-M_fused mismatch is a lattice-sum cutoff
artifact, NOT a compute_2c_eri_lattice bug. This corrects the
overstated “structural cross-L bug” framing of commit b43e5b3.
The decisive isolation (gdf_aft_pyscf_diff_lih.py, the
MOLECULAR-vs-LATTICE per-(region,L) grids):
MOLECULAR 2c metric — vibe-qc
compute_2c_erivs PySCFfused_cell.intor('int2c2e')(single cell, no lattice sum): every (region, L_P, L_Q) block matches at ratio 1.00000. The basic libint 2c call is bit-perfect, all L.LATTICE 2c metric — vibe-qc
compute_2c_eri_latticevs PySCFpbc_intor('int2c2e'): the weird non-factorizable grid ((0,2)=0.04,(1,2)=0.17,(2,3)=2.4, …).
The key realization: the bare fused 2c metric is a
conditionally-convergent lattice sum. The modrho_aux block carries
net charge — Σ_g (P_0|Q_g) for the aux block does NOT converge;
only the charge-neutral (modrho_aux − chg) combination (i.e. the
compensated metric, or the AFT-corrected metric) converges. So
comparing the raw bare M_fused at vibe-qc’s cutoff_bohr=30 vs
PySCF’s own cell.rcut compares two different partial sums of a
(near-)divergent series — and different L-blocks converge at
different rates, which is exactly the non-factorizable grid seen.
Each individual image term (P_0|Q_g) vibe-qc computes IS correct
(it is a molecular integral at a shifted centre, and compute_2c_eri
shift_shellsare bit-perfect).compute_2c_eri_latticeis not buggy. The bare-metric comparison was simply the wrong thing to compare.
The quantity that must match PySCF is the compensated, AFT-
corrected metric A · (M_fused − j2c_p) · Aᵀ, which is the
convergent one. With Bug 4a fixed (correct FT scale) the AFT
subtraction makes the (cutoff-dependent) bare metric into a
(cutoff-independent) compensated metric.
Why these can’t be fixed independently. Bug 4a and Bug 4b
currently partially cancel. The H2 “sub-mHa” result uses
ft_convention="libint" (the correct rsgdf routine) at the one
η=1.0 where the bare-M_fused error happens to cancel. Fixing Bug 4a
alone (making “libcint” == “libint” == correct) makes the multi-k
default diverge on LiH, because it exposes Bug 4b. The two must be
fixed together with full SCF verification at every step.
Next steps (open tasks)¶
Task #28 — fix Bug 4a; compare the convergent compensated metric¶
compute_2c_eri_lattice is NOT buggy — the bare-metric mismatch is
a conditionally-convergent-lattice-sum cutoff artifact (see Bug 4b
above; the MOLECULAR 2c metric matches PySCF bit-exactly for every
L-block, ratio 1.0). The actual remaining work:
Fix Bug 4a.
_aft_fourier_transform_libcint_conventionis off by√((2L+1)/(4π))per L vs PySCFft_ao. The proven- correct routine isrsgdf_aux_fourier_transform(== PySCFft_aobit-exactly, all L). Route_compcell_aft_correction’sft_convention="libcint"path throughrsgdf_aux_fourier_transform(or add the missing√(4π/(2L+1))per-shell scale to_aft_fourier_transform_libcint_convention).Compare the CONVERGENT quantity. Extend
gdf_aft_pyscf_diff_lih.pyto compare the compensated AFT- corrected metricA·(M_fused − j2c_p)·Aᵀvs PySCF’s_CCGDFBuilder.get_2c2e()output — that is the cutoff- independent target. The bare M_fused per-L grid is NOT a meaningful target (it is a near-divergent sum; the modrho_aux block carries net charge, only the modrho−chg compensated combination converges).Verify cutoff-independence of the compensated metric as
cutoff_bohrgrows (15 → 30 → 45). If it drifts, the AFT mesh (aft_precision) and the lattice cutoff are not matched.
Verify with the H2 sub-mHa tests AND the LiH primitive multi-k bench at every step. Target: LiH primitive multi-k < 1 mHa vs PySCF.
Task #27 — 3c-tensor element-wise PySCF diff (after #28)¶
Analogous element-wise diff for the bare 3c tensor T[P, μ, ν]
(compute_3c_eri_lattice vs PySCF int3c2e_sph). Lower priority
than #28 — fix the 2c metric first, then re-measure the 3c residue.
Task #25 — MgO conv-cell ~1500 Ha residue¶
MgO 8-atom conv cell Γ-only is ~+1500 Ha off PySCF (η-sweep flat across 0.3–2.0). Different signature from LiH (def2-svp-jkfit on Mg/O has higher-L aux shells). May share a root cause with #27 or be a distinct higher-L issue. Diagnose after #27.
Reproduction / environment¶
The worktree’s editable install points at a _vibeqc_core.so built
from the main checkout’s third_party/, so you must set the
dyld fallback path:
export DYLD_FALLBACK_LIBRARY_PATH=\
<main-checkout>/third_party/libint/install/lib:\
<main-checkout>/third_party/libxc/install/lib:\
<main-checkout>/third_party/spglib/install/lib:\
<main-checkout>/third_party/fftw/install/lib:\
<main-checkout>/third_party/libecpint/install/lib
PySCF subprocess runner (CLAUDE.md §10 — PySCF is never
imported inside python/vibeqc/; parity scripts spawn it):
export VIBEQC_PYSCF_PYTHON=<a-venv-with-pyscf>/bin/python
Key debug scripts under examples/debug/:
gdf_pair_ft_pyscf_diff.py— pair-FT element-wise diff (the diagnostic that cracked bug 2). Now bit-parity.gdf_lih_primitive_multik_parity.py— LiH primitive multi-k bench with the joint precision sweep.gdf_aft_calibration.py— L>0 single-AO FT convention.gdf_bare_aux_divergence.py— bare-aux divergence + compcell plateau demo.
Run the compcell test suite:
cd <worktree> && python -m pytest tests/test_pbc_gdf_compcell.py -q
Discipline notes (CLAUDE.md)¶
§7 — periodic SCF landing at impossible energies is a bug; don’t paper over with damping / level-shift / empirical thresholds. The +22 Ha LiH residue is being chased as a bug.
§10 — never
import pyscfinsidepython/vibeqc/. All parity validation is out-of-process subprocess runners.§2 — rebase-then-push; never force-push main.